Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
13e0353
Updates to ScheduleFile to facilitate fromTimeSeries.
joseph-robertson May 7, 2026
840ec98
Add setTimeSeries method, and tests.
joseph-robertson May 7, 2026
a14232e
Formatting.
joseph-robertson May 7, 2026
2b9697c
Clean up ScheduleFile files a bit.
joseph-robertson May 11, 2026
534afff
Add test for alt ctor.
joseph-robertson May 11, 2026
65aeabe
Re-enable /Zc:inline: CMake until 4.1 was adding it.
jmarrec Jul 13, 2026
7e2cfb1
#5631 - Split openstudiolib.dll into 4 DLLs
jmarrec Jul 13, 2026
58da206
Moving openstudio_osversion into openstudiomodellib since it's mutual…
jmarrec Jul 13, 2026
27847d6
Fix MSVC LNK2005 duplicate symbols from epmodel double-linking
jmarrec Jul 13, 2026
37533df
Merge branch '5631-os-dev-4.0-split_DLL' into os-4.0-dev-schfile
joseph-robertson Jul 15, 2026
07bd196
Set minutes per item for convenience in fromTimeSeries.
joseph-robertson Jul 15, 2026
93d6c4c
Update and improve ScheduleFile::fromTimeSeries unit tests.
joseph-robertson Jul 15, 2026
ee4dc19
Add remaining deprecated methods, and update all unit tests.
joseph-robertson Jul 17, 2026
6a4f74d
Check log warning messages for bad file path.
joseph-robertson Jul 17, 2026
e5e80a0
Merge branch 'os-4.0-dev' into os-4.0-dev-schfile
joseph-robertson Aug 25, 2026
6533613
Updates for timeSeries getter and tests.
joseph-robertson Aug 27, 2026
876b5ab
Check that csv data contains values for an entire year.
joseph-robertson Aug 28, 2026
2bf6452
Improve timeSeries tests for ScheduleFile.
joseph-robertson Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
242 changes: 236 additions & 6 deletions src/epmodel/ScheduleInterval/ScheduleFile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,47 @@
#include <utilities/idd/IddFactory.hxx>
#include <utilities/idd/IddObject.hpp>
#include <utilities/idd/Schedule_File_FieldEnums.hxx>
#include <utilities/core/DeprecatedHelpers.hpp>
#include <utilities/core/Logger.hpp>

#include "../utilities/data/TimeSeries.hpp"
#include "../model/YearDescription.hpp"
#include "../model/YearDescription_Impl.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);
}
}
OS_ASSERT(exists(filePath));

bool ok = true;
ok &= setFileName(toString(p));
ok &= setColumnNumber(column);
ok &= setRowstoSkipatTop(rowsToSkip);
OS_ASSERT(ok);
}

Expand All @@ -39,6 +71,14 @@ namespace epmodel {
return getIddKeyNames(IddFactory::instance().getObject(iddObjectType()).get(), openstudio::Schedule_FileFields::MinutesperItem);
}

std::string ScheduleFile::fileName() const {
return getImpl<detail::ScheduleFile_Impl>()->fileName();
}

bool ScheduleFile::setFileName(std::string fileName) {
return getImpl<detail::ScheduleFile_Impl>()->setFileName(fileName);
}

int ScheduleFile::columnNumber() const {
return getImpl<detail::ScheduleFile_Impl>()->columnNumber();
}
Expand Down Expand Up @@ -99,7 +139,7 @@ namespace epmodel {
getImpl<detail::ScheduleFile_Impl>()->resetInterpolatetoTimestep();
}

