Skip to content

Conversation

@jmarrec
Copy link
Collaborator

@jmarrec jmarrec commented Nov 20, 2025

Pull request overview

I've tested on OpenStudio-HPXML

for i in {1..10}; do $os_build_rel/Products/openstudio workflow/run_simulation.rb -x workflow/sample_files/base.xml -o test$i --debug; done

python:

from pathlib import Path

HPXML_ROOT = Path('/home/julien/Software/Others/openstudio_gems/OpenStudio-HPXML')
idf_paths = sorted(list(HPXML_ROOT.glob('test*/run/in.idf')))
assert len(idf_paths) == 10
expected = idf_paths[0].read_text()
for idf_path in idf_paths[1:]:
    assert expected == idf_path.read_text()

Pull Request Author

  • Model API Changes / Additions
  • Any new or modified fields have been implemented in the EnergyPlus ForwardTranslator (and ReverseTranslator as appropriate)
  • Model API methods are tested (in src/model/test)
  • EnergyPlus ForwardTranslator Tests (in src/energyplus/Test)
  • If a new object or method, added a test in NREL/OpenStudio-resources: Add Link
  • If needed, added VersionTranslation rules for the objects (src/osversion/VersionTranslator.cpp)
  • Verified that C# bindings built fine on Windows, partial classes used as needed, etc.
  • All new and existing tests passes
  • If methods have been deprecated, update rest of code to use the new methods

Labels:

  • If change to an IDD file, add the label IDDChange
  • If breaking existing API, add the label APIChange
  • If deemed ready, add label Pull Request - Ready for CI so that CI builds your PR

Review Checklist

This will not be exhaustively relevant to every PR.

  • Perform a Code Review on GitHub
  • Code Style, strip trailing whitespace, etc.
  • All related changes have been implemented: model changes, model tests, FT changes, FT tests, VersionTranslation, OS App
  • Labeling is ok
  • If defect, verify by running develop branch and reproducing defect, then running PR and reproducing fix
  • If feature, test running new feature, try creative ways to break it
  • CI status: all green or justified

@jmarrec jmarrec self-assigned this Nov 20, 2025
@jmarrec jmarrec added severity - Normal Bug component - IDF Translation Pull Request - Ready for CI This pull request if finalized and is ready for continuous integration verification prior to merge. labels Nov 20, 2025
@github-actions
Copy link

github-actions bot commented Nov 20, 2025

🧪 Test Results Dashboard

Summary

Metric Value
Total Tests 4155
Passed 4145
Failed 10
Errors 0
Skipped 0
Success Rate 99.8%
Generated 2025-11-20 15:40:30 UTC

⚠️ Minor Issues Detected

🔍 Failed Tests (10 failures)

Linux-c++ (10 failures)

BCLFixture.BCLMeasure_CTor_throw_invalid_xml.BCLFixture.BCLMeasure_CTor_throw_invalid_xml (run1)

Error Message:

Failed

Full Details:

No details available
CLITest-test_bundle-bundle.CLITest-test_bundle-bundle (run1)

Error Message:

Failed

Full Details:

No details available
CLITest-test_bundle-bundle_git.CLITest-test_bundle-bundle_git (run1)

Error Message:

Failed

Full Details:

No details available
CLITest-test_bundle-bundle_native_embedded.CLITest-test_bundle-bundle_native_embedded (run1)

Error Message:

Failed

Full Details:

No details available
CLITest-test_bundle-bundle.CLITest-test_bundle-bundle (run3)

Error Message:

Failed

Full Details:

No details available
CLITest-test_bundle-bundle_git.CLITest-test_bundle-bundle_git (run3)

Error Message:

Failed

Full Details:

No details available
CLITest-test_bundle-bundle_native_embedded.CLITest-test_bundle-bundle_native_embedded (run3)

Error Message:

Failed

Full Details:

No details available
CLITest-test_bundle-bundle.CLITest-test_bundle-bundle (run2)

Error Message:

Failed

Full Details:

No details available
CLITest-test_bundle-bundle_git.CLITest-test_bundle-bundle_git (run2)

Error Message:

Failed

Full Details:

No details available
CLITest-test_bundle-bundle_native_embedded.CLITest-test_bundle-bundle_native_embedded (run2)

Error Message:

Failed

Full Details:

No details available

📊 Test Run Information

Run XML File Status
run1 results.xml ✅ Found
run3 results.xml ✅ Found
run2 results.xml ✅ Found

