-
Notifications
You must be signed in to change notification settings - Fork 222
Fix #5510 - Order of Output:Meter:XXX objects (w/ different reporting frequences) in IDF are not deterministic #5532
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
🧪 Test Results DashboardSummary
|
| Run | XML File | Status |
|---|---|---|
| run1 | results.xml |
✅ Found |
| run3 | results.xml |
✅ Found |
| run2 | results.xml |
✅ Found |
| // #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; | ||
| } |
There was a problem hiding this comment.
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
| if (iddObjectType == IddObjectType::OS_Output_Meter) { | ||
| std::sort(objects.begin(), objects.end(), OutputMeterSorterPredicate); | ||
| } else { | ||
| std::sort(objects.begin(), objects.end(), WorkspaceObjectNameLess()); | ||
| } |
There was a problem hiding this comment.
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
| if (a.iddObject().type() == IddObjectType::OS_Output_Meter) { | ||
| return OutputMeterSorterPredicate(a, b); | ||
| } | ||
| return istringLess(aname, bname); | ||
|
|
||
| return WorkspaceObjectNameLess()(a, b); |
There was a problem hiding this comment.
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.
OpenStudio/src/model/Building.cpp
Lines 574 to 583 in c7f13ad
| 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:
OpenStudio/src/energyplus/ForwardTranslator.cpp
Lines 473 to 478 in c7f13ad
| // 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
OpenStudio/src/energyplus/ForwardTranslator.cpp
Lines 3341 to 3356 in c7f13ad
| // 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
OpenStudio/src/energyplus/ForwardTranslator.cpp
Lines 597 to 608 in c7f13ad
| // 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(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WAs missing
| 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()); | ||
| } |
There was a problem hiding this comment.
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
| // 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."; | ||
| } |
There was a problem hiding this comment.
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
07cc43b to
44b14e6
Compare
|
CI Results for 44b14e6:
|
There was a problem hiding this 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!
Pull request overview
I've tested on OpenStudio-HPXML
python:
Pull Request Author
src/model/test)src/energyplus/Test)src/osversion/VersionTranslator.cpp)Labels:
IDDChangeAPIChangePull Request - Ready for CIso that CI builds your PRReview Checklist
This will not be exhaustively relevant to every PR.