boost::optional<std::string> ScheduleFile::minutesperItem() const {
int ScheduleFile::minutesperItem() const {
return getImpl<detail::ScheduleFile_Impl>()->minutesperItem();
}

Expand Down Expand Up @@ -139,6 +179,72 @@ namespace epmodel {
getImpl<detail::ScheduleFile_Impl>()->resetAdjustScheduleforDaylightSavings();
}

boost::optional<CSVFile> ScheduleFile::csvFile() const {
return getImpl<detail::ScheduleFile_Impl>()->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<epmodel::detail::ScheduleFile_Impl>()->translatedFilePath();
}

openstudio::TimeSeries ScheduleFile::timeSeries() const {
return getImpl<detail::ScheduleFile_Impl>()->timeSeries();
}

boost::optional<ScheduleFile> ScheduleFile::fromTimeSeries(const openstudio::TimeSeries& timeSeries, Model& model) {
boost::optional<ScheduleFile> result;

boost::optional<openstudio::Time> intervalTime = timeSeries.intervalLength();
if (intervalTime) {
result = ScheduleFile(model, 2); // FIXME: 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_FREE_AND_THROW("openstudio.epmodel.ScheduleFile", "Cannot find file \"" << toString(filePath) << "\" for " << result->briefDescription());
} else {
// make the path correct for this system
p = system_complete(filePath);
}
OS_ASSERT(exists(filePath));

bool ok = true;
ok &= result->setFileName(toString(p));
ok &= result->getImpl<detail::ScheduleFile_Impl>()->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

Expand All @@ -150,6 +256,115 @@ namespace epmodel {
return openstudio::Schedule_FileFields::ScheduleTypeLimitsName;
}

openstudio::TimeSeries ScheduleFile_Impl::timeSeries() const {
boost::optional<CSVFile> csvFile = this->csvFile();
if (!csvFile) {
LOG_FREE_AND_THROW("openstudio.epmodel.ScheduleFile", "Did not set File Name for " << briefDescription());
}
OS_ASSERT(csvFile);

int columnIndex = this->columnNumber() - 1;
std::vector<std::string> values = csvFile->getColumnAsStringVector(columnIndex);
const int rowsToSkip = this->rowstoSkipatTop();
Vector vectorValues(values.size() - rowsToSkip);
for (size_t i = rowsToSkip; i < values.size(); ++i) {
double value = std::stod(values[i]);
vectorValues[i - rowsToSkip] = value;
}

int minutesperItem = this->minutesperItem();
openstudio::Time intervalLength(0, 0, minutesperItem);

int year = 2009;
// FIXME
// if (boost::optional<YearDescription> yd = this->model().getOptionalUniqueModelObject<YearDescription>()) {
// year = yd->assumedYear();
// }
Date startDate(MonthOfYear::Jan, 1, year);

openstudio::TimeSeries result(startDate, intervalLength, vectorValues, "");

// error checking
DateTimeVector dateTimes = result.dateTimes();
DateTime lastDateTime = DateTime(Date(MonthOfYear::Dec, 31, year), openstudio::Time(0, 24));
if (dateTimes.back() != lastDateTime) {
LOG_FREE(Warn, "openstudio.epmodel.ScheduleFile", "With " << minutesperItem << " minutes per item, last date time is " << dateTimes.back() << " for " << this->fileName() << " referenced by " << briefDescription());
}

return result;
}

bool ScheduleFile_Impl::setTimeSeries(const openstudio::TimeSeries& timeSeries) {
boost::optional<openstudio::Time> 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<int>(integralPart);
}
return -1;
};

// check the interval
const double intervalLengthDouble = intervalTime->totalMinutes();
const int intervalLength = intervalLengthAsInteger(intervalLengthDouble);
if (intervalLength < 0) {
return false;
}

// check that first report is whole number of intervals from start date
const DateTime firstReportDateTime = timeSeries.firstReportDateTime();
const DateTime startDateTime = timeSeries.startDateTime();
const Date startDate = startDateTime.date();

const Time firstReportTime = firstReportDateTime.time();

const double numIntervalsToFirstReportDouble = std::max(1.0, firstReportTime.totalMinutes() / intervalLengthDouble);
const int numIntervalsToFirstReport = intervalLengthAsInteger(numIntervalsToFirstReportDouble);
if (numIntervalsToFirstReport < 0) {
return false;
}

// check the values
const openstudio::Vector values = timeSeries.values();
for (size_t pos = 0; const auto& value : values) {
// Check validity, cannot be NaN, Inf, etc
if (std::isinf(value)) {
LOG_FREE(Warn, "openstudio.epmodel.ScheduleFile", "There is Infinity on position " << pos << " in the timeSeries provided for " << briefDescription());
return false;
} else if (std::isnan(value)) {
LOG_FREE(Warn, "openstudio.epmodel.ScheduleFile", "There is a NaN on position " << pos << " in the timeSeries provided for " << briefDescription());
return false;
}
++pos;
}

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);
Expand Down Expand Up @@ -220,10 +435,10 @@ namespace epmodel {
OS_ASSERT(setString(openstudio::Schedule_FileFields::InterpolatetoTimestep, ""));
}