Comment on lines +179 to +218
// #5510 - Sort by name first, then by reporting frequency
// This is a weird case where the "Name" field is actually not unique
bool OutputMeterSorterPredicate(const WorkspaceObject& a, const WorkspaceObject& b) {
// 1. Sort by name
const auto nameA = a.nameString();
const auto nameB = b.nameString();
if (nameA != nameB) {
return istringLess(nameA, nameB);
}

// Safe to cast. Now we'll sort by the remaining fields in case of name equality
// I don't use std::tuple{meterA.reportingFrequency(), meterA.meterFileOnly(), meterA.cumulative()} < tuple{...}
// Because I don't want to greedily evaluate all fields if not needed
auto meterA = a.cast<OutputMeter>();
auto meterB = b.cast<OutputMeter>();

// 2. reportingFrequency
const auto freqA = meterA.reportingFrequency();
const auto freqB = meterB.reportingFrequency();
if (freqA != freqB) {
return freqA < freqB;
}

// 3. meterFileOnly
const bool fileA = meterA.meterFileOnly();
const bool fileB = meterB.meterFileOnly();
if (fileA != fileB) {
return !fileA; // A comes first if it's not File Only
}

// 4. cumulative
const bool cumA = meterA.cumulative();
const bool cumB = meterB.cumulative();
if (cumA != cumB) {
return !cumA; // A comes first if it's not cumulative
}

// Equal in all fields
return false;
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Predicate for deterministic ordering of OutputMeters

Comment on lines +645 to +649
if (iddObjectType == IddObjectType::OS_Output_Meter) {
std::sort(objects.begin(), objects.end(), OutputMeterSorterPredicate);
} else {
std::sort(objects.begin(), objects.end(), WorkspaceObjectNameLess());
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most meters (except children of Building) are translated here when we loop on iddObjectsToTranslate, so we sort differently then

Comment on lines +690 to +694
if (a.iddObject().type() == IddObjectType::OS_Output_Meter) {
return OutputMeterSorterPredicate(a, b);
}
return istringLess(aname, bname);

return WorkspaceObjectNameLess()(a, b);
Copy link
Collaborator Author

@jmarrec jmarrec Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On 16db367 I explained this.

Anyways, let's put it here too. This is the ChildSorter operator() comparator. Here's the explanation why it's needed:


I did it in two commits to be clearer...

I realized that the OutputMeter with an InstallLocationType::Building are children of the Building object.

OutputMeterVector Building_Impl::meters() const {
auto filterOutMeter = [](const auto& meter) {
auto instalLocType_ = meter.installLocationType();
return instalLocType_ && (InstallLocationType::Building != instalLocType_.get().value());
};
OutputMeterVector result = this->model().getConcreteModelObjects<OutputMeter>();
result.erase(std::remove_if(result.begin(), result.end(), filterOutMeter), result.end());
return result;
}

In FT, we instantiate the Building object earlier and translate it here:

// ensure that building exists
boost::optional<model::Building> building = model.building();
if (!building) {
building = model.getUniqueModelObject<model::Building>();
}
translateAndMapModelObject(*building);

And in translateAndMapModelObject, we translate the children of the object right after it

// is this redundant?
// ETH@20120112 Yes.
OptionalParentObject opo = modelObject.optionalCast<ParentObject>();
if (opo) {
ModelObjectVector children = opo->children();
IddObjectTypeVector types = iddObjectsToTranslate();
// sort these objects as well
std::sort(children.begin(), children.end(), ChildSorter(types));
for (auto& elem : children) {
if (std::find(types.begin(), types.end(), elem.iddObject().type()) != types.end()) {
translateAndMapModelObject(elem);
}
}
}

So the Meters such as "Electricity:Total" are translated right there, and not later when we iterate on iddObjectsToTranslate here

// now loop over all objects
for (const IddObjectType& iddObjectType : iddObjectsToTranslate()) {
// get objects by type in sorted order
std::vector<WorkspaceObject> objects = model.getObjectsByType(iddObjectType);
std::sort(objects.begin(), objects.end(), WorkspaceObjectNameLess());
for (const WorkspaceObject& workspaceObject : objects) {
auto modelObject = workspaceObject.cast<ModelObject>();
translateAndMapModelObject(modelObject);
}
}

const boost::optional<FuelType>& fuelType, const boost::optional<InstallLocationType>& installLocationType,
const boost::optional<std::string>& specificInstallLocation);

static std::vector<std::string> reportingFrequencyValues();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WAs missing

Comment on lines +36 to +78
TEST_F(EnergyPlusFixture, ForwardTranslator_OutputMeter_Regular) {

ForwardTranslator ft;

Model m;
OutputMeter meter(m);
EXPECT_TRUE(meter.setInstallLocationType(InstallLocationType::Facility));
EXPECT_TRUE(meter.setFuelType(FuelType::Gas));

EXPECT_TRUE(meter.setReportingFrequency("Timestep"));
EXPECT_TRUE(meter.setMeterFileOnly(false));
EXPECT_TRUE(meter.setCumulative(false));

const Workspace w = ft.translateModel(m);
const auto idfObjs = w.getObjectsByType(IddObjectType::Output_Meter);
ASSERT_EQ(1u, idfObjs.size());

const auto& idfObject = idfObjs.front();
EXPECT_EQ("NaturalGas:Facility", idfObject.getString(Output_MeterFields::KeyName).get());
EXPECT_EQ("Timestep", idfObject.getString(Output_MeterFields::ReportingFrequency).get());
}

TEST_F(EnergyPlusFixture, ForwardTranslator_OutputMeter_MeterFileOnly) {

ForwardTranslator ft;

Model m;
OutputMeter meter(m);
EXPECT_TRUE(meter.setInstallLocationType(InstallLocationType::Facility));
EXPECT_TRUE(meter.setFuelType(FuelType::Gas));

EXPECT_TRUE(meter.setReportingFrequency("Timestep"));
EXPECT_TRUE(meter.setMeterFileOnly(true));
EXPECT_TRUE(meter.setCumulative(false));

const Workspace w = ft.translateModel(m);
const auto idfObjs = w.getObjectsByType(IddObjectType::Output_Meter_MeterFileOnly);
ASSERT_EQ(1u, idfObjs.size());

const auto& idfObject = idfObjs.front();
EXPECT_EQ("NaturalGas:Facility", idfObject.getString(Output_Meter_MeterFileOnlyFields::KeyName).get());
EXPECT_EQ("Timestep", idfObject.getString(Output_Meter_MeterFileOnlyFields::ReportingFrequency).get());
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a bunch of FT/RT tests since we had zero

Comment on lines 212 to 385
// This will grab the objects in the same order as the serialization to IDF
std::vector<WorkspaceObject> getMetersInSerializedOrder(const Workspace& w) {
WorkspaceObjectVector result;
const std::array<IddObjectType, 4> iddTypes = {
IddObjectType::Output_Meter,
IddObjectType::Output_Meter_MeterFileOnly,
IddObjectType::Output_Meter_Cumulative,
IddObjectType::Output_Meter_Cumulative_MeterFileOnly,
};
// objects(sorted=true): same as serialization order
for (const WorkspaceObject& object : w.objects(true)) {
if (std::find(iddTypes.begin(), iddTypes.end(), object.iddObject().type()) != iddTypes.end()) {
result.push_back(object);
}
}
return result;
}

struct MeterInfo
{
std::string name;
std::string reportingFrequency;
bool meterFileOnly;
bool cumulative;

MeterInfo(const WorkspaceObject& wo) {
switch (wo.iddObject().type().value()) {
case IddObjectType::Output_Meter: {
name = wo.getString(Output_MeterFields::KeyName).get();
reportingFrequency = wo.getString(Output_MeterFields::ReportingFrequency).get();
meterFileOnly = false;
cumulative = false;
break;
}
case IddObjectType::Output_Meter_MeterFileOnly: {
name = wo.getString(Output_Meter_MeterFileOnlyFields::KeyName).get();
reportingFrequency = wo.getString(Output_Meter_MeterFileOnlyFields::ReportingFrequency).get();
meterFileOnly = true;
cumulative = false;
break;
}
case IddObjectType::Output_Meter_Cumulative: {
name = wo.getString(Output_Meter_CumulativeFields::KeyName).get();
reportingFrequency = wo.getString(Output_Meter_CumulativeFields::ReportingFrequency).get();
meterFileOnly = false;
cumulative = true;
break;
}
case IddObjectType::Output_Meter_Cumulative_MeterFileOnly: {
name = wo.getString(Output_Meter_Cumulative_MeterFileOnlyFields::KeyName).get();
reportingFrequency = wo.getString(Output_Meter_Cumulative_MeterFileOnlyFields::ReportingFrequency).get();
meterFileOnly = true;
cumulative = true;
break;
}
default:
throw std::runtime_error("Unexpected IddObjectType in MeterInfo constructor");
}
}

std::string meterType() const {
if (cumulative && meterFileOnly) {
return "Output:Meter:Cumulative:MeterFileOnly";
} else if (cumulative) {
return "Output:Meter:Cumulative";
} else if (meterFileOnly) {
return "Output:Meter:MeterFileOnly";
} else {
return "Output:Meter";
}
}

auto operator<=>(const MeterInfo&) const = default;
};

std::ostream& operator<<(std::ostream& os, const MeterInfo& mi) {
os << mi.meterType() << "{name = '" << mi.name << "', reportingFrequency ='" << mi.reportingFrequency << "'}";
return os;
}

TEST_F(EnergyPlusFixture, ForwardTranslator_OutputMeter_ReproducibleOrder) {

ForwardTranslator ft;

// Electricity:Total ends up being a child of Building, so it's translated first
const std::vector<std::string> meter_names{"Electricity:Total", "NaturalGas:Facility", "Electricity:Facility"};
const std::vector<std::string> reporting_frequencies{"RunPeriod", "Monthly"};
const std::vector<bool> fileonlys{false};
const std::vector<bool> cumulatives{false};

auto prepareModel = [&meter_names, &reporting_frequencies, &fileonlys, &cumulatives]() -> openstudio::model::Model {
Model m;
for (const auto& name : meter_names) {
for (const auto& freq : reporting_frequencies) {
for (const auto& meterFileOnly : fileonlys) {
for (const auto& cumulative : cumulatives) {
OutputMeter meter(m);
EXPECT_TRUE(meter.setName(name));
EXPECT_TRUE(meter.setReportingFrequency(freq));
EXPECT_TRUE(meter.setMeterFileOnly(meterFileOnly));
EXPECT_TRUE(meter.setCumulative(cumulative));
}
}
}
}
return m;
};

auto produceMeterInfo = [&]() -> std::vector<MeterInfo> {
auto m = prepareModel();

const Workspace w = ft.translateModel(prepareModel());
const auto idfObjs = getMetersInSerializedOrder(w);

std::vector<MeterInfo> meterInfos;
meterInfos.reserve(idfObjs.size());
std::transform(idfObjs.begin(), idfObjs.end(), std::back_inserter(meterInfos), [](const WorkspaceObject& wo) { return MeterInfo(wo); });
return meterInfos;
};

auto meterInfos = produceMeterInfo();
ASSERT_EQ(meter_names.size() * reporting_frequencies.size() * fileonlys.size() * cumulatives.size(), meterInfos.size());

size_t n_failures = 0;
for (size_t n = 1; n < 10; ++n) {
auto newMeterInfos = produceMeterInfo();
// EXPECT_EQ(meterInfos, newMeterInfos) << "MeterInfos differ on iteration " << n;
EXPECT_EQ(meterInfos.size(), newMeterInfos.size());
for (size_t i = 0; i < meterInfos.size(); ++i) {
EXPECT_EQ(meterInfos[i], newMeterInfos[i]) << "MeterInfos differ at index " << i << " on iteration " << n;
}
if (meterInfos != newMeterInfos) {
++n_failures;
}
}
EXPECT_EQ(0, n_failures) << "Out of 9 re-translations, " << n_failures << " produced different OutputMeter orderings.";
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specific testing for the #5510. It fails without fix.

Run 1

```
[ RUN      ] EnergyPlusFixture.ForwardTranslator_OutputMeter_ReproducibleOrder
/home/julien/Software/Others/OpenStudio/src/energyplus/Test/OutputMeter_GTest.cpp:341: Failure
Expected equality of these values:
  meterInfos[i]
    Which is: Output:Meter{name = 'Electricity:Total', reportingFrequency ='RunPeriod'}
  newMeterInfos[i]
    Which is: Output:Meter{name = 'Electricity:Total', reportingFrequency ='Monthly'}
MeterInfos differ at index 0 on iteration 2

/home/julien/Software/Others/OpenStudio/src/energyplus/Test/OutputMeter_GTest.cpp:341: Failure
Expected equality of these values:
  meterInfos[i]
    Which is: Output:Meter{name = 'Electricity:Total', reportingFrequency ='Monthly'}
  newMeterInfos[i]
    Which is: Output:Meter{name = 'Electricity:Total', reportingFrequency ='RunPeriod'}
MeterInfos differ at index 1 on iteration 2

/home/julien/Software/Others/OpenStudio/src/energyplus/Test/OutputMeter_GTest.cpp:341: Failure
Expected equality of these values:
  meterInfos[i]
    Which is: Output:Meter{name = 'Electricity:Total', reportingFrequency ='RunPeriod'}
  newMeterInfos[i]
    Which is: Output:Meter{name = 'Electricity:Total', reportingFrequency ='Monthly'}
MeterInfos differ at index 0 on iteration 6

/home/julien/Software/Others/OpenStudio/src/energyplus/Test/OutputMeter_GTest.cpp:341: Failure
Expected equality of these values:
  meterInfos[i]
    Which is: Output:Meter{name = 'Electricity:Total', reportingFrequency ='Monthly'}
  newMeterInfos[i]
    Which is: Output:Meter{name = 'Electricity:Total', reportingFrequency ='RunPeriod'}
MeterInfos differ at index 1 on iteration 6

/home/julien/Software/Others/OpenStudio/src/energyplus/Test/OutputMeter_GTest.cpp:341: Failure
Expected equality of these values:
  meterInfos[i]
    Which is: Output:Meter{name = 'NaturalGas:Facility', reportingFrequency ='RunPeriod'}
  newMeterInfos[i]
    Which is: Output:Meter{name = 'NaturalGas:Facility', reportingFrequency ='Monthly'}
MeterInfos differ at index 4 on iteration 7

/home/julien/Software/Others/OpenStudio/src/energyplus/Test/OutputMeter_GTest.cpp:341: Failure
Expected equality of these values:
  meterInfos[i]
    Which is: Output:Meter{name = 'NaturalGas:Facility', reportingFrequency ='Monthly'}
  newMeterInfos[i]
    Which is: Output:Meter{name = 'NaturalGas:Facility', reportingFrequency ='RunPeriod'}
MeterInfos differ at index 5 on iteration 7

/home/julien/Software/Others/OpenStudio/src/energyplus/Test/OutputMeter_GTest.cpp:347: Failure
Expected equality of these values:
  0
  n_failures
    Which is: 3
Out of 9 re-translations, 3 produced different OutputMeter orderings
```

Run 2: "Out of 9 re-translations, 8 produced different OutputMeter orderings"

Run 3: Out of 9 re-translations, 4 produced different OutputMeter orderings.
I did it in two commits to be clearer...

I realized that the OutputMeter with an InstallLocationType::Building are children of the Building object.

https://github.com/NREL/OpenStudio/blob/c7f13ad61579ceacf4fbe742b9a6e0c71a14cb4d/src/model/Building.cpp#L574-L583

In FT, we instantiate the Building object earlier and translate it here:

https://github.com/NREL/OpenStudio/blob/c7f13ad61579ceacf4fbe742b9a6e0c71a14cb4d/src/energyplus/ForwardTranslator.cpp#L473-L478

And in translateAndMapModelObject, we translate the children of the object right after it

https://github.com/NREL/OpenStudio/blob/c7f13ad61579ceacf4fbe742b9a6e0c71a14cb4d/src/energyplus/ForwardTranslator.cpp#L3341-L3356

So the Meters such as "Electricity:Total" are translated right there, and not later when we iterate on iddObjectsToTranslate here

https://github.com/NREL/OpenStudio/blob/c7f13ad61579ceacf4fbe742b9a6e0c71a14cb4d/src/energyplus/ForwardTranslator.cpp#L597-L608
@jmarrec jmarrec force-pushed the 5510_OutputMeter_Order branch from 07cc43b to 44b14e6 Compare November 20, 2025 12:29
@ci-commercialbuildings
Copy link
Collaborator

ci-commercialbuildings commented Nov 20, 2025

CI Results for 44b14e6:

Copy link
Contributor

@shorowit shorowit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used the Windows build and confirmed it fixes the issue. Thanks @jmarrec!

@jmarrec jmarrec merged commit 9778fd6 into develop Nov 20, 2025
3 of 5 checks passed
@jmarrec jmarrec deleted the 5510_OutputMeter_Order branch November 20, 2025 15:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component - IDF Translation Pull Request - Ready for CI This pull request if finalized and is ready for continuous integration verification prior to merge. severity - Normal Bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Order of Output:Meter:MeterFileOnly objects (w/ different reporting frequences) in IDF are not deterministic

4 participants