diff --git a/src/epmodel/ScheduleInterval/ScheduleFile.cpp b/src/epmodel/ScheduleInterval/ScheduleFile.cpp index edadfe8f12..d97e2ea770 100644 --- a/src/epmodel/ScheduleInterval/ScheduleFile.cpp +++ b/src/epmodel/ScheduleInterval/ScheduleFile.cpp @@ -13,15 +13,44 @@ #include #include #include +#include +#include + +#include "../utilities/data/TimeSeries.hpp" namespace openstudio { namespace epmodel { - ScheduleFile::ScheduleFile(const Model& model) : Schedule(ScheduleFile::iddObjectType(), model) { + // ScheduleFile(const ExternalFile& externalfile, int column = 1, int rowsToSkip = 0) : Schedule(ScheduleFile::iddObjectType(), model) {} + + ScheduleFile::ScheduleFile(const Model& model, int column, int rowsToSkip) : Schedule(ScheduleFile::iddObjectType(), model) { // Mirror preserved counterpart constructor behavior for required scalar fields. bool ok = true; - ok &= setColumnNumber(1); - ok &= setRowstoSkipatTop(0); + ok &= setColumnNumber(column); + ok &= setRowstoSkipatTop(rowsToSkip); + OS_ASSERT(ok); + } + + ScheduleFile::ScheduleFile(const Model& model, const openstudio::path& filePath, int column, int rowsToSkip, bool translateFileWithRelativePath) + : Schedule(ScheduleFile::iddObjectType(), model) { + + openstudio::path p; + if (!exists(filePath)) { + this->remove(); + LOG_FREE_AND_THROW("openstudio.epmodel.ScheduleFile", "Cannot find file \"" << toString(filePath) << "\" for " << briefDescription()); + } else { + if (translateFileWithRelativePath) { + p = filePath; + } else { + // make the path correct for this system + p = system_complete(filePath); + } + } + + bool ok = true; + ok &= getImpl()->setFileName(toString(p)); + ok &= setColumnNumber(column); + ok &= setRowstoSkipatTop(rowsToSkip); OS_ASSERT(ok); } @@ -39,6 +68,14 @@ namespace epmodel { return getIddKeyNames(IddFactory::instance().getObject(iddObjectType()).get(), openstudio::Schedule_FileFields::MinutesperItem); } + // std::string ScheduleFile::fileName() const { + // return getImpl()->fileName(); + // } + + // bool ScheduleFile::setFileName(std::string fileName) { + // return getImpl()->setFileName(fileName); + // } + int ScheduleFile::columnNumber() const { return getImpl()->columnNumber(); } @@ -139,6 +176,67 @@ namespace epmodel { getImpl()->resetAdjustScheduleforDaylightSavings(); } + boost::optional ScheduleFile::csvFile() const { + return getImpl()->csvFile(); + } + + bool ScheduleFile::translateFileWithRelativePath() const { + DEPRECATED_AT_MSG(4, 0, 0, "Schedule:File is no longer 'translated'."); + return false; + } + + bool ScheduleFile::isTranslateFileWithRelativePathDefaulted() const { + DEPRECATED_AT_MSG(4, 0, 0, "Schedule:File is no longer 'translated'."); + return false; + } + + bool ScheduleFile::setTranslateFileWithRelativePath(bool translateFileWithRelativePath) { + DEPRECATED_AT_MSG(4, 0, 0, "Schedule:File is no longer 'translated'."); + return false; + } + + void ScheduleFile::resetTranslateFileWithRelativePath() { + DEPRECATED_AT_MSG(4, 0, 0, "Schedule:File is no longer 'translated'."); + } + + openstudio::path ScheduleFile::translatedFilePath() const { + return getImpl()->translatedFilePath(); + } + + boost::optional ScheduleFile::fromTimeSeries(const openstudio::TimeSeries& timeSeries, Model& model) { + boost::optional result; + + boost::optional intervalTime = timeSeries.intervalLength(); + if (intervalTime) { + result = ScheduleFile(model, 2); // FT ScheduleFixedInterval wrote the dateTimes to file + const std::string name = result->nameString(); + openstudio::path filePath = toPath(name + ".csv"); + + CSVFile csvFile; + csvFile.addColumn(timeSeries.dateTimes()); + csvFile.addColumn(timeSeries.values()); + csvFile.saveAs(filePath); + + openstudio::path p; + if (!exists(filePath)) { + result->remove(); + // LOG_AND_THROW("Cannot find file \"" << toString(filePath) << "\" for " << briefDescription()); + } else { + // make the path correct for this system + p = system_complete(filePath); + } + + bool ok = true; + ok &= result->getImpl()->setFileName(toString(p)); + ok &= result->getImpl()->setTimeSeries(timeSeries); + OS_ASSERT(ok); + } else { + LOG_FREE(Warn, "openstudio.epmodel.ScheduleFile", "Timeseries does not have an interval length defined, but ScheduleVariableInterval is deprecated"); + } + + return result; + } + } // namespace epmodel } // namespace openstudio @@ -146,6 +244,50 @@ namespace openstudio { namespace epmodel { namespace detail { + bool ScheduleFile_Impl::setTimeSeries(const openstudio::TimeSeries& timeSeries) { + boost::optional intervalTime = timeSeries.intervalLength(); + if (!intervalTime) { + return false; + } + + auto intervalLengthAsInteger = [](const double value) -> int { + double integralPart = 0.0; + if (std::modf(value, &integralPart) == 0.0) { + // The intervalLength is actually an int, not a double + return static_cast(integralPart); + } + return -1; + }; + + // check the interval + const double intervalLengthDouble = intervalTime->totalMinutes(); + const int intervalLength = intervalLengthAsInteger(intervalLengthDouble); + if (intervalLength < 0) { + return false; + } + + bool ok = true; + ok &= this->setMinutesperItem(intervalLength); + // Do we actually need the following? They aren't required in the IDD. + //ok &= this->setNumberofHoursofData(8760); + //ok &= this->setColumnSeparator("Comma"); + //ok &= this->setInterpolatetoTimestep(true); + //ok &= this->setAdjustScheduleforDaylightSavings(true); + return true; + } + + std::string ScheduleFile_Impl::fileName() const { + const auto value = getString(openstudio::Schedule_FileFields::FileName, true); + OS_ASSERT(value); + return *value; + } + + bool ScheduleFile_Impl::setFileName(std::string fileName) { + const bool result = setString(openstudio::Schedule_FileFields::FileName, fileName); + OS_ASSERT(result); + return result; + } + int ScheduleFile_Impl::columnNumber() const { const auto value = getInt(openstudio::Schedule_FileFields::ColumnNumber, true); OS_ASSERT(value); @@ -255,6 +397,20 @@ namespace epmodel { OS_ASSERT(setString(openstudio::Schedule_FileFields::AdjustScheduleforDaylightSavings, "")); } + boost::optional ScheduleFile_Impl::csvFile() const { + boost::optional csvFile; + csvFile = CSVFile::load(this->fileName()); + return csvFile; + } + + openstudio::path ScheduleFile_Impl::translatedFilePath() const { + openstudio::path filePath = this->fileName(); + if (!exists(filePath)) { + LOG_FREE(Warn, "openstudio.epmodel.ScheduleFile", "Cannot find file \"" << filePath << "\""); + } + return filePath; + } + std::vector ScheduleFile_Impl::columnSeparatorValues() const { return openstudio::epmodel::ScheduleFile::columnSeparatorValues(); } diff --git a/src/epmodel/ScheduleInterval/ScheduleFile.hpp b/src/epmodel/ScheduleInterval/ScheduleFile.hpp index 53b5cef071..21ebf2c24e 100644 --- a/src/epmodel/ScheduleInterval/ScheduleFile.hpp +++ b/src/epmodel/ScheduleInterval/ScheduleFile.hpp @@ -8,6 +8,9 @@ #include "EPModelAPI.hpp" #include "Schedule/Schedule.hpp" +#include "../../utilities/core/Path.hpp" +#include "../../utilities/filetypes/CSVFile.hpp" +#include #include @@ -15,6 +18,7 @@ #include namespace openstudio { + namespace epmodel { class Model; @@ -26,7 +30,9 @@ namespace epmodel { class EPMODEL_API ScheduleFile : public Schedule { public: - explicit ScheduleFile(const Model& model); + // explicit ScheduleFile(const ExternalFile& externalfile, int column = 1, int rowsToSkip = 0); // FIXME: how do we maintain this? + explicit ScheduleFile(const Model& model, int column = 1, int rowsToSkip = 0); // new ctor + explicit ScheduleFile(const Model& model, const openstudio::path& filePath, int column = 1, int rowsToSkip = 0, bool translateFileWithRelativePath = false); // old ctor virtual ~ScheduleFile() override = default; ScheduleFile(const ScheduleFile& other) = default; @@ -47,6 +53,10 @@ namespace epmodel { // - Field Mapping: ScheduleTypeLimitsName and FileName are intentionally excluded in this scalar-only pass // (relationship/file-path behavior is handled separately from scalar accessors). // - TODO(parity): Add relationship and path-translation parity APIs incrementally after scalar scaffold saturation. + + // std::string fileName() const; + // bool setFileName(std::string fileName); + int columnNumber() const; bool setColumnNumber(int columnNumber); @@ -78,6 +88,16 @@ namespace epmodel { bool setAdjustScheduleforDaylightSavings(bool adjustScheduleforDaylightSavings); void resetAdjustScheduleforDaylightSavings(); + static boost::optional fromTimeSeries(const openstudio::TimeSeries& timeSeries, Model& model); + + // Extra setters/getters + boost::optional csvFile() const; + OS_DEPRECATED(4, 0, 0) bool translateFileWithRelativePath() const; + OS_DEPRECATED(4, 0, 0) bool isTranslateFileWithRelativePathDefaulted() const; + OS_DEPRECATED(4, 0, 0) bool setTranslateFileWithRelativePath(bool translateFileWithRelativePath); + OS_DEPRECATED(4, 0, 0) void resetTranslateFileWithRelativePath(); + openstudio::path translatedFilePath() const; + protected: using ImplType = detail::ScheduleFile_Impl; @@ -86,6 +106,9 @@ namespace epmodel { friend class openstudio::detail::IdfObject_Impl; explicit ScheduleFile(std::shared_ptr impl); + + private: + REGISTER_LOGGER("openstudio.epmodel.ScheduleFile"); }; } // namespace epmodel diff --git a/src/epmodel/ScheduleInterval/ScheduleFile_Impl.hpp b/src/epmodel/ScheduleInterval/ScheduleFile_Impl.hpp index 2d320c1625..f71905b7d9 100644 --- a/src/epmodel/ScheduleInterval/ScheduleFile_Impl.hpp +++ b/src/epmodel/ScheduleInterval/ScheduleFile_Impl.hpp @@ -7,6 +7,7 @@ #define EPMODEL_SCHEDULEFILE_IMPL_HPP #include "Schedule/Schedule_Impl.hpp" +#include "../utilities/filetypes/CSVFile.hpp" #include @@ -20,6 +21,9 @@ namespace epmodel { using Schedule_Impl::Schedule_Impl; virtual ~ScheduleFile_Impl() override = default; + std::string fileName() const; + bool setFileName(std::string fileName); + int columnNumber() const; bool setColumnNumber(int columnNumber); @@ -50,6 +54,18 @@ namespace epmodel { bool setAdjustScheduleforDaylightSavings(bool adjustScheduleforDaylightSavings); void resetAdjustScheduleforDaylightSavings(); + // openstudio::TimeSeries timeSeries() const; + bool setTimeSeries(const openstudio::TimeSeries& timeSeries); + + // Extra setters/getters + boost::optional csvFile() const; + // bool translateFileWithRelativePath() const; + // bool isTranslateFileWithRelativePathDefaulted() const; + // void ensureNoLeapDays(); + // bool setTranslateFileWithRelativePath(bool translateFileWithRelativePath); + // void resetTranslateFileWithRelativePath(); + openstudio::path translatedFilePath() const; + std::vector columnSeparatorValues() const; std::vector minutesperItemValues() const; }; diff --git a/src/epmodel/test/ScheduleFile_GTest.cpp b/src/epmodel/test/ScheduleFile_GTest.cpp index da192e9caa..e7340becd6 100644 --- a/src/epmodel/test/ScheduleFile_GTest.cpp +++ b/src/epmodel/test/ScheduleFile_GTest.cpp @@ -7,7 +7,12 @@ #include "EPModelFixture.hpp" #include "../ScheduleInterval/ScheduleFile.hpp" +#include "../ScheduleInterval/ScheduleFile_Impl.hpp" +#include "../../utilities/data/TimeSeries.hpp" +#include "../../utilities/core/StringStreamLogSink.hpp" + +using namespace openstudio; using namespace openstudio::epmodel; TEST_F(EPModelFixture, ScheduleFile_DefaultConstructor) { @@ -67,3 +72,206 @@ TEST_F(EPModelFixture, ScheduleFile_ScalarAccessors_RoundTrip) { schedule.resetAdjustScheduleforDaylightSavings(); EXPECT_TRUE(schedule.adjustScheduleforDaylightSavings()); } + +TEST_F(EPModelFixture, ScheduleFile_AltCtor) { + Model model; + // EXPECT_EQ(0u, model.getConcreteModelObjects().size()); + EXPECT_EQ(0u, model.getConcreteModelObjects().size()); + + path p = resourcesPath() / toPath("model/schedulefile.csv"); + EXPECT_TRUE(exists(p)); + +/* path expectedDestDir; + std::vector absoluteFilePaths = model.workflowJSON().absoluteFilePaths(); + if (absoluteFilePaths.empty()) { + expectedDestDir = model.workflowJSON().absoluteRootDir(); + } else { + expectedDestDir = absoluteFilePaths[0]; + } + + if (exists(expectedDestDir)) { + removeDirectory(expectedDestDir); + } + ASSERT_FALSE(exists(expectedDestDir)); */ + + ScheduleFile schedule(model, openstudio::toString(p)); + EXPECT_EQ(1u, model.getConcreteModelObjects().size()); +/* EXPECT_EQ(1u, model.getConcreteModelObjects().size()); + ExternalFile externalfile = schedule.externalFile(); + EXPECT_EQ(1u, externalfile.scheduleFiles().size()); + EXPECT_EQ(openstudio::toString(p), externalfile.fileName()); + EXPECT_TRUE(externalfile.isColumnSeparatorDefaulted()); + EXPECT_FALSE(equivalent(expectedDestDir / externalfile.fileName(), externalfile.filePath())); + EXPECT_TRUE(exists(externalfile.filePath())); + EXPECT_EQ(p, externalfile.filePath()); */ + EXPECT_TRUE(schedule.isNumberofHoursofDataDefaulted()); + EXPECT_EQ(1, schedule.columnNumber()); + schedule.setColumnNumber(1); + EXPECT_EQ(1, schedule.columnNumber()); + EXPECT_EQ(0, schedule.rowstoSkipatTop()); + schedule.setRowstoSkipatTop(1); + EXPECT_EQ(1, schedule.rowstoSkipatTop()); + + EXPECT_EQ("Comma", schedule.columnSeparator()); + EXPECT_TRUE(schedule.isColumnSeparatorDefaulted()); + EXPECT_TRUE(schedule.setColumnSeparator("Tab")); + EXPECT_EQ("Tab", schedule.columnSeparator()); + + ScheduleFile schedule2(model, openstudio::toString(p)); + EXPECT_EQ(2u, model.getConcreteModelObjects().size()); +/* EXPECT_EQ(1u, model.getConcreteModelObjects().size()); + ExternalFile externalfile2 = schedule2.externalFile(); + EXPECT_EQ(2u, externalfile.scheduleFiles().size()); + EXPECT_EQ(externalfile.handle(), externalfile2.handle()); */ + EXPECT_TRUE(schedule2.isNumberofHoursofDataDefaulted()); + EXPECT_EQ(1, schedule2.columnNumber()); + schedule2.setColumnNumber(2); + EXPECT_EQ(2, schedule2.columnNumber()); + EXPECT_EQ(0, schedule2.rowstoSkipatTop()); + schedule2.setRowstoSkipatTop(1); + EXPECT_EQ(1, schedule2.rowstoSkipatTop()); + + openstudio::path filePath = schedule2.getImpl()->fileName(); + ASSERT_TRUE(exists(filePath)); + EXPECT_EQ("schedulefile.csv", filePath.filename()); + + schedule2.remove(); + EXPECT_EQ(1u, model.getConcreteModelObjects().size()); +} + +TEST_F(EPModelFixture, ScheduleFile_ExtraSettersGetters) { + Model model; + + path p = toPath("resources/model/schedulefile.csv"); + EXPECT_TRUE(exists(p)); + + bool translateFileWithRelativePath = false; + ScheduleFile schedule(model, openstudio::toString(p), 1, 0, translateFileWithRelativePath); + + boost::optional csvFile = schedule.csvFile(); + ASSERT_TRUE(csvFile); + // EXPECT_EQ(3, csvFile->numColumns()); + // EXPECT_EQ(8760, csvFile->numRows()); + EXPECT_FALSE(schedule.translateFileWithRelativePath()); + EXPECT_FALSE(schedule.isTranslateFileWithRelativePathDefaulted()); + EXPECT_FALSE(schedule.setTranslateFileWithRelativePath(true)); + schedule.resetTranslateFileWithRelativePath(); + openstudio::path filePath = schedule.getImpl()->fileName(); + ASSERT_TRUE(exists(filePath)); + EXPECT_NE(p, filePath); + EXPECT_NE(p, schedule.translatedFilePath()); + + translateFileWithRelativePath = true; + ScheduleFile schedule2(model, openstudio::toString(p), 1, 0, translateFileWithRelativePath); + + boost::optional csvFile2 = schedule2.csvFile(); + ASSERT_TRUE(csvFile2); + // EXPECT_EQ(3, csvFile2->numColumns()); + // EXPECT_EQ(8760, csvFile2->numRows()); + EXPECT_FALSE(schedule2.translateFileWithRelativePath()); + EXPECT_FALSE(schedule2.isTranslateFileWithRelativePathDefaulted()); + EXPECT_FALSE(schedule2.setTranslateFileWithRelativePath(true)); + schedule2.resetTranslateFileWithRelativePath(); + openstudio::path filePath2 = schedule2.getImpl()->fileName(); + ASSERT_TRUE(exists(filePath2)); + EXPECT_EQ(p, filePath2); + EXPECT_EQ(p, schedule2.translatedFilePath()); +} + +TEST_F(EPModelFixture, ScheduleFile_CheckCannotFindFile) { + Model model; + + StringStreamLogSink sink; + sink.setLogLevel(Warn); + + path p = toPath("resources/model/schedulefile2.csv"); + EXPECT_FALSE(exists(p)); + EXPECT_THROW(ScheduleFile(model, openstudio::toString(p)), openstudio::Exception); + EXPECT_EQ(1, sink.logMessages().size()); + EXPECT_EQ("openstudio.epmodel.ScheduleFile", sink.logMessages().front().logChannel()); + EXPECT_EQ("Cannot find file \"resources/model/schedulefile2.csv\" for Object of type 'Schedule:File' and named 'Schedule File 1'", sink.logMessages().front().logMessage()); + + path p2 = toPath("resources/model/schedulefile.csv"); + EXPECT_TRUE(exists(p2)); + ScheduleFile schedule(model, openstudio::toString(p2)); + EXPECT_TRUE(schedule.getImpl()->setFileName(toString(p))); + + StringStreamLogSink sink2; + sink2.setLogLevel(Warn); + + EXPECT_EQ(p, schedule.translatedFilePath()); + EXPECT_EQ(1, sink2.logMessages().size()); + EXPECT_EQ("openstudio.epmodel.ScheduleFile", sink2.logMessages().front().logChannel()); + EXPECT_EQ("Cannot find file \"resources/model/schedulefile2.csv\"", sink2.logMessages().front().logMessage()); +} + +TEST_F(EPModelFixture, ScheduleFile_fromTimeSeries_intervalLengthYes) { + Model model; + + // hourly + Date startDate(MonthOfYear::Jan, 1); + Time intervalLength(0, 0, 60); + Vector values(8760); + for (unsigned i = 0; i < values.size(); ++i) { + values[i] = i % 24; + } + + TimeSeries timeSeries(startDate, intervalLength, values, ""); + + boost::optional schedule = ScheduleFile::fromTimeSeries(timeSeries, model); + ASSERT_TRUE(schedule); + openstudio::path filePath = schedule->getImpl()->fileName(); + ASSERT_TRUE(exists(filePath)); + EXPECT_EQ("Schedule File 1.csv", filePath.filename()); + EXPECT_EQ(2, schedule->columnNumber()); + EXPECT_EQ(0, schedule->rowstoSkipatTop()); + ASSERT_TRUE(schedule->numberofHoursofData()); + EXPECT_EQ(8760, schedule->numberofHoursofData().get()); + EXPECT_EQ("Comma", schedule->columnSeparator()); + EXPECT_FALSE(schedule->interpolatetoTimestep()); + ASSERT_TRUE(schedule->minutesperItem()); + EXPECT_EQ("60", schedule->minutesperItem().get()); + EXPECT_TRUE(schedule->adjustScheduleforDaylightSavings()); + + // 15-minutely + Time intervalLength2(0, 0, 15); + Vector values2(4 * 8760); + for (unsigned i = 0; i < values2.size(); ++i) { + values2[i] = i % 24; + } + + TimeSeries timeSeries2(startDate, intervalLength2, values2, ""); + + boost::optional schedule2 = ScheduleFile::fromTimeSeries(timeSeries2, model); + ASSERT_TRUE(schedule2); + openstudio::path filePath2 = schedule2->getImpl()->fileName(); + ASSERT_TRUE(exists(filePath2)); + EXPECT_EQ("Schedule File 2.csv", filePath2.filename()); + EXPECT_EQ(2, schedule2->columnNumber()); + EXPECT_EQ(0, schedule2->rowstoSkipatTop()); + ASSERT_TRUE(schedule2->numberofHoursofData()); + EXPECT_EQ(8760, schedule2->numberofHoursofData().get()); + EXPECT_EQ("Comma", schedule2->columnSeparator()); + EXPECT_FALSE(schedule2->interpolatetoTimestep()); + ASSERT_TRUE(schedule2->minutesperItem()); + EXPECT_EQ("15", schedule2->minutesperItem().get()); + EXPECT_TRUE(schedule2->adjustScheduleforDaylightSavings()); +} + +TEST_F(EPModelFixture, ScheduleFile_fromTimeSeries_intervalLengthNo) { + Model model; + + Date startDate(MonthOfYear::Jan, 1); + Time intervalLength(0, 0, 60); + std::vector dateTimes; + Vector values(8760); + for (unsigned i = 0; i < values.size(); ++i) { + dateTimes.push_back(DateTime(startDate, intervalLength * (i + 1))); + values[i] = i % 24; + } + + TimeSeries timeSeries(dateTimes, values, ""); + + boost::optional schedule = ScheduleFile::fromTimeSeries(timeSeries, model); + ASSERT_FALSE(schedule); +}