boost::optional<std::string> ScheduleFile_Impl::minutesperItem() const {
int ScheduleFile_Impl::minutesperItem() const {
const auto value = getInt(openstudio::Schedule_FileFields::MinutesperItem, true);
OS_ASSERT(value);
return std::to_string(*value);
return *value;
}

bool ScheduleFile_Impl::isMinutesperItemDefaulted() const {
Expand Down Expand Up @@ -259,6 +474,21 @@ namespace epmodel {
OS_ASSERT(setString(openstudio::Schedule_FileFields::AdjustScheduleforDaylightSavings, ""));
}

boost::optional<CSVFile> ScheduleFile_Impl::csvFile() const {
boost::optional<CSVFile> csvFile;
openstudio::path filePath = this->fileName();
csvFile = CSVFile::load(filePath);
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<std::string> ScheduleFile_Impl::columnSeparatorValues() const {
return openstudio::epmodel::ScheduleFile::columnSeparatorValues();
}
Expand Down
27 changes: 25 additions & 2 deletions src/epmodel/ScheduleInterval/ScheduleFile.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@

#include "EPModelAPI.hpp"
#include "Schedule/Schedule.hpp"
#include "../../utilities/core/Path.hpp"
#include "../../utilities/filetypes/CSVFile.hpp"
#include <utilities/core/Deprecated.hpp>

#include <utilities/idd/IddEnums.hxx>

#include <memory>
#include <vector>

namespace openstudio {

namespace epmodel {

class Model;
Expand Down Expand Up @@ -53,7 +57,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;
Expand All @@ -66,6 +72,9 @@ namespace epmodel {
static std::vector<std::string> columnSeparatorValues();
static std::vector<std::string> minutesperItemValues();

std::string fileName() const;
bool setFileName(std::string fileName);

int columnNumber() const;
bool setColumnNumber(int columnNumber);

Expand All @@ -86,7 +95,7 @@ namespace epmodel {
bool setInterpolatetoTimestep(bool interpolatetoTimestep);
void resetInterpolatetoTimestep();

boost::optional<std::string> minutesperItem() const;
int minutesperItem() const;
bool isMinutesperItemDefaulted() const;
bool setMinutesperItem(const std::string& minutesperItem);
bool setMinutesperItem(int minutesperItem);
Expand All @@ -97,6 +106,17 @@ namespace epmodel {
bool setAdjustScheduleforDaylightSavings(bool adjustScheduleforDaylightSavings);
void resetAdjustScheduleforDaylightSavings();

openstudio::TimeSeries timeSeries() const;
static boost::optional<ScheduleFile> fromTimeSeries(const openstudio::TimeSeries& timeSeries, Model& model);

// Extra setters/getters
boost::optional<CSVFile> 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;

Expand All @@ -105,6 +125,9 @@ namespace epmodel {
friend class openstudio::detail::IdfObject_Impl;

explicit ScheduleFile(std::shared_ptr<detail::ScheduleFile_Impl> impl);

private:
REGISTER_LOGGER("openstudio.epmodel.ScheduleFile");
};

} // namespace epmodel
Expand Down
Loading
Loading