Fix #5510 - Order of Output:Meter:XXX objects (w/ different reporting frequences) in IDF are not deterministic#5532
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.
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.
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.
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.
In FT, we instantiate the Building object earlier and translate it here:
And in translateAndMapModelObject, we translate the children of the object right after it
So the Meters such as "Electricity:Total" are translated right there, and not later when we iterate on iddObjectsToTranslate here
| const boost::optional<FuelType>& fuelType, const boost::optional<InstallLocationType>& installLocationType, | ||
| const boost::optional<std::string>& specificInstallLocation); | ||
|
|
||
| static std::vector<std::string> reportingFrequencyValues(); |
| 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.
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.
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:
|
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.