V26.2.0-IOFreeze: new ZoneMRTCalculation object - #5649
Conversation
🧪 Test Results DashboardSummary
❌ Significant Test Failures📊 Test Run Information
|
There was a problem hiding this comment.
🟡 Changes recommended
Weight-sum validation, stale references, non-finite values, and partial-add failure reporting can currently produce invalid behavior.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds Model API and EnergyPlus translation support for ZoneMRTCalculation.
Changes:
- Adds the model object, MRT weighting-factor API, and ThermalZone integration.
- Adds forward/reverse translators and binding support.
- Updates IDDs, build registration, and tests.
File summaries
| File | Description |
|---|---|
src/model/ZoneMRTCalculation.hpp |
Declares the new API. |
src/model/ZoneMRTCalculation.cpp |
Implements weighting-factor behavior. |
src/model/ZoneMRTCalculation_Impl.hpp |
Declares implementation details. |
src/model/ThermalZone.hpp |
Exposes MRT calculation access. |
src/model/ThermalZone.cpp |
Implements creation and lookup. |
src/model/ThermalZone_Impl.hpp |
Adds implementation declaration. |
src/model/test/ZoneMRTCalculation_GTest.cpp |
Tests the model API. |
src/model/ModelHVAC.i |
Adds C# ThermalZone access. |
src/model/ModelGeometry.i |
Registers binding templates. |
src/model/Model.cpp |
Registers constructors. |
src/model/ConcreteModelObjects.hpp |
Includes the new object. |
src/model/CMakeLists.txt |
Adds model sources and tests. |
src/energyplus/Test/ZoneMRTCalculation_GTest.cpp |
Tests both translators. |
src/energyplus/ReverseTranslator/ReverseTranslateZoneMRTCalculation.cpp |
Implements reverse translation. |
src/energyplus/ReverseTranslator.hpp |
Declares reverse translation. |
src/energyplus/ReverseTranslator.cpp |
Dispatches reverse translation. |
src/energyplus/ForwardTranslator/ForwardTranslateZoneMRTCalculation.cpp |
Implements forward translation. |
src/energyplus/ForwardTranslator.hpp |
Declares forward translation. |
src/energyplus/ForwardTranslator.cpp |
Registers forward translation. |
src/energyplus/CMakeLists.txt |
Adds translator sources and tests. |
resources/model/OpenStudio.idd |
Defines the OpenStudio object. |
resources/energyplus/ProposedEnergy+.idd |
Converts EnergyPlus fields to extensible groups. |
Review details
Suppressed comments (1)
src/model/ZoneMRTCalculation.cpp:107
- Removing a referenced
Peopleobject clears this pointer but leaves the extensible group. A subsequent call toaddMRTWeightingFactorreaches this predicate and calls.get()on an empty optional, throwing instead of adding the new factor. Treat a missing field as a non-match (and add a removal regression test).
auto it = std::find_if(egs.begin(), egs.end(), [&](const WorkspaceExtensibleGroup& eg) {
return eg.getField(OS_ZoneMRTCalculationExtensibleFields::PeopleName).get() == peopleHandle;
});
- Files reviewed: 22/22 changed files
- Comments generated: 4
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| for (const MRTWeightingFactor& mrtWeightingFactor : mrtWeightingFactors) { | ||
| People people = mrtWeightingFactor.people(); | ||
| boost::optional<IdfObject> _people = translateAndMapModelObject(people); | ||
|
|
||
| if (_people) { |
| return result; | ||
| } | ||
|
|
||
| boost::optional<unsigned> existingIndex = mrtWeightingFactorIndex(mrtWeightingFactor); |
…tWeightingFactorIndex to use just People, update model tests.
| // The hooks below would remove ZoneMRTCalculation extensible groups as soon as a referenced People object | ||
| // becomes invalid by being moved to another Space, reset from its Space, assigned to a SpaceType, or losing | ||
| // its thermal comfort model types. That is more proactive than most existing extensible-reference patterns in | ||
| // the model, which generally clean references only from the referenced object's remove() path and otherwise | ||
| // rely on explicit remove APIs or read/translation-time filtering. |
There was a problem hiding this comment.
Not sure if we'd want this -- it may be overkill.
There was a problem hiding this comment.
🟡 Changes recommended
Thermal-zone deletion can leave an invalid ZoneMRTCalculation object that later causes assertions during access or translation.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
resources/model/OpenStudio.idd:6830
- This field declares two conflicting
\typevalues; all other OpenStudio object-list fields use only\type object-list(for example, lines 6808-6809 and 6837-6838). Remove the redundant alpha declaration so the IDD metadata has one unambiguous field type.
src/model/ZoneMRTCalculation.cpp:85 - These adjacent string literals concatenate as
andthat, making the thrown clone error malformed. Preserve a space at the literal boundary.
- Files reviewed: 25/25 changed files
- Comments generated: 1
- Review effort level: Balanced
| ZoneMRTCalculation ThermalZone_Impl::getZoneMRTCalculation() const { | ||
| auto thisThermalZone = getObject<ThermalZone>(); | ||
| std::vector<ZoneMRTCalculation> zoneMRTCalculations = | ||
| thisThermalZone.getModelObjectSources<ZoneMRTCalculation>(ZoneMRTCalculation::iddObjectType()); |
There was a problem hiding this comment.
I think we'd have the same issue when deleting a Space.
Removing a Space bulk-removes its child People through ParentObject_Impl::remove(), but it does not call People_Impl::remove(). So cleanup would require Space_Impl::remove() to inspect all child People, then find and prune any ZoneMRTCalculation groups for each one. That reaches across more ownership boundaries and is less consistent with the existing bulk child-removal mechanism.
So: zone removal is direct source-object cleanup; space removal is indirect child/reference cleanup.
jmarrec
left a comment
There was a problem hiding this comment.
Looking pretty good. A couple of things to change in the code review
And please fill out the PR checklist, most important item of it: we need a OpenStudio-resources matching test for this new object
| \note A People object assigned directly to a Space retains its input name. | ||
| \note A People object expanded across multiple Spaces must use an expanded instance name | ||
| \note formed as "<Space Name> <People Name>"; its original input name is not accepted. | ||
| \note People instances in automatically generated remainder Spaces are not supported. |
There was a problem hiding this comment.
Yeah, not totally sure what to do here. If the model has People object that references a SpaceType (instead of a Space), then the IDF's People object references a SpaceList. So I think we'd want to support this (somehow), and the resulting IDF's ZoneMRTCalculation object would look something like (?)
ZoneMRTCalculation,
[...]
Story 1 East Perimeter Space Baseline Model People, !- People 1 Name
0.25, !- MRT Weighting Factor 1
Story 1 West Perimeter Space Baseline Model People, !- People 2 Name
0.75, !- MRT Weighting Factor 2
There was a problem hiding this comment.
I think your current implementation which enforces things at model time plus FT time is fine
More importantly this is going to be a very niche object, so if you choose to use it you can also deal with the preconditions.
For eg I don't think we need to check is assigned to a spaceType and the sp has only one space
| * This constructor shouldn't be used, ThermalZone::getZoneMRTCalculation should be preferred | ||
| * as it will either instantiate a new object or return the existing one. | ||
| * A ThermalZone should not have more than one ZoneMRTCalculation. If you try to instantiate a second one for a given | ||
| * ThermalZone this constructor will throw. | ||
| */ | ||
| explicit ZoneMRTCalculation(const ThermalZone& thermalZone); |
There was a problem hiding this comment.
Ok, I follow.
But since you rightfully want the constructor to NOT be used, make it protected, and friend ThermalZone_Impl
There was a problem hiding this comment.
Should we do the same for ZonePropertyUserViewFactorsBySurfaceName?
|
|
||
| ThermalZone thermalZone() const; | ||
|
|
||
| std::vector<MRTWeightingFactor> mrtWeightingFactors() const; |
There was a problem hiding this comment.
This is a nitpick, but I like to handle all extensible fields next to each other inside the @other section
| // Direct construction is intentionally blocked once the ThermalZone already has one object. | ||
| EXPECT_THROW((ZoneMRTCalculation(thermalZone)), openstudio::Exception); | ||
| EXPECT_EQ(size + 1, model.modelObjects().size()); |
There was a problem hiding this comment.
This shouldn't compile after you make it protected
| boost::optional<ThermalZone> thermalZone = space->thermalZone(); | ||
| if (!thermalZone || (thermalZone->handle() != this->thermalZone().handle())) { | ||
| LOG(Error, "Cannot add " << people.briefDescription() << " to " << briefDescription() << " because it is not assigned to the ThermalZone."); | ||
| return result; | ||
| } |
There was a problem hiding this comment.
| boost::optional<ThermalZone> thermalZone = space->thermalZone(); | |
| if (!thermalZone || (thermalZone->handle() != this->thermalZone().handle())) { | |
| LOG(Error, "Cannot add " << people.briefDescription() << " to " << briefDescription() << " because it is not assigned to the ThermalZone."); | |
| return result; | |
| } | |
| boost::optional<ThermalZone> thermalZone = space->thermalZone(); | |
| if (!thermalZone) { | |
| LOG(Error, "Cannot add " << people.briefDescription() << " to " << briefDescription() << " because it is not assigned to any ThermalZone."); | |
| } else if (thermalZone->handle() != this->thermalZone().handle()) { | |
| LOG(Error, "Cannot add " << people.briefDescription() << " to " << briefDescription() << " because it is assigned to ThermalZone '" << thermalZone->nameString() << "' instead of '" << this->thermalZone().nameString() << "'."); | |
| return result; | |
| } |
| for (const MRTWeightingFactor& mrtWeightingFactor : mrtWeightingFactors) { | ||
| People people = mrtWeightingFactor.people(); | ||
|
|
||
| boost::optional<Space> space = people.space(); | ||
| boost::optional<ThermalZone> thermalZone; | ||
| if (space) { | ||
| thermalZone = space->thermalZone(); | ||
| } |
There was a problem hiding this comment.
You need to throw and skip if !people.space() to begin with, saying it must be part of a space.
I know you're incidentally doing this (if people.space() is empty, so will thermalZone), but it's not as clear as it can be
| continue; | ||
| } | ||
|
|
||
| translatedMRTWeightingFactors.emplace_back(_people->name().get(), value); |
There was a problem hiding this comment.
Nitpick: use nameString() everywhere
| for (const auto& translatedMRTWeightingFactor : translatedMRTWeightingFactors) { | ||
| auto eg = idfObject.pushExtensibleGroup(); | ||
| eg.setString(ZoneMRTCalculationExtensibleFields::PeopleName, translatedMRTWeightingFactor.first); | ||
| eg.setDouble(ZoneMRTCalculationExtensibleFields::MRTWeightingFactor, translatedMRTWeightingFactor.second); | ||
| } |
There was a problem hiding this comment.
Nitpick: structured bindings for the win
| for (const auto& translatedMRTWeightingFactor : translatedMRTWeightingFactors) { | |
| auto eg = idfObject.pushExtensibleGroup(); | |
| eg.setString(ZoneMRTCalculationExtensibleFields::PeopleName, translatedMRTWeightingFactor.first); | |
| eg.setDouble(ZoneMRTCalculationExtensibleFields::MRTWeightingFactor, translatedMRTWeightingFactor.second); | |
| } | |
| for (const auto& [name, weight] : translatedMRTWeightingFactors) { | |
| auto eg = idfObject.pushExtensibleGroup(); | |
| eg.setString(ZoneMRTCalculationExtensibleFields::PeopleName, name); | |
| eg.setDouble(ZoneMRTCalculationExtensibleFields::MRTWeightingFactor, weight); | |
| } |
Pull request overview
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.