From afefd660aa61ff71f33d48accd8766468e382952 Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Tue, 1 Sep 2026 14:07:42 -0700 Subject: [PATCH 01/13] Update both IDDs for new ZoneMRTCalculation object. --- resources/energyplus/ProposedEnergy+.idd | 135 +---------------------- resources/model/OpenStudio.idd | 32 ++++++ 2 files changed, 33 insertions(+), 134 deletions(-) diff --git a/resources/energyplus/ProposedEnergy+.idd b/resources/energyplus/ProposedEnergy+.idd index a68c6e9e0e..a5a0e7727d 100644 --- a/resources/energyplus/ProposedEnergy+.idd +++ b/resources/energyplus/ProposedEnergy+.idd @@ -10271,144 +10271,11 @@ ZoneMRTCalculation, \note A People object expanded across multiple Spaces must use an expanded instance name \note formed as " "; its original input name is not accepted. \note People instances in automatically generated remainder Spaces are not supported. - N1 , \field MRT Weighting Factor 1 + N1 ; \field MRT Weighting Factor 1 \type real \minimum 0.0 \maximum 1.0 \required-field - A3 , \field People 2 Name - \type object-list - \object-list PeopleNames - N2 , \field MRT Weighting Factor 2 - \type real - \minimum 0.0 - \maximum 1.0 - A4 , \field People 3 Name - \type object-list - \object-list PeopleNames - N3 , \field MRT Weighting Factor 3 - \type real - \minimum 0.0 - \maximum 1.0 - A5 , \field People 4 Name - \type object-list - \object-list PeopleNames - N4 , \field MRT Weighting Factor 4 - \type real - \minimum 0.0 - \maximum 1.0 - A6 , \field People 5 Name - \type object-list - \object-list PeopleNames - N5 , \field MRT Weighting Factor 5 - \type real - \minimum 0.0 - \maximum 1.0 - A7 , \field People 6 Name - \type object-list - \object-list PeopleNames - N6 , \field MRT Weighting Factor 6 - \type real - \minimum 0.0 - \maximum 1.0 - A8 , \field People 7 Name - \type object-list - \object-list PeopleNames - N7 , \field MRT Weighting Factor 7 - \type real - \minimum 0.0 - \maximum 1.0 - A9 , \field People 8 Name - \type object-list - \object-list PeopleNames - N8 , \field MRT Weighting Factor 8 - \type real - \minimum 0.0 - \maximum 1.0 - A10 , \field People 9 Name - \type object-list - \object-list PeopleNames - N9 , \field MRT Weighting Factor 9 - \type real - \minimum 0.0 - \maximum 1.0 - A11, \field People 10 Name - \type object-list - \object-list PeopleNames - N10, \field MRT Weighting Factor 10 - \type real - \minimum 0.0 - \maximum 1.0 - A12, \field People 11 Name - \type object-list - \object-list PeopleNames - N11, \field MRT Weighting Factor 11 - \type real - \minimum 0.0 - \maximum 1.0 - A13, \field People 12 Name - \type object-list - \object-list PeopleNames - N12, \field MRT Weighting Factor 12 - \type real - \minimum 0.0 - \maximum 1.0 - A14, \field People 13 Name - \type object-list - \object-list PeopleNames - N13, \field MRT Weighting Factor 13 - \type real - \minimum 0.0 - \maximum 1.0 - A15, \field People 14 Name - \type object-list - \object-list PeopleNames - N14, \field MRT Weighting Factor 14 - \type real - \minimum 0.0 - \maximum 1.0 - A16, \field People 15 Name - \type object-list - \object-list PeopleNames - N15, \field MRT Weighting Factor 15 - \type real - \minimum 0.0 - \maximum 1.0 - A17, \field People 16 Name - \type object-list - \object-list PeopleNames - N16, \field MRT Weighting Factor 16 - \type real - \minimum 0.0 - \maximum 1.0 - A18, \field People 17 Name - \type object-list - \object-list PeopleNames - N17, \field MRT Weighting Factor 17 - \type real - \minimum 0.0 - \maximum 1.0 - A19, \field People 18 Name - \type object-list - \object-list PeopleNames - N18, \field MRT Weighting Factor 18 - \type real - \minimum 0.0 - \maximum 1.0 - A20, \field People 19 Name - \type object-list - \object-list PeopleNames - N19, \field MRT Weighting Factor 19 - \type real - \minimum 0.0 - \maximum 1.0 - A21, \field People 20 Name - \type object-list - \object-list PeopleNames - N20; \field MRT Weighting Factor 20 - \type real - \minimum 0.0 - \maximum 1.0 \group Detailed Ground Heat Transfer diff --git a/resources/model/OpenStudio.idd b/resources/model/OpenStudio.idd index bc016ed874..70fa199e76 100644 --- a/resources/model/OpenStudio.idd +++ b/resources/model/OpenStudio.idd @@ -6815,6 +6815,38 @@ OS:ZoneProperty:UserViewFactors:BySurfaceName, \type real \maximum 1.0 +OS:ZoneMRTCalculation, + \min-fields 4 + \memo Blends MRT values calculated for People instances into one Zone MRT. + \memo The weighting factors must sum to a value from 0.0 through 1.0. + \extensible:2 - repeat last two fields, remembering to remove ; from "inner" fields. + \memo Only one ZoneMRTCalculation object is allowed for each Zone. + \memo All referenced People instances must be in the named Zone and must select at least + \memo one Thermal Comfort Model Type. + A1, \field Handle + \type handle + \required-field + A2, \field Thermal Zone Name + \type alpha + \type object-list + \object-list ThermalZoneNames + \required-field + \note This field accepts a Zone name only, not a Space or SpaceList name. + A3, \field People 1 Name + \begin-extensible + \type object-list + \object-list PeopleNames + \required-field + \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 " "; its original input name is not accepted. + \note People instances in automatically generated remainder Spaces are not supported. + N1; \field MRT Weighting Factor 1 + \type real + \minimum 0.0 + \maximum 1.0 + \required-field + OS:SurfaceProperty:SurroundingSurfaces, \min-fields 9 \memo This object defines a list of surrounding surfaces for an exterior surface. From 03627f168982d75b74619b3cfcfc87e45632ee60 Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Tue, 1 Sep 2026 14:19:58 -0700 Subject: [PATCH 02/13] Add model files for new ZoneMRTCalculation object. --- src/model/CMakeLists.txt | 3 + src/model/ConcreteModelObjects.hpp | 2 + src/model/ThermalZone.cpp | 22 ++ src/model/ThermalZone.hpp | 3 + src/model/ThermalZone_Impl.hpp | 3 + src/model/ZoneMRTCalculation.cpp | 327 ++++++++++++++++++++++++++ src/model/ZoneMRTCalculation.hpp | 124 ++++++++++ src/model/ZoneMRTCalculation_Impl.hpp | 94 ++++++++ 8 files changed, 578 insertions(+) create mode 100644 src/model/ZoneMRTCalculation.cpp create mode 100644 src/model/ZoneMRTCalculation.hpp create mode 100644 src/model/ZoneMRTCalculation_Impl.hpp diff --git a/src/model/CMakeLists.txt b/src/model/CMakeLists.txt index ee61221148..e7cdb771e4 100644 --- a/src/model/CMakeLists.txt +++ b/src/model/CMakeLists.txt @@ -1843,6 +1843,9 @@ set(${target_name}_src ZoneMixing.hpp ZoneMixing_Impl.hpp ZoneMixing.cpp + ZoneMRTCalculation.hpp + ZoneMRTCalculation_Impl.hpp + ZoneMRTCalculation.cpp ZonePropertyUserViewFactorsBySurfaceName.hpp ZonePropertyUserViewFactorsBySurfaceName_Impl.hpp ZonePropertyUserViewFactorsBySurfaceName.cpp diff --git a/src/model/ConcreteModelObjects.hpp b/src/model/ConcreteModelObjects.hpp index b128c9a412..2835b2addc 100644 --- a/src/model/ConcreteModelObjects.hpp +++ b/src/model/ConcreteModelObjects.hpp @@ -565,6 +565,7 @@ #include "ZoneHVACUnitVentilator.hpp" #include "ZoneHVACWaterToAirHeatPump.hpp" #include "ZoneMixing.hpp" +#include "ZoneMRTCalculation.hpp" #include "ZonePropertyUserViewFactorsBySurfaceName.hpp" #include "ZoneVentilationDesignFlowRate.hpp" #include "ZoneVentilationWindandStackOpenArea.hpp" @@ -1129,6 +1130,7 @@ #include "ZoneHVACUnitVentilator_Impl.hpp" #include "ZoneHVACWaterToAirHeatPump_Impl.hpp" #include "ZoneMixing_Impl.hpp" +#include "ZoneMRTCalculation_Impl.hpp" #include "ZonePropertyUserViewFactorsBySurfaceName_Impl.hpp" #include "ZoneVentilationDesignFlowRate_Impl.hpp" #include "ZoneVentilationWindandStackOpenArea_Impl.hpp" diff --git a/src/model/ThermalZone.cpp b/src/model/ThermalZone.cpp index 7fcdd5390a..bb6bbf7cb0 100644 --- a/src/model/ThermalZone.cpp +++ b/src/model/ThermalZone.cpp @@ -87,6 +87,8 @@ #include "AirflowNetworkZone_Impl.hpp" #include "ZonePropertyUserViewFactorsBySurfaceName.hpp" #include "ZonePropertyUserViewFactorsBySurfaceName_Impl.hpp" +#include "ZoneMRTCalculation.hpp" +#include "ZoneMRTCalculation_Impl.hpp" #include "ScheduleTypeLimits.hpp" #include "ScheduleTypeRegistry.hpp" @@ -2650,6 +2652,22 @@ namespace model { return zoneProp; } + ZoneMRTCalculation ThermalZone_Impl::getZoneMRTCalculation() const { + auto thisThermalZone = getObject(); + std::vector zoneMRTCalculations = + thisThermalZone.getModelObjectSources(ZoneMRTCalculation::iddObjectType()); + if (!zoneMRTCalculations.empty()) { + if (zoneMRTCalculations.size() > 1u) { + OS_ASSERT(false); + LOG(Error, briefDescription() << " is referenced by more than one ZoneMRTCalculation, returning the first"); + } + return zoneMRTCalculations[0]; + } + + ZoneMRTCalculation zoneMRTCalculation(thisThermalZone); + return zoneMRTCalculation; + } + boost::optional ThermalZone_Impl::getAutosizedValueFromZoneSizes(const std::string& columnName, const std::string& loadType) const { // Check that the model has a sql file if (!model().sqlFile()) { @@ -3394,6 +3412,10 @@ SELECT {} FROM ZoneSizes return getImpl()->getZonePropertyUserViewFactorsBySurfaceName(); } + ZoneMRTCalculation ThermalZone::getZoneMRTCalculation() const { + return getImpl()->getZoneMRTCalculation(); + } + /// @cond ThermalZone::ThermalZone(std::shared_ptr impl) : HVACComponent(std::move(impl)) {} /// @endcond diff --git a/src/model/ThermalZone.hpp b/src/model/ThermalZone.hpp index bac07b70be..93fe22d180 100644 --- a/src/model/ThermalZone.hpp +++ b/src/model/ThermalZone.hpp @@ -27,6 +27,7 @@ namespace model { class ZoneMixing; class AirflowNetworkZone; class ZonePropertyUserViewFactorsBySurfaceName; + class ZoneMRTCalculation; class Schedule; namespace detail { @@ -104,6 +105,8 @@ namespace model { ZonePropertyUserViewFactorsBySurfaceName getZonePropertyUserViewFactorsBySurfaceName() const; + ZoneMRTCalculation getZoneMRTCalculation() const; + //@} /** @name Setters */ //@{ diff --git a/src/model/ThermalZone_Impl.hpp b/src/model/ThermalZone_Impl.hpp index 36a72f7141..b314d075b6 100644 --- a/src/model/ThermalZone_Impl.hpp +++ b/src/model/ThermalZone_Impl.hpp @@ -28,6 +28,7 @@ namespace model { class ZoneHVACEquipmentList; class AirflowNetworkZone; class ZonePropertyUserViewFactorsBySurfaceName; + class ZoneMRTCalculation; class Schedule; namespace detail { @@ -123,6 +124,8 @@ namespace model { ZonePropertyUserViewFactorsBySurfaceName getZonePropertyUserViewFactorsBySurfaceName() const; + ZoneMRTCalculation getZoneMRTCalculation() const; + //@} /** @name Setters */ //@{ diff --git a/src/model/ZoneMRTCalculation.cpp b/src/model/ZoneMRTCalculation.cpp new file mode 100644 index 0000000000..31995d8c4a --- /dev/null +++ b/src/model/ZoneMRTCalculation.cpp @@ -0,0 +1,327 @@ +/*********************************************************************************************************************** +* OpenStudio(R), Copyright (c) Alliance for Energy Innovation, LLC. +* See also https://openstudio.net/license +***********************************************************************************************************************/ + +#include "ZoneMRTCalculation.hpp" +#include "ZoneMRTCalculation_Impl.hpp" +#include "People.hpp" +#include "People_Impl.hpp" +#include "Space.hpp" +#include "Space_Impl.hpp" +#include "ThermalZone.hpp" +#include "ThermalZone_Impl.hpp" + +#include "Model.hpp" +#include "Model_Impl.hpp" + +#include "ModelExtensibleGroup.hpp" + +#include +#include + +#include "../utilities/core/Assert.hpp" + +#include + +namespace openstudio { +namespace model { + + /***************************************************************************************************************************************************** +* M R T W E I G H T I N G F A C T O R * +*****************************************************************************************************************************************************/ + MRTWeightingFactor::MRTWeightingFactor(const People& people, double mrtWeightingFactor) + : m_people(people), m_mrtWeightingFactor(mrtWeightingFactor) { + if ((m_mrtWeightingFactor < 0.0) || (m_mrtWeightingFactor > 1.0)) { + LOG_AND_THROW("Unable to create MRT weighting factor, factor of " << m_mrtWeightingFactor << " is not between 0 and 1"); + } + } + + People MRTWeightingFactor::people() const { + return m_people; + } + + double MRTWeightingFactor::mrtWeightingFactor() const { + return m_mrtWeightingFactor; + } + + std::ostream& operator<<(std::ostream& out, const openstudio::model::MRTWeightingFactor& mrtWeightingFactor) { + out << "(people " << mrtWeightingFactor.people().iddObject().name() << "='" << mrtWeightingFactor.people().nameString() + << "', MRT weighting factor=" << mrtWeightingFactor.mrtWeightingFactor() << ")"; + return out; + } + + /***************************************************************************************************************************************************** +* Z O N E M R T C A L C U L A T I O N * +*****************************************************************************************************************************************************/ + + namespace detail { + + ZoneMRTCalculation_Impl::ZoneMRTCalculation_Impl(const IdfObject& idfObject, Model_Impl* model, bool keepHandle) + : ModelObject_Impl(idfObject, model, keepHandle) { + OS_ASSERT(idfObject.iddObject().type() == ZoneMRTCalculation::iddObjectType()); + } + + ZoneMRTCalculation_Impl::ZoneMRTCalculation_Impl(const openstudio::detail::WorkspaceObject_Impl& other, Model_Impl* model, bool keepHandle) + : ModelObject_Impl(other, model, keepHandle) { + OS_ASSERT(other.iddObject().type() == ZoneMRTCalculation::iddObjectType()); + } + + ZoneMRTCalculation_Impl::ZoneMRTCalculation_Impl(const ZoneMRTCalculation_Impl& other, Model_Impl* model, bool keepHandle) + : ModelObject_Impl(other, model, keepHandle) {} + + const std::vector& ZoneMRTCalculation_Impl::outputVariableNames() const { + static const std::vector result; + return result; + } + + IddObjectType ZoneMRTCalculation_Impl::iddObjectType() const { + return ZoneMRTCalculation::iddObjectType(); + } + + ModelObject ZoneMRTCalculation_Impl::clone(Model /*model*/) const { + LOG_AND_THROW("Cloning isn't allowed for ZoneMRTCalculation in order to guarantee that every ZoneMRTCalculation has a thermal zone, and" + "that a thermal zone must have only one ZoneMRTCalculation."); + } + + ThermalZone ZoneMRTCalculation_Impl::thermalZone() const { + boost::optional thermalZone = + getObject().getModelObjectTarget(OS_ZoneMRTCalculationFields::ThermalZoneName); + OS_ASSERT(thermalZone); + return thermalZone.get(); + } + + unsigned int ZoneMRTCalculation_Impl::numberofMRTWeightingFactors() const { + return numExtensibleGroups(); + } + + boost::optional ZoneMRTCalculation_Impl::mrtWeightingFactorIndex(const MRTWeightingFactor& mrtWeightingFactor) const { + boost::optional result; + + auto egs = castVector(extensibleGroups()); + auto peopleHandle = toString(mrtWeightingFactor.people().handle()); + auto it = std::find_if(egs.begin(), egs.end(), [&](const WorkspaceExtensibleGroup& eg) { + return eg.getField(OS_ZoneMRTCalculationExtensibleFields::PeopleName).get() == peopleHandle; + }); + + if (it != egs.end()) { + result = std::distance(egs.begin(), it); + } + + return result; + } + + boost::optional ZoneMRTCalculation_Impl::getPeople(unsigned groupIndex) const { + boost::optional result; + + if (groupIndex >= numberofMRTWeightingFactors()) { + LOG(Error, "Asked to get People with index " << groupIndex << ", but " << briefDescription() << " has just " << numberofMRTWeightingFactors() + << " MRT weighting factors."); + return result; + } + + auto group = getExtensibleGroup(groupIndex).cast(); + result = group.getModelObjectTarget(OS_ZoneMRTCalculationExtensibleFields::PeopleName); + + if (!result) { + LOG(Error, "Could not retrieve People Name for extensible group " << group.groupIndex() << "."); + } + + return result; + } + + boost::optional ZoneMRTCalculation_Impl::getMRTWeightingFactorValue(unsigned groupIndex) const { + boost::optional result; + + if (groupIndex >= numberofMRTWeightingFactors()) { + LOG(Error, "Asked to get MRT Weighting Factor with index " << groupIndex << ", but " << briefDescription() << " has just " + << numberofMRTWeightingFactors() << " MRT weighting factors."); + return result; + } + + auto group = getExtensibleGroup(groupIndex).cast(); + result = group.getDouble(OS_ZoneMRTCalculationExtensibleFields::MRTWeightingFactor); + + if (!result) { + LOG(Error, "Could not retrieve MRT Weighting Factor for extensible group " << group.groupIndex() << "."); + } + + return result; + } + + boost::optional ZoneMRTCalculation_Impl::getMRTWeightingFactor(unsigned groupIndex) const { + boost::optional result; + + boost::optional people = getPeople(groupIndex); + boost::optional value = getMRTWeightingFactorValue(groupIndex); + + if (people && value) { + result = MRTWeightingFactor(people.get(), value.get()); + } + + return result; + } + + std::vector ZoneMRTCalculation_Impl::mrtWeightingFactors() const { + std::vector result; + + for (unsigned i = 0; i < numberofMRTWeightingFactors(); ++i) { + boost::optional mrtWeightingFactor = getMRTWeightingFactor(i); + if (mrtWeightingFactor) { + result.push_back(mrtWeightingFactor.get()); + } else { + LOG(Error, briefDescription() << " has an invalid MRTWeightingFactor group at index " << i); + } + } + + return result; + } + + bool ZoneMRTCalculation_Impl::addMRTWeightingFactor(const MRTWeightingFactor& mrtWeightingFactor) { + bool result = false; + + People people = mrtWeightingFactor.people(); + + if (people.model() != model()) { + LOG(Error, "Cannot add " << people.briefDescription() << " to " << briefDescription() << " because they are in different models."); + return result; + } + + boost::optional space = people.space(); + if (!space) { + LOG(Error, "Cannot add " << people.briefDescription() << " to " << briefDescription() << " because it is not directly assigned to a Space."); + return result; + } + + boost::optional 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 existingIndex = mrtWeightingFactorIndex(mrtWeightingFactor); + if (existingIndex) { + boost::optional existingMRTWeightingFactor = getMRTWeightingFactor(existingIndex.get()); + OS_ASSERT(existingMRTWeightingFactor); + LOG(Warn, "For " << briefDescription() << ", MRTWeightingFactor already exists, will be modified in place from " + << existingMRTWeightingFactor.get() << " to " << mrtWeightingFactor << "."); + } + + std::vector temp; + ModelExtensibleGroup group = (existingIndex ? getExtensibleGroup(existingIndex.get()).cast() + : pushExtensibleGroup(temp, false).cast()); + + bool peopleSet = group.setPointer(OS_ZoneMRTCalculationExtensibleFields::PeopleName, people.handle(), false); + if (!peopleSet) { + LOG(Error, "Unable to add MRT Weighting Factor which has an incompatible People object to " << briefDescription()); + OS_ASSERT(false); + } + + bool factorSet = group.setDouble(OS_ZoneMRTCalculationExtensibleFields::MRTWeightingFactor, mrtWeightingFactor.mrtWeightingFactor()); + + if (peopleSet && factorSet) { + result = true; + } else { + getObject().eraseExtensibleGroup(group.groupIndex()); + } + + return result; + } + + bool ZoneMRTCalculation_Impl::addMRTWeightingFactor(const People& people, double mrtWeightingFactor) { + MRTWeightingFactor weightingFactor(people, mrtWeightingFactor); + return addMRTWeightingFactor(weightingFactor); + } + + bool ZoneMRTCalculation_Impl::addMRTWeightingFactors(const std::vector& mrtWeightingFactors) { + bool result = true; + + for (const MRTWeightingFactor& mrtWeightingFactor : mrtWeightingFactors) { + bool thisResult = addMRTWeightingFactor(mrtWeightingFactor); + if (!thisResult) { + LOG(Error, "Could not add mrtWeightingFactor " << mrtWeightingFactor << " to " << briefDescription() << ". Continuing with others."); + } + } + + return result; + } + + bool ZoneMRTCalculation_Impl::removeMRTWeightingFactor(unsigned groupIndex) { + bool result = false; + + if (groupIndex < numberofMRTWeightingFactors()) { + getObject().eraseExtensibleGroup(groupIndex); + result = true; + } + + return result; + } + + void ZoneMRTCalculation_Impl::removeAllMRTWeightingFactors() { + getObject().clearExtensibleGroups(); + } + + } // namespace detail + + ZoneMRTCalculation::ZoneMRTCalculation(const ThermalZone& thermalZone) : ModelObject(ZoneMRTCalculation::iddObjectType(), thermalZone.model()) { + std::vector existingObjects = thermalZone.getModelObjectSources(ZoneMRTCalculation::iddObjectType()); + if (!existingObjects.empty()) { + remove(); + LOG_AND_THROW(thermalZone.briefDescription() << " already has a ZoneMRTCalculation, cannot create a new one."); + } + OS_ASSERT(getImpl()); + + bool ok = setPointer(OS_ZoneMRTCalculationFields::ThermalZoneName, thermalZone.handle()); + OS_ASSERT(ok); + } + + IddObjectType ZoneMRTCalculation::iddObjectType() { + return {IddObjectType::OS_ZoneMRTCalculation}; + } + + ThermalZone ZoneMRTCalculation::thermalZone() const { + return getImpl()->thermalZone(); + } + + std::vector ZoneMRTCalculation::mrtWeightingFactors() const { + return getImpl()->mrtWeightingFactors(); + } + + unsigned int ZoneMRTCalculation::numberofMRTWeightingFactors() const { + return getImpl()->numberofMRTWeightingFactors(); + } + + boost::optional ZoneMRTCalculation::mrtWeightingFactorIndex(const MRTWeightingFactor& mrtWeightingFactor) const { + return getImpl()->mrtWeightingFactorIndex(mrtWeightingFactor); + } + + boost::optional ZoneMRTCalculation::getMRTWeightingFactor(unsigned groupIndex) const { + return getImpl()->getMRTWeightingFactor(groupIndex); + } + + bool ZoneMRTCalculation::addMRTWeightingFactor(const MRTWeightingFactor& mrtWeightingFactor) { + return getImpl()->addMRTWeightingFactor(mrtWeightingFactor); + } + + bool ZoneMRTCalculation::addMRTWeightingFactor(const People& people, double mrtWeightingFactor) { + return getImpl()->addMRTWeightingFactor(people, mrtWeightingFactor); + } + + bool ZoneMRTCalculation::addMRTWeightingFactors(const std::vector& mrtWeightingFactors) { + return getImpl()->addMRTWeightingFactors(mrtWeightingFactors); + } + + void ZoneMRTCalculation::removeMRTWeightingFactor(int groupIndex) { + getImpl()->removeMRTWeightingFactor(groupIndex); + } + + void ZoneMRTCalculation::removeAllMRTWeightingFactors() { + getImpl()->removeAllMRTWeightingFactors(); + } + + /// @cond + ZoneMRTCalculation::ZoneMRTCalculation(std::shared_ptr impl) : ModelObject(std::move(impl)) {} + /// @endcond + +} // namespace model +} // namespace openstudio \ No newline at end of file diff --git a/src/model/ZoneMRTCalculation.hpp b/src/model/ZoneMRTCalculation.hpp new file mode 100644 index 0000000000..a3a272b1c4 --- /dev/null +++ b/src/model/ZoneMRTCalculation.hpp @@ -0,0 +1,124 @@ +/*********************************************************************************************************************** +* OpenStudio(R), Copyright (c) Alliance for Energy Innovation, LLC. +* See also https://openstudio.net/license +***********************************************************************************************************************/ + +#ifndef MODEL_ZONEMRTCALCULATION_HPP +#define MODEL_ZONEMRTCALCULATION_HPP + +#include "ModelAPI.hpp" +#include "ModelObject.hpp" +#include "People.hpp" + +namespace openstudio { +namespace model { + + class ThermalZone; + + namespace detail { + + class ZoneMRTCalculation_Impl; + + } // namespace detail + + /** This class implements an MRT weighting factor. */ + class MODEL_API MRTWeightingFactor + { + public: + MRTWeightingFactor(const People& people, double mrtWeightingFactor); + + People people() const; + double mrtWeightingFactor() const; + + private: + People m_people; + double m_mrtWeightingFactor; + REGISTER_LOGGER("openstudio.model.MRTWeightingFactor"); + }; + + /** \relates MRTWeightingFactor */ + MODEL_API std::ostream& operator<<(std::ostream& out, const openstudio::model::MRTWeightingFactor& mrtWeightingFactor); + + /** ZoneMRTCalculation is a ModelObject that wraps the OpenStudio IDD object 'OS:ZoneMRTCalculation'. */ + class MODEL_API ZoneMRTCalculation : public ModelObject + { + public: + /** @name Constructors and Destructors */ + //@{ + + /* + * 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); + + virtual ~ZoneMRTCalculation() override = default; + // Default the copy and move operators because the virtual dtor is explicit + ZoneMRTCalculation(const ZoneMRTCalculation& other) = default; + ZoneMRTCalculation(ZoneMRTCalculation&& other) = default; + ZoneMRTCalculation& operator=(const ZoneMRTCalculation&) = default; + ZoneMRTCalculation& operator=(ZoneMRTCalculation&&) = default; + + //@} + + static IddObjectType iddObjectType(); + + /** @name Getters */ + //@{ + + ThermalZone thermalZone() const; + + std::vector mrtWeightingFactors() const; + + unsigned int numberofMRTWeightingFactors() const; + + boost::optional mrtWeightingFactorIndex(const MRTWeightingFactor& mrtWeightingFactor) const; + + boost::optional getMRTWeightingFactor(unsigned groupIndex) const; + + //@} + /** @name Setters */ + //@{ + + bool addMRTWeightingFactor(const MRTWeightingFactor& mrtWeightingFactor); + + bool addMRTWeightingFactor(const People& people, double mrtWeightingFactor); + + bool addMRTWeightingFactors(const std::vector& mrtWeightingFactors); + + void removeMRTWeightingFactor(int groupIndex); + + void removeAllMRTWeightingFactors(); + + //@} + /** @name Other */ + //@{ + + //@} + protected: + /// @cond + using ImplType = detail::ZoneMRTCalculation_Impl; + + explicit ZoneMRTCalculation(std::shared_ptr impl); + + friend class detail::ZoneMRTCalculation_Impl; + friend class Model; + friend class IdfObject; + friend class openstudio::detail::IdfObject_Impl; + /// @endcond + private: + REGISTER_LOGGER("openstudio.model.ZoneMRTCalculation"); + }; + + /** \relates ZoneMRTCalculation*/ + using OptionalZoneMRTCalculation = boost::optional; + + /** \relates ZoneMRTCalculation*/ + using ZoneMRTCalculationVector = std::vector; + +} // namespace model +} // namespace openstudio + +#endif // MODEL_ZONEMRTCALCULATION_HPP \ No newline at end of file diff --git a/src/model/ZoneMRTCalculation_Impl.hpp b/src/model/ZoneMRTCalculation_Impl.hpp new file mode 100644 index 0000000000..a7559d2141 --- /dev/null +++ b/src/model/ZoneMRTCalculation_Impl.hpp @@ -0,0 +1,94 @@ +/*********************************************************************************************************************** +* OpenStudio(R), Copyright (c) Alliance for Energy Innovation, LLC. +* See also https://openstudio.net/license +***********************************************************************************************************************/ + +#ifndef MODEL_ZONEMRTCALCULATION_IMPL_HPP +#define MODEL_ZONEMRTCALCULATION_IMPL_HPP + +#include "ModelAPI.hpp" +#include "ModelObject_Impl.hpp" + +namespace openstudio { +namespace model { + + class MRTWeightingFactor; + class People; + class ThermalZone; + + namespace detail { + + /** ZoneMRTCalculation_Impl is a ModelObject_Impl that is the implementation class for ZoneMRTCalculation.*/ + class MODEL_API ZoneMRTCalculation_Impl : public ModelObject_Impl + { + + public: + /** @name Constructors and Destructors */ + //@{ + + ZoneMRTCalculation_Impl(const IdfObject& idfObject, Model_Impl* model, bool keepHandle); + + ZoneMRTCalculation_Impl(const openstudio::detail::WorkspaceObject_Impl& other, Model_Impl* model, bool keepHandle); + + ZoneMRTCalculation_Impl(const ZoneMRTCalculation_Impl& other, Model_Impl* model, bool keepHandle); + + virtual ~ZoneMRTCalculation_Impl() override = default; + + //@} + /** @name Virtual Methods */ + //@{ + + virtual const std::vector& outputVariableNames() const override; + + virtual IddObjectType iddObjectType() const override; + + virtual ModelObject clone(Model model) const override; + + //@} + /** @name Getters */ + //@{ + + ThermalZone thermalZone() const; + + std::vector mrtWeightingFactors() const; + + unsigned int numberofMRTWeightingFactors() const; + + boost::optional mrtWeightingFactorIndex(const MRTWeightingFactor& mrtWeightingFactor) const; + + boost::optional getMRTWeightingFactor(unsigned groupIndex) const; + + //@} + /** @name Setters */ + //@{ + + bool addMRTWeightingFactor(const MRTWeightingFactor& mrtWeightingFactor); + + bool addMRTWeightingFactor(const People& people, double mrtWeightingFactor); + + bool addMRTWeightingFactors(const std::vector& mrtWeightingFactors); + + bool removeMRTWeightingFactor(unsigned groupIndex); + + void removeAllMRTWeightingFactors(); + + //@} + /** @name Other */ + //@{ + + //@} + protected: + private: + REGISTER_LOGGER("openstudio.model.ZoneMRTCalculation"); + + boost::optional getPeople(unsigned groupIndex) const; + + boost::optional getMRTWeightingFactorValue(unsigned groupIndex) const; + }; + + } // namespace detail + +} // namespace model +} // namespace openstudio + +#endif // MODEL_ZONEMRTCALCULATION_IMPL_HPP \ No newline at end of file From 23d26d295bf4c6e0b440de2dbcae24eb2f6d0825 Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Tue, 1 Sep 2026 14:24:54 -0700 Subject: [PATCH 03/13] Add ft and rt for new ZoneMRTCalculation object. --- src/energyplus/CMakeLists.txt | 2 + src/energyplus/ForwardTranslator.cpp | 5 + src/energyplus/ForwardTranslator.hpp | 3 + .../ForwardTranslateZoneMRTCalculation.cpp | 65 ++++++++++++ src/energyplus/ReverseTranslator.cpp | 4 + src/energyplus/ReverseTranslator.hpp | 2 + .../ReverseTranslateZoneMRTCalculation.cpp | 99 +++++++++++++++++++ src/model/Model.cpp | 1 + 8 files changed, 181 insertions(+) create mode 100644 src/energyplus/ForwardTranslator/ForwardTranslateZoneMRTCalculation.cpp create mode 100644 src/energyplus/ReverseTranslator/ReverseTranslateZoneMRTCalculation.cpp diff --git a/src/energyplus/CMakeLists.txt b/src/energyplus/CMakeLists.txt index 05af5e63ad..05bec9b68e 100644 --- a/src/energyplus/CMakeLists.txt +++ b/src/energyplus/CMakeLists.txt @@ -464,6 +464,7 @@ set(${target_name}_src ForwardTranslator/ForwardTranslateZoneHVACUnitVentilator.cpp ForwardTranslator/ForwardTranslateZoneHVACWaterToAirHeatPump.cpp ForwardTranslator/ForwardTranslateZoneMixing.cpp + ForwardTranslator/ForwardTranslateZoneMRTCalculation.cpp ForwardTranslator/ForwardTranslateZonePropertyUserViewFactorsBySurfaceName.cpp ForwardTranslator/ForwardTranslateZoneVentilationDesignFlowRate.cpp ForwardTranslator/ForwardTranslateZoneVentilationWindandStackOpenArea.cpp @@ -650,6 +651,7 @@ set(${target_name}_src ReverseTranslator/ReverseTranslateZoneInfiltrationFlowCoefficient.cpp ReverseTranslator/ReverseTranslateZoneList.cpp ReverseTranslator/ReverseTranslateZoneMixing.cpp + ReverseTranslator/ReverseTranslateZoneMRTCalculation.cpp ReverseTranslator/ReverseTranslateZonePropertyUserViewFactorsBySurfaceName.cpp ReverseTranslator/ReverseTranslateZoneVentilationDesignFlowRate.cpp ReverseTranslator/ReverseTranslateZoneVentilationWindandStackOpenArea.cpp diff --git a/src/energyplus/ForwardTranslator.cpp b/src/energyplus/ForwardTranslator.cpp index 0b107afd0a..6a499239da 100644 --- a/src/energyplus/ForwardTranslator.cpp +++ b/src/energyplus/ForwardTranslator.cpp @@ -3348,6 +3348,11 @@ namespace energyplus { retVal = translateZoneMixing(mo); break; } + case openstudio::IddObjectType::OS_ZoneMRTCalculation: { + auto mo = modelObject.cast(); + retVal = translateZoneMRTCalculation(mo); + break; + } case openstudio::IddObjectType::OS_ZoneProperty_UserViewFactors_BySurfaceName: { auto mo = modelObject.cast(); retVal = translateZonePropertyUserViewFactorsBySurfaceName(mo); diff --git a/src/energyplus/ForwardTranslator.hpp b/src/energyplus/ForwardTranslator.hpp index 29f189f90f..d34ea9e744 100644 --- a/src/energyplus/ForwardTranslator.hpp +++ b/src/energyplus/ForwardTranslator.hpp @@ -503,6 +503,7 @@ namespace model { class ZoneHVACUnitVentilator; class ZoneHVACWaterToAirHeatPump; class ZoneMixing; + class ZoneMRTCalculation; class ZonePropertyUserViewFactorsBySurfaceName; class ZoneVentilationDesignFlowRate; class ZoneVentilationWindandStackOpenArea; @@ -1616,6 +1617,8 @@ namespace energyplus { boost::optional translateZoneMixing(model::ZoneMixing& modelObject); + boost::optional translateZoneMRTCalculation(model::ZoneMRTCalculation& modelObject); + boost::optional translateZonePropertyUserViewFactorsBySurfaceName(model::ZonePropertyUserViewFactorsBySurfaceName& modelObject); boost::optional createAirLoopHVACSupplyPath(const model::Node& demandInletNode); diff --git a/src/energyplus/ForwardTranslator/ForwardTranslateZoneMRTCalculation.cpp b/src/energyplus/ForwardTranslator/ForwardTranslateZoneMRTCalculation.cpp new file mode 100644 index 0000000000..44ac384473 --- /dev/null +++ b/src/energyplus/ForwardTranslator/ForwardTranslateZoneMRTCalculation.cpp @@ -0,0 +1,65 @@ +/*********************************************************************************************************************** +* OpenStudio(R), Copyright (c) Alliance for Energy Innovation, LLC. +* See also https://openstudio.net/license +***********************************************************************************************************************/ + +#include "../ForwardTranslator.hpp" + +#include "../../model/Model.hpp" +#include "../../model/People.hpp" +#include "../../model/People_Impl.hpp" +#include "../../model/ThermalZone.hpp" +#include "../../model/ThermalZone_Impl.hpp" +#include "../../model/ZoneMRTCalculation.hpp" +#include "../../model/ZoneMRTCalculation_Impl.hpp" + +#include "../../utilities/idf/IdfExtensibleGroup.hpp" + +#include +#include + +using namespace openstudio::model; + +namespace openstudio { + +namespace energyplus { + + boost::optional ForwardTranslator::translateZoneMRTCalculation(ZoneMRTCalculation& modelObject) { + ThermalZone zone = modelObject.thermalZone(); + boost::optional _zone = translateAndMapModelObject(zone); + if (!_zone) { + LOG(Error, "ZoneMRTCalculation references a zone '" << zone.nameString() << "' that was not translated, so it will not be translated either"); + return boost::none; + } + + std::vector mrtWeightingFactors = modelObject.mrtWeightingFactors(); + if (mrtWeightingFactors.empty()) { + LOG(Error, + "ZoneMRTCalculation for zone '" << zone.nameString() << "' doesn't have at least one MRT weighting factor, it will not be translated."); + return boost::none; + } + + IdfObject idfObject(openstudio::IddObjectType::ZoneMRTCalculation); + m_idfObjects.push_back(idfObject); + + idfObject.setString(ZoneMRTCalculationFields::ZoneName, _zone->name().get()); + + for (const MRTWeightingFactor& mrtWeightingFactor : mrtWeightingFactors) { + People people = mrtWeightingFactor.people(); + boost::optional _people = translateAndMapModelObject(people); + + if (_people) { + auto eg = idfObject.pushExtensibleGroup(); + eg.setString(ZoneMRTCalculationExtensibleFields::PeopleName, _people->name().get()); + eg.setDouble(ZoneMRTCalculationExtensibleFields::MRTWeightingFactor, mrtWeightingFactor.mrtWeightingFactor()); + } else { + LOG(Error, "Could not translate an MRTWeightingFactor group for " << modelObject.briefDescription() << ". Continuing with the rest."); + } + } + + return idfObject; + } + +} // namespace energyplus + +} // namespace openstudio \ No newline at end of file diff --git a/src/energyplus/ReverseTranslator.cpp b/src/energyplus/ReverseTranslator.cpp index 01ac3ba5ed..34a708a991 100644 --- a/src/energyplus/ReverseTranslator.cpp +++ b/src/energyplus/ReverseTranslator.cpp @@ -1054,6 +1054,10 @@ namespace energyplus { modelObject = translateZoneMixing(workspaceObject); break; } + case openstudio::IddObjectType::ZoneMRTCalculation: { + modelObject = translateZoneMRTCalculation(workspaceObject); + break; + } case openstudio::IddObjectType::ZoneProperty_UserViewFactors_BySurfaceName: { modelObject = translateZonePropertyUserViewFactorsBySurfaceName(workspaceObject); break; diff --git a/src/energyplus/ReverseTranslator.hpp b/src/energyplus/ReverseTranslator.hpp index 0688aac0d6..2f30bcf542 100644 --- a/src/energyplus/ReverseTranslator.hpp +++ b/src/energyplus/ReverseTranslator.hpp @@ -411,6 +411,8 @@ namespace energyplus { boost::optional translateZoneMixing(const WorkspaceObject& workspaceObject); + boost::optional translateZoneMRTCalculation(const WorkspaceObject& workspaceObject); + boost::optional translateZonePropertyUserViewFactorsBySurfaceName(const WorkspaceObject& workspaceObject); boost::optional translateZoneVentilationDesignFlowRate(const WorkspaceObject& workspaceObject); diff --git a/src/energyplus/ReverseTranslator/ReverseTranslateZoneMRTCalculation.cpp b/src/energyplus/ReverseTranslator/ReverseTranslateZoneMRTCalculation.cpp new file mode 100644 index 0000000000..627ed36227 --- /dev/null +++ b/src/energyplus/ReverseTranslator/ReverseTranslateZoneMRTCalculation.cpp @@ -0,0 +1,99 @@ +/*********************************************************************************************************************** +* OpenStudio(R), Copyright (c) Alliance for Energy Innovation, LLC. +* See also https://openstudio.net/license +***********************************************************************************************************************/ + +#include "../ReverseTranslator.hpp" + +#include "../../model/People.hpp" +#include "../../model/People_Impl.hpp" +#include "../../model/Space.hpp" +#include "../../model/Space_Impl.hpp" +#include "../../model/ThermalZone.hpp" +#include "../../model/ThermalZone_Impl.hpp" +#include "../../model/ZoneMRTCalculation.hpp" +#include "../../model/ZoneMRTCalculation_Impl.hpp" + +#include "../../utilities/idf/WorkspaceExtensibleGroup.hpp" + +#include +#include "../../utilities/idd/IddEnums.hpp" +#include + +#include "../../utilities/core/Assert.hpp" + +using namespace openstudio::model; + +namespace openstudio { + +namespace energyplus { + + OptionalModelObject ReverseTranslator::translateZoneMRTCalculation(const WorkspaceObject& workspaceObject) { + if (workspaceObject.iddObject().type() != IddObjectType::ZoneMRTCalculation) { + LOG(Error, "WorkspaceObject is not IddObjectType: ZoneMRTCalculation"); + return boost::none; + } + + boost::optional zoneTarget = workspaceObject.getTarget(ZoneMRTCalculationFields::ZoneName); + if (!zoneTarget) { + LOG(Error, "Error importing object: " << workspaceObject.briefDescription() << " Can't find associated Zone."); + return boost::none; + } + + OptionalModelObject zoneModelObject = translateAndMapWorkspaceObject(*zoneTarget); + if (!zoneModelObject) { + LOG(Error, "Error importing object: " << workspaceObject.briefDescription() << " Can't translate associated Zone."); + return boost::none; + } + + boost::optional thermalZone; + if (boost::optional space = zoneModelObject->optionalCast()) { + thermalZone = space->thermalZone(); + } + + if (!thermalZone) { + LOG(Error, "Error importing object: " << workspaceObject.briefDescription() << " Can't find associated ThermalZone."); + return boost::none; + } + + openstudio::model::ZoneMRTCalculation zoneMRTCalculation = thermalZone->getZoneMRTCalculation(); + + for (const IdfExtensibleGroup& idfGroup : workspaceObject.extensibleGroups()) { + auto workspaceGroup = idfGroup.cast(); + + boost::optional peopleTarget = workspaceGroup.getTarget(ZoneMRTCalculationExtensibleFields::PeopleName); + OptionalDouble mrtWeightingFactor = workspaceGroup.getDouble(ZoneMRTCalculationExtensibleFields::MRTWeightingFactor); + + if (!peopleTarget || !mrtWeightingFactor) { + LOG(Error, "Could not retrieve an MRTWeightingFactor group for " << workspaceObject.briefDescription() << ". Continuing with the rest."); + continue; + } + + OptionalModelObject peopleModelObject = translateAndMapWorkspaceObject(*peopleTarget); + if (peopleModelObject) { + if (boost::optional people = peopleModelObject->optionalCast()) { + try { + MRTWeightingFactor weightingFactor(people.get(), mrtWeightingFactor.get()); + if (!zoneMRTCalculation.addMRTWeightingFactor(weightingFactor)) { + LOG(Warn, "Adding MRTWeightingFactor in ThermalZone " << thermalZone->nameString() + << " failed for mrtWeightingFactor=" << weightingFactor << "."); + } + } catch (...) { + LOG(Error, "Could not create MRTWeightingFactor in ThermalZone " << thermalZone->nameString() << " for people=(" + << people->briefDescription() + << ") and mrtWeightingFactor=" << mrtWeightingFactor.get() << "."); + } + } else { + LOG(Error, "Could not translate People for an MRTWeightingFactor group in " << workspaceObject.briefDescription() << "."); + } + } else { + LOG(Error, "Could not translate People for an MRTWeightingFactor group in " << workspaceObject.briefDescription() << "."); + } + } + + return zoneMRTCalculation; + } + +} // namespace energyplus + +} // namespace openstudio \ No newline at end of file diff --git a/src/model/Model.cpp b/src/model/Model.cpp index 47f31580b5..cad6eae944 100644 --- a/src/model/Model.cpp +++ b/src/model/Model.cpp @@ -4479,6 +4479,7 @@ namespace model { REGISTER_CONSTRUCTOR(ZoneHVACEnergyRecoveryVentilator); REGISTER_CONSTRUCTOR(ZoneHVACUnitVentilator); REGISTER_CONSTRUCTOR(ZoneMixing); + REGISTER_CONSTRUCTOR(ZoneMRTCalculation); REGISTER_CONSTRUCTOR(ZonePropertyUserViewFactorsBySurfaceName); REGISTER_CONSTRUCTOR(ZoneVentilationDesignFlowRate); REGISTER_CONSTRUCTOR(ZoneVentilationWindandStackOpenArea); From 0c8684f3e2b94f6ca672df9e9af93017d225503d Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Tue, 1 Sep 2026 14:30:35 -0700 Subject: [PATCH 04/13] Add model and translation tests for new ZoneMRTCalculation object. --- src/energyplus/CMakeLists.txt | 1 + .../Test/ZoneMRTCalculation_GTest.cpp | 117 ++++++++++++++++++ src/model/CMakeLists.txt | 1 + src/model/test/ZoneMRTCalculation_GTest.cpp | 98 +++++++++++++++ 4 files changed, 217 insertions(+) create mode 100644 src/energyplus/Test/ZoneMRTCalculation_GTest.cpp create mode 100644 src/model/test/ZoneMRTCalculation_GTest.cpp diff --git a/src/energyplus/CMakeLists.txt b/src/energyplus/CMakeLists.txt index 05bec9b68e..70ddec8be4 100644 --- a/src/energyplus/CMakeLists.txt +++ b/src/energyplus/CMakeLists.txt @@ -877,6 +877,7 @@ set(${target_name}_test_src Test/ZoneHVACUnitHeater_GTest.cpp Test/ZoneHVACWaterToAirHeatPump_GTest.cpp Test/ZoneMixing_GTest.cpp + Test/ZoneMRTCalculation_GTest.cpp Test/ZonePropertyUserViewFactorsBySurfaceName_GTest.cpp Test/ZoneVentilationDesignFlowRate_GTest.cpp Test/ZoneVentilationWindandStackOpenArea_GTest.cpp diff --git a/src/energyplus/Test/ZoneMRTCalculation_GTest.cpp b/src/energyplus/Test/ZoneMRTCalculation_GTest.cpp new file mode 100644 index 0000000000..763abba9bc --- /dev/null +++ b/src/energyplus/Test/ZoneMRTCalculation_GTest.cpp @@ -0,0 +1,117 @@ +/*********************************************************************************************************************** +* OpenStudio(R), Copyright (c) Alliance for Energy Innovation, LLC. +* See also https://openstudio.net/license +***********************************************************************************************************************/ + +#include + +#include "EnergyPlusFixture.hpp" + +#include "../ForwardTranslator.hpp" +#include "../ReverseTranslator.hpp" + +#include "../../model/Model.hpp" +#include "../../model/People.hpp" +#include "../../model/PeopleDefinition.hpp" +#include "../../model/Space.hpp" +#include "../../model/ThermalZone.hpp" +#include "../../model/ZoneMRTCalculation.hpp" + +#include "../../utilities/idf/IdfExtensibleGroup.hpp" +#include "../../utilities/idf/WorkspaceExtensibleGroup.hpp" +#include "../../utilities/idf/WorkspaceObject.hpp" + +#include +#include +#include + +using namespace openstudio::energyplus; +using namespace openstudio::model; +using namespace openstudio; + +TEST_F(EnergyPlusFixture, ForwardTranslator_ZoneMRTCalculation) { + Model model; + ThermalZone zone(model); + zone.setName("Thermal Zone 1"); + + Space space(model); + space.setName("Space 1"); + EXPECT_TRUE(space.setThermalZone(zone)); + + PeopleDefinition definition(model); + EXPECT_TRUE(definition.setNumberofPeople(1.0)); + EXPECT_TRUE(definition.setThermalComfortModelType(0, "Fanger")); + + People people1(definition); + people1.setName("People 1"); + EXPECT_TRUE(people1.setSpace(space)); + + People people2(definition); + people2.setName("People 2"); + EXPECT_TRUE(people2.setSpace(space)); + + ZoneMRTCalculation zoneMRTCalculation = zone.getZoneMRTCalculation(); + EXPECT_TRUE(zoneMRTCalculation.addMRTWeightingFactor(people1, 0.25)); + EXPECT_TRUE(zoneMRTCalculation.addMRTWeightingFactor(people2, 0.75)); + + ForwardTranslator ft; + Workspace workspace = ft.translateModel(model); + + std::vector idfZoneMRTCalculations = workspace.getObjectsByType(IddObjectType::ZoneMRTCalculation); + ASSERT_EQ(1u, idfZoneMRTCalculations.size()); + + WorkspaceObject idfZoneMRTCalculation = idfZoneMRTCalculations[0]; + EXPECT_EQ("Thermal Zone 1", idfZoneMRTCalculation.getString(ZoneMRTCalculationFields::ZoneName).get()); + ASSERT_EQ(2u, idfZoneMRTCalculation.numExtensibleGroups()); + + auto group0 = idfZoneMRTCalculation.extensibleGroups()[0].cast(); + EXPECT_EQ("People 1", group0.getString(ZoneMRTCalculationExtensibleFields::PeopleName).get()); + EXPECT_DOUBLE_EQ(0.25, group0.getDouble(ZoneMRTCalculationExtensibleFields::MRTWeightingFactor).get()); + + auto group1 = idfZoneMRTCalculation.extensibleGroups()[1].cast(); + EXPECT_EQ("People 2", group1.getString(ZoneMRTCalculationExtensibleFields::PeopleName).get()); + EXPECT_DOUBLE_EQ(0.75, group1.getDouble(ZoneMRTCalculationExtensibleFields::MRTWeightingFactor).get()); +} + +TEST_F(EnergyPlusFixture, ReverseTranslator_ZoneMRTCalculation) { + Workspace workspace(StrictnessLevel::Minimal, IddFileType::EnergyPlus); + + OptionalWorkspaceObject zone = workspace.addObject(IdfObject(IddObjectType::Zone)); + ASSERT_TRUE(zone); + zone->setName("Thermal Zone 1"); + + OptionalWorkspaceObject people = workspace.addObject(IdfObject(IddObjectType::People)); + ASSERT_TRUE(people); + people->setName("People 1"); + EXPECT_TRUE(people->setPointer(PeopleFields::ZoneorZoneListorSpaceorSpaceListName, zone->handle())); + EXPECT_TRUE(people->setString(PeopleFields::NumberofPeopleCalculationMethod, "People")); + EXPECT_TRUE(people->setDouble(PeopleFields::NumberofPeople, 1.0)); + EXPECT_TRUE(people->setString(PeopleFields::ThermalComfortModel1Type, "Fanger")); + + IdfObject idfZoneMRTCalculation(IddObjectType::ZoneMRTCalculation); + idfZoneMRTCalculation.setString(ZoneMRTCalculationFields::ZoneName, "Thermal Zone 1"); + IdfExtensibleGroup group = idfZoneMRTCalculation.pushExtensibleGroup(); + group.setString(ZoneMRTCalculationExtensibleFields::PeopleName, "People 1"); + group.setDouble(ZoneMRTCalculationExtensibleFields::MRTWeightingFactor, 0.5); + OptionalWorkspaceObject epZoneMRTCalculation = workspace.addObject(idfZoneMRTCalculation); + ASSERT_TRUE(epZoneMRTCalculation); + + ReverseTranslator rt; + Model model = rt.translateWorkspace(workspace); + + std::vector thermalZones = model.getConcreteModelObjects(); + ASSERT_EQ(1u, thermalZones.size()); + ThermalZone thermalZone = thermalZones[0]; + EXPECT_EQ("Thermal Zone 1", thermalZone.nameString()); + + std::vector modelPeople = model.getConcreteModelObjects(); + ASSERT_EQ(1u, modelPeople.size()); + EXPECT_EQ("People 1", modelPeople[0].nameString()); + + ZoneMRTCalculation zoneMRTCalculation = thermalZone.getZoneMRTCalculation(); + EXPECT_EQ(1u, zoneMRTCalculation.numberofMRTWeightingFactors()); + std::vector mrtWeightingFactors = zoneMRTCalculation.mrtWeightingFactors(); + ASSERT_EQ(1u, mrtWeightingFactors.size()); + EXPECT_EQ("People 1", mrtWeightingFactors[0].people().nameString()); + EXPECT_DOUBLE_EQ(0.5, mrtWeightingFactors[0].mrtWeightingFactor()); +} \ No newline at end of file diff --git a/src/model/CMakeLists.txt b/src/model/CMakeLists.txt index e7cdb771e4..c2e428202c 100644 --- a/src/model/CMakeLists.txt +++ b/src/model/CMakeLists.txt @@ -2422,6 +2422,7 @@ set(${target_name}_test_src test/ZoneHVACWaterToAirHeatPump_GTest.cpp test/ZoneHVACIdealLoadsAirSystem_GTest.cpp test/ZoneMixing_GTest.cpp + test/ZoneMRTCalculation_GTest.cpp test/ZonePropertyUserViewFactorsBySurfaceName_GTest.cpp test/ZoneVentilationDesignFlowRate_GTest.cpp test/ZoneVentilationWindandStackOpenArea_GTest.cpp diff --git a/src/model/test/ZoneMRTCalculation_GTest.cpp b/src/model/test/ZoneMRTCalculation_GTest.cpp new file mode 100644 index 0000000000..f6fd3d995e --- /dev/null +++ b/src/model/test/ZoneMRTCalculation_GTest.cpp @@ -0,0 +1,98 @@ +/*********************************************************************************************************************** +* OpenStudio(R), Copyright (c) Alliance for Energy Innovation, LLC. +* See also https://openstudio.net/license +***********************************************************************************************************************/ + +#include + +#include "ModelFixture.hpp" + +#include "../Model.hpp" +#include "../People.hpp" +#include "../PeopleDefinition.hpp" +#include "../Space.hpp" +#include "../ThermalZone.hpp" +#include "../ZoneMRTCalculation.hpp" +#include "../ZoneMRTCalculation_Impl.hpp" + +using namespace openstudio; +using namespace openstudio::model; + +TEST_F(ModelFixture, ZoneMRTCalculation_MRTWeightingFactor) { + Model model; + PeopleDefinition definition(model); + People people(definition); + + EXPECT_NO_THROW(MRTWeightingFactor(people, 0.5)); + EXPECT_THROW(MRTWeightingFactor(people, -0.1), openstudio::Exception); + EXPECT_THROW(MRTWeightingFactor(people, 1.1), openstudio::Exception); +} + +TEST_F(ModelFixture, ZoneMRTCalculation_Uniqueness) { + Model model; + ThermalZone thermalZone(model); + auto size = model.modelObjects().size(); + + ZoneMRTCalculation zoneMRTCalculation1 = thermalZone.getZoneMRTCalculation(); + EXPECT_EQ(thermalZone.handle(), zoneMRTCalculation1.thermalZone().handle()); + EXPECT_EQ(size + 1, model.modelObjects().size()); + + ZoneMRTCalculation zoneMRTCalculation2 = thermalZone.getZoneMRTCalculation(); + EXPECT_EQ(size + 1, model.modelObjects().size()); + EXPECT_EQ(zoneMRTCalculation1, zoneMRTCalculation2); + + EXPECT_EQ(0u, zoneMRTCalculation2.numberofMRTWeightingFactors()); + EXPECT_TRUE(zoneMRTCalculation2.mrtWeightingFactors().empty()); + + EXPECT_THROW((ZoneMRTCalculation(thermalZone)), openstudio::Exception); + EXPECT_EQ(size + 1, model.modelObjects().size()); + + EXPECT_THROW(zoneMRTCalculation1.clone(model), openstudio::Exception); + EXPECT_EQ(size + 1, model.modelObjects().size()); +} + +TEST_F(ModelFixture, ZoneMRTCalculation_AddAndRemoveMRTWeightingFactors) { + Model model; + ThermalZone thermalZone(model); + ZoneMRTCalculation zoneMRTCalculation = thermalZone.getZoneMRTCalculation(); + + Space space(model); + PeopleDefinition definition(model); + definition.setNumberofPeople(1.0); + EXPECT_TRUE(definition.setThermalComfortModelType(0, "Fanger")); + + People people(definition); + people.setName("People 1"); + + EXPECT_FALSE(zoneMRTCalculation.addMRTWeightingFactor(people, 0.5)); + EXPECT_EQ(0u, zoneMRTCalculation.numberofMRTWeightingFactors()); + + space.setThermalZone(thermalZone); + EXPECT_TRUE(people.setSpace(space)); + + EXPECT_TRUE(zoneMRTCalculation.addMRTWeightingFactor(people, 0.5)); + EXPECT_EQ(1u, zoneMRTCalculation.numberofMRTWeightingFactors()); + + boost::optional mrtWeightingFactor = zoneMRTCalculation.getMRTWeightingFactor(0); + ASSERT_TRUE(mrtWeightingFactor); + EXPECT_EQ(people.handle(), mrtWeightingFactor->people().handle()); + EXPECT_DOUBLE_EQ(0.5, mrtWeightingFactor->mrtWeightingFactor()); + + EXPECT_TRUE(zoneMRTCalculation.addMRTWeightingFactor(people, 0.25)); + EXPECT_EQ(1u, zoneMRTCalculation.numberofMRTWeightingFactors()); + mrtWeightingFactor = zoneMRTCalculation.getMRTWeightingFactor(0); + ASSERT_TRUE(mrtWeightingFactor); + EXPECT_DOUBLE_EQ(0.25, mrtWeightingFactor->mrtWeightingFactor()); + + People people2(definition); + people2.setName("People 2"); + EXPECT_TRUE(people2.setSpace(space)); + EXPECT_TRUE(zoneMRTCalculation.addMRTWeightingFactors({MRTWeightingFactor(people2, 0.75)})); + EXPECT_EQ(2u, zoneMRTCalculation.numberofMRTWeightingFactors()); + + zoneMRTCalculation.removeMRTWeightingFactor(0); + EXPECT_EQ(1u, zoneMRTCalculation.numberofMRTWeightingFactors()); + + zoneMRTCalculation.removeAllMRTWeightingFactors(); + EXPECT_EQ(0u, zoneMRTCalculation.numberofMRTWeightingFactors()); +} \ No newline at end of file From 5e92cbc31cd6e0f62687ae49e76ee1496eb80648 Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Tue, 1 Sep 2026 14:35:11 -0700 Subject: [PATCH 05/13] Expand new tests to cover more throw/error scenarios. --- .../Test/ZoneMRTCalculation_GTest.cpp | 45 +++++++++++++++++++ src/model/test/ZoneMRTCalculation_GTest.cpp | 24 ++++++++++ 2 files changed, 69 insertions(+) diff --git a/src/energyplus/Test/ZoneMRTCalculation_GTest.cpp b/src/energyplus/Test/ZoneMRTCalculation_GTest.cpp index 763abba9bc..dfda6e9dff 100644 --- a/src/energyplus/Test/ZoneMRTCalculation_GTest.cpp +++ b/src/energyplus/Test/ZoneMRTCalculation_GTest.cpp @@ -73,6 +73,18 @@ TEST_F(EnergyPlusFixture, ForwardTranslator_ZoneMRTCalculation) { EXPECT_DOUBLE_EQ(0.75, group1.getDouble(ZoneMRTCalculationExtensibleFields::MRTWeightingFactor).get()); } +TEST_F(EnergyPlusFixture, ForwardTranslator_ZoneMRTCalculation_Empty) { + Model model; + ThermalZone zone(model); + zone.setName("Thermal Zone 1"); + zone.getZoneMRTCalculation(); + + ForwardTranslator ft; + Workspace workspace = ft.translateModel(model); + + EXPECT_TRUE(workspace.getObjectsByType(IddObjectType::ZoneMRTCalculation).empty()); +} + TEST_F(EnergyPlusFixture, ReverseTranslator_ZoneMRTCalculation) { Workspace workspace(StrictnessLevel::Minimal, IddFileType::EnergyPlus); @@ -114,4 +126,37 @@ TEST_F(EnergyPlusFixture, ReverseTranslator_ZoneMRTCalculation) { ASSERT_EQ(1u, mrtWeightingFactors.size()); EXPECT_EQ("People 1", mrtWeightingFactors[0].people().nameString()); EXPECT_DOUBLE_EQ(0.5, mrtWeightingFactors[0].mrtWeightingFactor()); +} + +TEST_F(EnergyPlusFixture, ReverseTranslator_ZoneMRTCalculation_InvalidMRTWeightingFactor) { + Workspace workspace(StrictnessLevel::Minimal, IddFileType::EnergyPlus); + + OptionalWorkspaceObject zone = workspace.addObject(IdfObject(IddObjectType::Zone)); + ASSERT_TRUE(zone); + zone->setName("Thermal Zone 1"); + + OptionalWorkspaceObject people = workspace.addObject(IdfObject(IddObjectType::People)); + ASSERT_TRUE(people); + people->setName("People 1"); + EXPECT_TRUE(people->setPointer(PeopleFields::ZoneorZoneListorSpaceorSpaceListName, zone->handle())); + EXPECT_TRUE(people->setString(PeopleFields::NumberofPeopleCalculationMethod, "People")); + EXPECT_TRUE(people->setDouble(PeopleFields::NumberofPeople, 1.0)); + EXPECT_TRUE(people->setString(PeopleFields::ThermalComfortModel1Type, "Fanger")); + + IdfObject idfZoneMRTCalculation(IddObjectType::ZoneMRTCalculation); + idfZoneMRTCalculation.setString(ZoneMRTCalculationFields::ZoneName, "Thermal Zone 1"); + IdfExtensibleGroup group = idfZoneMRTCalculation.pushExtensibleGroup(); + group.setString(ZoneMRTCalculationExtensibleFields::PeopleName, "People 1"); + group.setDouble(ZoneMRTCalculationExtensibleFields::MRTWeightingFactor, 1.1); + OptionalWorkspaceObject epZoneMRTCalculation = workspace.addObject(idfZoneMRTCalculation); + ASSERT_TRUE(epZoneMRTCalculation); + + ReverseTranslator rt; + Model model; + EXPECT_NO_THROW(model = rt.translateWorkspace(workspace)); + + std::vector thermalZones = model.getConcreteModelObjects(); + ASSERT_EQ(1u, thermalZones.size()); + ZoneMRTCalculation zoneMRTCalculation = thermalZones[0].getZoneMRTCalculation(); + EXPECT_EQ(0u, zoneMRTCalculation.numberofMRTWeightingFactors()); } \ No newline at end of file diff --git a/src/model/test/ZoneMRTCalculation_GTest.cpp b/src/model/test/ZoneMRTCalculation_GTest.cpp index f6fd3d995e..bf74015ed6 100644 --- a/src/model/test/ZoneMRTCalculation_GTest.cpp +++ b/src/model/test/ZoneMRTCalculation_GTest.cpp @@ -24,6 +24,8 @@ TEST_F(ModelFixture, ZoneMRTCalculation_MRTWeightingFactor) { People people(definition); EXPECT_NO_THROW(MRTWeightingFactor(people, 0.5)); + EXPECT_NO_THROW(MRTWeightingFactor(people, 0.0)); + EXPECT_NO_THROW(MRTWeightingFactor(people, 1.0)); EXPECT_THROW(MRTWeightingFactor(people, -0.1), openstudio::Exception); EXPECT_THROW(MRTWeightingFactor(people, 1.1), openstudio::Exception); } @@ -51,6 +53,20 @@ TEST_F(ModelFixture, ZoneMRTCalculation_Uniqueness) { EXPECT_EQ(size + 1, model.modelObjects().size()); } +TEST_F(ModelFixture, ZoneMRTCalculation_ThrowingAPIs) { + Model model; + ThermalZone thermalZone(model); + ZoneMRTCalculation zoneMRTCalculation = thermalZone.getZoneMRTCalculation(); + + PeopleDefinition definition(model); + People people(definition); + + EXPECT_THROW(zoneMRTCalculation.addMRTWeightingFactor(people, -0.1), openstudio::Exception); + EXPECT_THROW(zoneMRTCalculation.addMRTWeightingFactor(people, 1.1), openstudio::Exception); + EXPECT_THROW((ZoneMRTCalculation(thermalZone)), openstudio::Exception); + EXPECT_THROW(zoneMRTCalculation.clone(model), openstudio::Exception); +} + TEST_F(ModelFixture, ZoneMRTCalculation_AddAndRemoveMRTWeightingFactors) { Model model; ThermalZone thermalZone(model); @@ -67,6 +83,13 @@ TEST_F(ModelFixture, ZoneMRTCalculation_AddAndRemoveMRTWeightingFactors) { EXPECT_FALSE(zoneMRTCalculation.addMRTWeightingFactor(people, 0.5)); EXPECT_EQ(0u, zoneMRTCalculation.numberofMRTWeightingFactors()); + ThermalZone otherThermalZone(model); + Space otherSpace(model); + EXPECT_TRUE(otherSpace.setThermalZone(otherThermalZone)); + EXPECT_TRUE(people.setSpace(otherSpace)); + EXPECT_FALSE(zoneMRTCalculation.addMRTWeightingFactor(people, 0.5)); + EXPECT_EQ(0u, zoneMRTCalculation.numberofMRTWeightingFactors()); + space.setThermalZone(thermalZone); EXPECT_TRUE(people.setSpace(space)); @@ -77,6 +100,7 @@ TEST_F(ModelFixture, ZoneMRTCalculation_AddAndRemoveMRTWeightingFactors) { ASSERT_TRUE(mrtWeightingFactor); EXPECT_EQ(people.handle(), mrtWeightingFactor->people().handle()); EXPECT_DOUBLE_EQ(0.5, mrtWeightingFactor->mrtWeightingFactor()); + EXPECT_FALSE(zoneMRTCalculation.getMRTWeightingFactor(1)); EXPECT_TRUE(zoneMRTCalculation.addMRTWeightingFactor(people, 0.25)); EXPECT_EQ(1u, zoneMRTCalculation.numberofMRTWeightingFactors()); From 222ac39fdb3d4c30f8e70cf3876d4271394d1ddc Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Tue, 1 Sep 2026 14:44:51 -0700 Subject: [PATCH 06/13] Another pass through model and tests with additions and cleanups. --- src/energyplus/Test/ZoneMRTCalculation_GTest.cpp | 5 ++++- src/model/ModelGeometry.i | 5 +++++ src/model/ModelHVAC.i | 11 +++++++++++ src/model/ZoneMRTCalculation.cpp | 8 ++++++++ src/model/test/ZoneMRTCalculation_GTest.cpp | 8 ++++++++ 5 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/energyplus/Test/ZoneMRTCalculation_GTest.cpp b/src/energyplus/Test/ZoneMRTCalculation_GTest.cpp index dfda6e9dff..de7b66ec4a 100644 --- a/src/energyplus/Test/ZoneMRTCalculation_GTest.cpp +++ b/src/energyplus/Test/ZoneMRTCalculation_GTest.cpp @@ -157,6 +157,9 @@ TEST_F(EnergyPlusFixture, ReverseTranslator_ZoneMRTCalculation_InvalidMRTWeighti std::vector thermalZones = model.getConcreteModelObjects(); ASSERT_EQ(1u, thermalZones.size()); - ZoneMRTCalculation zoneMRTCalculation = thermalZones[0].getZoneMRTCalculation(); + std::vector zoneMRTCalculations = model.getConcreteModelObjects(); + ASSERT_EQ(1u, zoneMRTCalculations.size()); + ZoneMRTCalculation zoneMRTCalculation = zoneMRTCalculations[0]; + EXPECT_EQ(thermalZones[0].handle(), zoneMRTCalculation.thermalZone().handle()); EXPECT_EQ(0u, zoneMRTCalculation.numberofMRTWeightingFactors()); } \ No newline at end of file diff --git a/src/model/ModelGeometry.i b/src/model/ModelGeometry.i index 2207c80b6c..d776068a93 100644 --- a/src/model/ModelGeometry.i +++ b/src/model/ModelGeometry.i @@ -44,6 +44,8 @@ // Ignore this ctor, use of zone.getZonePropertyUserViewFactorsBySurfaceName is preferred anyways (so I won't even reimplement it using partial classes) %ignore openstudio::model::ZonePropertyUserViewFactorsBySurfaceName::ZonePropertyUserViewFactorsBySurfaceName(const ThermalZone& thermalZone); %ignore openstudio::model::ZonePropertyUserViewFactorsBySurfaceName::thermalZone; + %ignore openstudio::model::ZoneMRTCalculation::ZoneMRTCalculation(const ThermalZone& thermalZone); + %ignore openstudio::model::ZoneMRTCalculation::thermalZone; // ignore airflow objects for now, add back in with partial classes in ModelAirflow.i %ignore openstudio::model::Surface::getAirflowNetworkSurface; @@ -243,6 +245,8 @@ MODELOBJECT_TEMPLATES(FoundationKiva); MODELOBJECT_TEMPLATES(SurfacePropertyExposedFoundationPerimeter); MODELOBJECT_TEMPLATES(ViewFactor); // Helper class defined in ZonePropertyUserViewFactorsBySurfaceName MODELOBJECT_TEMPLATES(ZonePropertyUserViewFactorsBySurfaceName); +MODELOBJECT_TEMPLATES(MRTWeightingFactor); // Helper class defined in ZoneMRTCalculation +MODELOBJECT_TEMPLATES(ZoneMRTCalculation); MODELOBJECT_TEMPLATES(ExteriorLoadInstance); MODELOBJECT_TEMPLATES(ExteriorLights); MODELOBJECT_TEMPLATES(ExteriorFuelEquipment); @@ -299,6 +303,7 @@ SWIG_MODELOBJECT(LightingSimulationZone, 1); SWIG_MODELOBJECT(FoundationKiva, 1); SWIG_MODELOBJECT(SurfacePropertyExposedFoundationPerimeter, 1); SWIG_MODELOBJECT(ZonePropertyUserViewFactorsBySurfaceName, 1); +SWIG_MODELOBJECT(ZoneMRTCalculation, 1); SWIG_MODELOBJECT(ExteriorLoadInstance, 0); SWIG_MODELOBJECT(ExteriorLights, 1); SWIG_MODELOBJECT(ExteriorFuelEquipment, 1); diff --git a/src/model/ModelHVAC.i b/src/model/ModelHVAC.i index c8145dd8f2..1c4ce76471 100644 --- a/src/model/ModelHVAC.i +++ b/src/model/ModelHVAC.i @@ -433,6 +433,10 @@ SWIG_MODELOBJECT(LoadingIndex, 1); return zoneProp.thermalZone(); } + openstudio::model::ThermalZone getThermalZone(const openstudio::model::ZoneMRTCalculation& zoneMRTCalculation){ + return zoneMRTCalculation.thermalZone(); + } + // EMS Actuator getter/setter for ThermalZone (reimplemented from ModelCore.i) boost::optional getThermalZoneForEMSActuator(const openstudio::model::EnergyManagementSystemActuator& actuator) { return actuator.thermalZone(); @@ -530,6 +534,13 @@ SWIG_MODELOBJECT(LoadingIndex, 1); } } + public partial class ZoneMRTCalculation : ModelObject { + public ThermalZone thermalZone() + { + return OpenStudio.OpenStudioModelHVAC.getThermalZone(this); + } + } + public partial class EnergyManagementSystemActuator : ModelObject { public OptionalThermalZone thermalZone() { return OpenStudio.OpenStudioModelHVAC.getThermalZoneForEMSActuator(this); diff --git a/src/model/ZoneMRTCalculation.cpp b/src/model/ZoneMRTCalculation.cpp index 31995d8c4a..026d8ab2fc 100644 --- a/src/model/ZoneMRTCalculation.cpp +++ b/src/model/ZoneMRTCalculation.cpp @@ -7,6 +7,8 @@ #include "ZoneMRTCalculation_Impl.hpp" #include "People.hpp" #include "People_Impl.hpp" +#include "PeopleDefinition.hpp" +#include "PeopleDefinition_Impl.hpp" #include "Space.hpp" #include "Space_Impl.hpp" #include "ThermalZone.hpp" @@ -199,6 +201,12 @@ namespace model { return result; } + if (people.peopleDefinition().numThermalComfortModelTypes() == 0) { + LOG(Error, "Cannot add " << people.briefDescription() << " to " << briefDescription() + << " because it does not select at least one Thermal Comfort Model Type."); + return result; + } + boost::optional existingIndex = mrtWeightingFactorIndex(mrtWeightingFactor); if (existingIndex) { boost::optional existingMRTWeightingFactor = getMRTWeightingFactor(existingIndex.get()); diff --git a/src/model/test/ZoneMRTCalculation_GTest.cpp b/src/model/test/ZoneMRTCalculation_GTest.cpp index bf74015ed6..b0918ca4a0 100644 --- a/src/model/test/ZoneMRTCalculation_GTest.cpp +++ b/src/model/test/ZoneMRTCalculation_GTest.cpp @@ -93,6 +93,14 @@ TEST_F(ModelFixture, ZoneMRTCalculation_AddAndRemoveMRTWeightingFactors) { space.setThermalZone(thermalZone); EXPECT_TRUE(people.setSpace(space)); + PeopleDefinition noComfortDefinition(model); + noComfortDefinition.setNumberofPeople(1.0); + People peopleWithoutThermalComfortModel(noComfortDefinition); + peopleWithoutThermalComfortModel.setName("People Without Thermal Comfort Model"); + EXPECT_TRUE(peopleWithoutThermalComfortModel.setSpace(space)); + EXPECT_FALSE(zoneMRTCalculation.addMRTWeightingFactor(peopleWithoutThermalComfortModel, 0.5)); + EXPECT_EQ(0u, zoneMRTCalculation.numberofMRTWeightingFactors()); + EXPECT_TRUE(zoneMRTCalculation.addMRTWeightingFactor(people, 0.5)); EXPECT_EQ(1u, zoneMRTCalculation.numberofMRTWeightingFactors()); From 0f9a9389d28ae4692e7f36811209c41c88a0db4b Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Tue, 1 Sep 2026 15:17:25 -0700 Subject: [PATCH 07/13] Address updates in ft and test, and model cpp. --- src/energyplus/ForwardTranslator.cpp | 1 + src/energyplus/Test/ZoneMRTCalculation_GTest.cpp | 4 ++++ src/model/Model.cpp | 1 + 3 files changed, 6 insertions(+) diff --git a/src/energyplus/ForwardTranslator.cpp b/src/energyplus/ForwardTranslator.cpp index 6a499239da..1149dc3c93 100644 --- a/src/energyplus/ForwardTranslator.cpp +++ b/src/energyplus/ForwardTranslator.cpp @@ -3509,6 +3509,7 @@ namespace energyplus { IddObjectType::OS_ShadingSurfaceGroup, IddObjectType::OS_ShadingSurface, + IddObjectType::OS_ZoneMRTCalculation, IddObjectType::OS_ZoneProperty_UserViewFactors_BySurfaceName, IddObjectType::OS_Daylighting_Control, diff --git a/src/energyplus/Test/ZoneMRTCalculation_GTest.cpp b/src/energyplus/Test/ZoneMRTCalculation_GTest.cpp index de7b66ec4a..8b671220b8 100644 --- a/src/energyplus/Test/ZoneMRTCalculation_GTest.cpp +++ b/src/energyplus/Test/ZoneMRTCalculation_GTest.cpp @@ -12,13 +12,17 @@ #include "../../model/Model.hpp" #include "../../model/People.hpp" +#include "../../model/People_Impl.hpp" #include "../../model/PeopleDefinition.hpp" #include "../../model/Space.hpp" #include "../../model/ThermalZone.hpp" +#include "../../model/ThermalZone_Impl.hpp" #include "../../model/ZoneMRTCalculation.hpp" +#include "../../model/ZoneMRTCalculation_Impl.hpp" #include "../../utilities/idf/IdfExtensibleGroup.hpp" #include "../../utilities/idf/WorkspaceExtensibleGroup.hpp" +#include "../../utilities/idf/WorkspaceObject_Impl.hpp" #include "../../utilities/idf/WorkspaceObject.hpp" #include diff --git a/src/model/Model.cpp b/src/model/Model.cpp index cad6eae944..9e4ff763d4 100644 --- a/src/model/Model.cpp +++ b/src/model/Model.cpp @@ -5064,6 +5064,7 @@ namespace model { REGISTER_COPYCONSTRUCTORS(ZoneHVACEnergyRecoveryVentilator); REGISTER_COPYCONSTRUCTORS(ZoneHVACUnitVentilator); REGISTER_COPYCONSTRUCTORS(ZoneMixing); + REGISTER_COPYCONSTRUCTORS(ZoneMRTCalculation); REGISTER_COPYCONSTRUCTORS(ZonePropertyUserViewFactorsBySurfaceName); REGISTER_COPYCONSTRUCTORS(ZoneVentilationDesignFlowRate); REGISTER_COPYCONSTRUCTORS(ZoneVentilationWindandStackOpenArea); From d56eff7ee11194f3f15c2776bbf738d30a6cd79a Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Tue, 1 Sep 2026 15:52:17 -0700 Subject: [PATCH 08/13] Updates related to stale people references, nan, weighting factors sum, per review. --- .../ForwardTranslateZoneMRTCalculation.cpp | 62 ++++++++++++++++--- .../Test/ZoneMRTCalculation_GTest.cpp | 39 ++++++++++++ src/model/ZoneMRTCalculation.cpp | 29 ++++++++- src/model/test/ZoneMRTCalculation_GTest.cpp | 20 ++++++ 4 files changed, 141 insertions(+), 9 deletions(-) diff --git a/src/energyplus/ForwardTranslator/ForwardTranslateZoneMRTCalculation.cpp b/src/energyplus/ForwardTranslator/ForwardTranslateZoneMRTCalculation.cpp index 44ac384473..91c115f884 100644 --- a/src/energyplus/ForwardTranslator/ForwardTranslateZoneMRTCalculation.cpp +++ b/src/energyplus/ForwardTranslator/ForwardTranslateZoneMRTCalculation.cpp @@ -8,6 +8,10 @@ #include "../../model/Model.hpp" #include "../../model/People.hpp" #include "../../model/People_Impl.hpp" +#include "../../model/PeopleDefinition.hpp" +#include "../../model/PeopleDefinition_Impl.hpp" +#include "../../model/Space.hpp" +#include "../../model/Space_Impl.hpp" #include "../../model/ThermalZone.hpp" #include "../../model/ThermalZone_Impl.hpp" #include "../../model/ZoneMRTCalculation.hpp" @@ -39,24 +43,68 @@ namespace energyplus { return boost::none; } - IdfObject idfObject(openstudio::IddObjectType::ZoneMRTCalculation); - m_idfObjects.push_back(idfObject); - - idfObject.setString(ZoneMRTCalculationFields::ZoneName, _zone->name().get()); + std::vector> translatedMRTWeightingFactors; + double sum = 0.0; for (const MRTWeightingFactor& mrtWeightingFactor : mrtWeightingFactors) { People people = mrtWeightingFactor.people(); + + boost::optional space = people.space(); + boost::optional thermalZone; + if (space) { + thermalZone = space->thermalZone(); + } + + if (!thermalZone || (thermalZone->handle() != zone.handle())) { + LOG(Error, "Could not translate an MRTWeightingFactor group for " << modelObject.briefDescription() << " because " + << people.briefDescription() + << " is not assigned to the ZoneMRTCalculation ThermalZone."); + continue; + } + + if (people.peopleDefinition().numThermalComfortModelTypes() == 0) { + LOG(Error, "Could not translate an MRTWeightingFactor group for " << modelObject.briefDescription() << " because " + << people.briefDescription() + << " does not select at least one Thermal Comfort Model Type."); + continue; + } + boost::optional _people = translateAndMapModelObject(people); if (_people) { - auto eg = idfObject.pushExtensibleGroup(); - eg.setString(ZoneMRTCalculationExtensibleFields::PeopleName, _people->name().get()); - eg.setDouble(ZoneMRTCalculationExtensibleFields::MRTWeightingFactor, mrtWeightingFactor.mrtWeightingFactor()); + double value = mrtWeightingFactor.mrtWeightingFactor(); + sum += value; + if (sum > 1.0) { + LOG(Error, "Could not translate an MRTWeightingFactor group for " << modelObject.briefDescription() + << " because the MRT Weighting Factors would sum to " << sum + << ", which is greater than 1."); + sum -= value; + continue; + } + + translatedMRTWeightingFactors.emplace_back(_people->name().get(), value); } else { LOG(Error, "Could not translate an MRTWeightingFactor group for " << modelObject.briefDescription() << ". Continuing with the rest."); } } + if (translatedMRTWeightingFactors.empty()) { + LOG(Error, "ZoneMRTCalculation for zone '" << zone.nameString() + << "' doesn't have at least one valid MRT weighting factor, it will not be translated."); + return boost::none; + } + + IdfObject idfObject(openstudio::IddObjectType::ZoneMRTCalculation); + m_idfObjects.push_back(idfObject); + + idfObject.setString(ZoneMRTCalculationFields::ZoneName, _zone->name().get()); + + for (const auto& translatedMRTWeightingFactor : translatedMRTWeightingFactors) { + auto eg = idfObject.pushExtensibleGroup(); + eg.setString(ZoneMRTCalculationExtensibleFields::PeopleName, translatedMRTWeightingFactor.first); + eg.setDouble(ZoneMRTCalculationExtensibleFields::MRTWeightingFactor, translatedMRTWeightingFactor.second); + } + return idfObject; } diff --git a/src/energyplus/Test/ZoneMRTCalculation_GTest.cpp b/src/energyplus/Test/ZoneMRTCalculation_GTest.cpp index 8b671220b8..bf3d7452f2 100644 --- a/src/energyplus/Test/ZoneMRTCalculation_GTest.cpp +++ b/src/energyplus/Test/ZoneMRTCalculation_GTest.cpp @@ -89,6 +89,45 @@ TEST_F(EnergyPlusFixture, ForwardTranslator_ZoneMRTCalculation_Empty) { EXPECT_TRUE(workspace.getObjectsByType(IddObjectType::ZoneMRTCalculation).empty()); } +TEST_F(EnergyPlusFixture, ForwardTranslator_ZoneMRTCalculation_StalePeopleReferences) { + Model model; + ThermalZone zone(model); + zone.setName("Thermal Zone 1"); + + Space space(model); + space.setName("Space 1"); + EXPECT_TRUE(space.setThermalZone(zone)); + + PeopleDefinition definition1(model); + EXPECT_TRUE(definition1.setNumberofPeople(1.0)); + EXPECT_TRUE(definition1.setThermalComfortModelType(0, "Fanger")); + People people1(definition1); + people1.setName("People 1"); + EXPECT_TRUE(people1.setSpace(space)); + + PeopleDefinition definition2(model); + EXPECT_TRUE(definition2.setNumberofPeople(1.0)); + EXPECT_TRUE(definition2.setThermalComfortModelType(0, "Fanger")); + People people2(definition2); + people2.setName("People 2"); + EXPECT_TRUE(people2.setSpace(space)); + + ZoneMRTCalculation zoneMRTCalculation = zone.getZoneMRTCalculation(); + EXPECT_TRUE(zoneMRTCalculation.addMRTWeightingFactor(people1, 0.25)); + EXPECT_TRUE(zoneMRTCalculation.addMRTWeightingFactor(people2, 0.50)); + + ThermalZone otherZone(model); + Space otherSpace(model); + EXPECT_TRUE(otherSpace.setThermalZone(otherZone)); + EXPECT_TRUE(people1.setSpace(otherSpace)); + EXPECT_TRUE(definition2.eraseThermalComfortModelType(0)); + + ForwardTranslator ft; + Workspace workspace = ft.translateModel(model); + + EXPECT_TRUE(workspace.getObjectsByType(IddObjectType::ZoneMRTCalculation).empty()); +} + TEST_F(EnergyPlusFixture, ReverseTranslator_ZoneMRTCalculation) { Workspace workspace(StrictnessLevel::Minimal, IddFileType::EnergyPlus); diff --git a/src/model/ZoneMRTCalculation.cpp b/src/model/ZoneMRTCalculation.cpp index 026d8ab2fc..b144f28e04 100644 --- a/src/model/ZoneMRTCalculation.cpp +++ b/src/model/ZoneMRTCalculation.cpp @@ -34,7 +34,7 @@ namespace model { *****************************************************************************************************************************************************/ MRTWeightingFactor::MRTWeightingFactor(const People& people, double mrtWeightingFactor) : m_people(people), m_mrtWeightingFactor(mrtWeightingFactor) { - if ((m_mrtWeightingFactor < 0.0) || (m_mrtWeightingFactor > 1.0)) { + if (!((m_mrtWeightingFactor >= 0.0) && (m_mrtWeightingFactor <= 1.0))) { LOG_AND_THROW("Unable to create MRT weighting factor, factor of " << m_mrtWeightingFactor << " is not between 0 and 1"); } } @@ -215,6 +215,28 @@ namespace model { << existingMRTWeightingFactor.get() << " to " << mrtWeightingFactor << "."); } + double sum = mrtWeightingFactor.mrtWeightingFactor(); + for (unsigned i = 0; i < numberofMRTWeightingFactors(); ++i) { + if (existingIndex && (i == existingIndex.get())) { + continue; + } + + boost::optional existingValue = getMRTWeightingFactorValue(i); + if (!existingValue || !((*existingValue >= 0.0) && (*existingValue <= 1.0))) { + LOG(Error, + "Cannot add " << people.briefDescription() << " to " << briefDescription() << " because an existing MRT Weighting Factor is invalid."); + return result; + } + + sum += existingValue.get(); + } + + if (sum > 1.0) { + LOG(Error, "Cannot add " << people.briefDescription() << " to " << briefDescription() << " because the MRT Weighting Factors would sum to " + << sum << ", which is greater than 1."); + return result; + } + std::vector temp; ModelExtensibleGroup group = (existingIndex ? getExtensibleGroup(existingIndex.get()).cast() : pushExtensibleGroup(temp, false).cast()); @@ -230,7 +252,9 @@ namespace model { if (peopleSet && factorSet) { result = true; } else { - getObject().eraseExtensibleGroup(group.groupIndex()); + if (!existingIndex) { + getObject().eraseExtensibleGroup(group.groupIndex()); + } } return result; @@ -247,6 +271,7 @@ namespace model { for (const MRTWeightingFactor& mrtWeightingFactor : mrtWeightingFactors) { bool thisResult = addMRTWeightingFactor(mrtWeightingFactor); if (!thisResult) { + result = false; LOG(Error, "Could not add mrtWeightingFactor " << mrtWeightingFactor << " to " << briefDescription() << ". Continuing with others."); } } diff --git a/src/model/test/ZoneMRTCalculation_GTest.cpp b/src/model/test/ZoneMRTCalculation_GTest.cpp index b0918ca4a0..e7b578c930 100644 --- a/src/model/test/ZoneMRTCalculation_GTest.cpp +++ b/src/model/test/ZoneMRTCalculation_GTest.cpp @@ -15,6 +15,8 @@ #include "../ZoneMRTCalculation.hpp" #include "../ZoneMRTCalculation_Impl.hpp" +#include + using namespace openstudio; using namespace openstudio::model; @@ -28,6 +30,7 @@ TEST_F(ModelFixture, ZoneMRTCalculation_MRTWeightingFactor) { EXPECT_NO_THROW(MRTWeightingFactor(people, 1.0)); EXPECT_THROW(MRTWeightingFactor(people, -0.1), openstudio::Exception); EXPECT_THROW(MRTWeightingFactor(people, 1.1), openstudio::Exception); + EXPECT_THROW(MRTWeightingFactor(people, std::numeric_limits::quiet_NaN()), openstudio::Exception); } TEST_F(ModelFixture, ZoneMRTCalculation_Uniqueness) { @@ -122,6 +125,23 @@ TEST_F(ModelFixture, ZoneMRTCalculation_AddAndRemoveMRTWeightingFactors) { EXPECT_TRUE(zoneMRTCalculation.addMRTWeightingFactors({MRTWeightingFactor(people2, 0.75)})); EXPECT_EQ(2u, zoneMRTCalculation.numberofMRTWeightingFactors()); + People people3(definition); + people3.setName("People 3"); + EXPECT_TRUE(people3.setSpace(space)); + EXPECT_FALSE(zoneMRTCalculation.addMRTWeightingFactor(people3, 0.01)); + EXPECT_EQ(2u, zoneMRTCalculation.numberofMRTWeightingFactors()); + + EXPECT_TRUE(zoneMRTCalculation.addMRTWeightingFactor(people, 0.20)); + EXPECT_EQ(2u, zoneMRTCalculation.numberofMRTWeightingFactors()); + mrtWeightingFactor = zoneMRTCalculation.getMRTWeightingFactor(0); + ASSERT_TRUE(mrtWeightingFactor); + EXPECT_DOUBLE_EQ(0.20, mrtWeightingFactor->mrtWeightingFactor()); + + zoneMRTCalculation.removeAllMRTWeightingFactors(); + EXPECT_TRUE(zoneMRTCalculation.addMRTWeightingFactor(people, 0.5)); + EXPECT_FALSE(zoneMRTCalculation.addMRTWeightingFactors({MRTWeightingFactor(people2, 0.25), MRTWeightingFactor(people3, 0.5)})); + EXPECT_EQ(2u, zoneMRTCalculation.numberofMRTWeightingFactors()); + zoneMRTCalculation.removeMRTWeightingFactor(0); EXPECT_EQ(1u, zoneMRTCalculation.numberofMRTWeightingFactors()); From c5bc900865d54e7417ac504ce7b8325c2d1fb289 Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Tue, 1 Sep 2026 15:58:14 -0700 Subject: [PATCH 09/13] Override people remove so we can modify zone mrt extensible groups. --- src/model/People.cpp | 15 +++++++++++++++ src/model/People_Impl.hpp | 2 ++ src/model/test/ZoneMRTCalculation_GTest.cpp | 8 +++++++- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/model/People.cpp b/src/model/People.cpp index ba31b91fa4..8f6b5dbf76 100644 --- a/src/model/People.cpp +++ b/src/model/People.cpp @@ -17,6 +17,8 @@ #include "Space_Impl.hpp" #include "SpaceType.hpp" #include "SpaceType_Impl.hpp" +#include "ZoneMRTCalculation.hpp" +#include "ZoneMRTCalculation_Impl.hpp" #include "DefaultScheduleSet.hpp" #include "DefaultScheduleSet_Impl.hpp" #include "LifeCycleCost.hpp" @@ -90,6 +92,19 @@ namespace model { return People::iddObjectType(); } + std::vector People_Impl::remove() { + People people = getObject(); + std::vector zoneMRTCalculations = people.getModelObjectSources(ZoneMRTCalculation::iddObjectType()); + + for (ZoneMRTCalculation& zoneMRTCalculation : zoneMRTCalculations) { + while (boost::optional index = zoneMRTCalculation.mrtWeightingFactorIndex(MRTWeightingFactor(people, 0.0))) { + zoneMRTCalculation.removeMRTWeightingFactor(index.get()); + } + } + + return SpaceLoadInstance_Impl::remove(); + } + std::vector People_Impl::getScheduleTypeKeys(const Schedule& schedule) const { std::vector result; UnsignedVector fieldIndices = getSourceIndices(schedule.handle()); diff --git a/src/model/People_Impl.hpp b/src/model/People_Impl.hpp index f5eb8eb4ff..809869e76c 100644 --- a/src/model/People_Impl.hpp +++ b/src/model/People_Impl.hpp @@ -39,6 +39,8 @@ namespace model { virtual const std::vector& outputVariableNames() const override; + virtual std::vector remove() override; + virtual IddObjectType iddObjectType() const override; virtual std::vector getScheduleTypeKeys(const Schedule& schedule) const override; diff --git a/src/model/test/ZoneMRTCalculation_GTest.cpp b/src/model/test/ZoneMRTCalculation_GTest.cpp index e7b578c930..29022f83cb 100644 --- a/src/model/test/ZoneMRTCalculation_GTest.cpp +++ b/src/model/test/ZoneMRTCalculation_GTest.cpp @@ -137,9 +137,15 @@ TEST_F(ModelFixture, ZoneMRTCalculation_AddAndRemoveMRTWeightingFactors) { ASSERT_TRUE(mrtWeightingFactor); EXPECT_DOUBLE_EQ(0.20, mrtWeightingFactor->mrtWeightingFactor()); + people2.remove(); + EXPECT_EQ(1u, zoneMRTCalculation.numberofMRTWeightingFactors()); + EXPECT_TRUE(zoneMRTCalculation.addMRTWeightingFactor(people3, 0.80)); + EXPECT_EQ(2u, zoneMRTCalculation.numberofMRTWeightingFactors()); + zoneMRTCalculation.removeAllMRTWeightingFactors(); EXPECT_TRUE(zoneMRTCalculation.addMRTWeightingFactor(people, 0.5)); - EXPECT_FALSE(zoneMRTCalculation.addMRTWeightingFactors({MRTWeightingFactor(people2, 0.25), MRTWeightingFactor(people3, 0.5)})); + EXPECT_FALSE( + zoneMRTCalculation.addMRTWeightingFactors({MRTWeightingFactor(peopleWithoutThermalComfortModel, 0.25), MRTWeightingFactor(people3, 0.5)})); EXPECT_EQ(2u, zoneMRTCalculation.numberofMRTWeightingFactors()); zoneMRTCalculation.removeMRTWeightingFactor(0); From 445c0f65229ee3e26c5f0151ba9d6ae7f8744eb8 Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Tue, 1 Sep 2026 21:14:44 -0700 Subject: [PATCH 10/13] Add optional proactive extensible group removal for People, update mrtWeightingFactorIndex to use just People, update model tests. --- src/model/People.cpp | 64 +++++++++++++++++- src/model/People_Impl.hpp | 4 ++ src/model/ZoneMRTCalculation.cpp | 10 +-- src/model/ZoneMRTCalculation.hpp | 2 +- src/model/ZoneMRTCalculation_Impl.hpp | 2 +- ...nePropertyUserViewFactorsBySurfaceName.hpp | 2 +- src/model/test/ZoneMRTCalculation_GTest.cpp | 66 ++++++++++++++++++- 7 files changed, 138 insertions(+), 12 deletions(-) diff --git a/src/model/People.cpp b/src/model/People.cpp index 8f6b5dbf76..49814157a1 100644 --- a/src/model/People.cpp +++ b/src/model/People.cpp @@ -17,6 +17,8 @@ #include "Space_Impl.hpp" #include "SpaceType.hpp" #include "SpaceType_Impl.hpp" +#include "ThermalZone.hpp" +#include "ThermalZone_Impl.hpp" #include "ZoneMRTCalculation.hpp" #include "ZoneMRTCalculation_Impl.hpp" #include "DefaultScheduleSet.hpp" @@ -93,17 +95,73 @@ namespace model { } std::vector People_Impl::remove() { - People people = getObject(); + removeZoneMRTCalculationReferences(); + return SpaceLoadInstance_Impl::remove(); + } + + void People_Impl::removeZoneMRTCalculationReferences() { + People people = getObject().cast(); std::vector zoneMRTCalculations = people.getModelObjectSources(ZoneMRTCalculation::iddObjectType()); for (ZoneMRTCalculation& zoneMRTCalculation : zoneMRTCalculations) { - while (boost::optional index = zoneMRTCalculation.mrtWeightingFactorIndex(MRTWeightingFactor(people, 0.0))) { + while (boost::optional index = zoneMRTCalculation.mrtWeightingFactorIndex(people)) { zoneMRTCalculation.removeMRTWeightingFactor(index.get()); } } + } - return SpaceLoadInstance_Impl::remove(); + // 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. + /* + bool People_Impl::setSpace(const Space& space) { + bool result = SpaceLoadInstance_Impl::setSpace(space); + if (result) { + removeInvalidZoneMRTCalculationReferences(); + } + return result; + } + + void People_Impl::resetSpace() { + SpaceLoadInstance_Impl::resetSpace(); + removeInvalidZoneMRTCalculationReferences(); + } + + bool People_Impl::setSpaceType(const SpaceType& spaceType) { + bool result = SpaceLoadInstance_Impl::setSpaceType(spaceType); + if (result) { + removeInvalidZoneMRTCalculationReferences(); + } + return result; + } + + void People_Impl::resetSpaceType() { + SpaceLoadInstance_Impl::resetSpaceType(); + removeInvalidZoneMRTCalculationReferences(); + } + + void People_Impl::removeInvalidZoneMRTCalculationReferences() { + People people = getObject().cast(); + std::vector zoneMRTCalculations = people.getModelObjectSources(ZoneMRTCalculation::iddObjectType()); + + for (ZoneMRTCalculation& zoneMRTCalculation : zoneMRTCalculations) { + bool valid = false; + boost::optional space = people.space(); + if (space && (people.peopleDefinition().numThermalComfortModelTypes() > 0)) { + boost::optional thermalZone = space->thermalZone(); + valid = thermalZone && (thermalZone->handle() == zoneMRTCalculation.thermalZone().handle()); + } + + if (!valid) { + while (boost::optional index = zoneMRTCalculation.mrtWeightingFactorIndex(people)) { + zoneMRTCalculation.removeMRTWeightingFactor(index.get()); + } + } + } } + */ std::vector People_Impl::getScheduleTypeKeys(const Schedule& schedule) const { std::vector result; diff --git a/src/model/People_Impl.hpp b/src/model/People_Impl.hpp index 809869e76c..1c2bbec154 100644 --- a/src/model/People_Impl.hpp +++ b/src/model/People_Impl.hpp @@ -15,6 +15,8 @@ namespace model { class Schedule; class PeopleDefinition; class People; + class Space; + class SpaceType; namespace detail { @@ -199,6 +201,8 @@ namespace model { private: REGISTER_LOGGER("openstudio.model.People"); + void removeZoneMRTCalculationReferences(); + boost::optional peopleDefinitionAsModelObject() const; boost::optional numberofPeopleScheduleAsModelObject() const; boost::optional activityLevelScheduleAsModelObject() const; diff --git a/src/model/ZoneMRTCalculation.cpp b/src/model/ZoneMRTCalculation.cpp index b144f28e04..9ccb9060f9 100644 --- a/src/model/ZoneMRTCalculation.cpp +++ b/src/model/ZoneMRTCalculation.cpp @@ -97,11 +97,11 @@ namespace model { return numExtensibleGroups(); } - boost::optional ZoneMRTCalculation_Impl::mrtWeightingFactorIndex(const MRTWeightingFactor& mrtWeightingFactor) const { + boost::optional ZoneMRTCalculation_Impl::mrtWeightingFactorIndex(const People& people) const { boost::optional result; auto egs = castVector(extensibleGroups()); - auto peopleHandle = toString(mrtWeightingFactor.people().handle()); + auto peopleHandle = toString(people.handle()); auto it = std::find_if(egs.begin(), egs.end(), [&](const WorkspaceExtensibleGroup& eg) { return eg.getField(OS_ZoneMRTCalculationExtensibleFields::PeopleName).get() == peopleHandle; }); @@ -207,7 +207,7 @@ namespace model { return result; } - boost::optional existingIndex = mrtWeightingFactorIndex(mrtWeightingFactor); + boost::optional existingIndex = mrtWeightingFactorIndex(people); if (existingIndex) { boost::optional existingMRTWeightingFactor = getMRTWeightingFactor(existingIndex.get()); OS_ASSERT(existingMRTWeightingFactor); @@ -324,8 +324,8 @@ namespace model { return getImpl()->numberofMRTWeightingFactors(); } - boost::optional ZoneMRTCalculation::mrtWeightingFactorIndex(const MRTWeightingFactor& mrtWeightingFactor) const { - return getImpl()->mrtWeightingFactorIndex(mrtWeightingFactor); + boost::optional ZoneMRTCalculation::mrtWeightingFactorIndex(const People& people) const { + return getImpl()->mrtWeightingFactorIndex(people); } boost::optional ZoneMRTCalculation::getMRTWeightingFactor(unsigned groupIndex) const { diff --git a/src/model/ZoneMRTCalculation.hpp b/src/model/ZoneMRTCalculation.hpp index a3a272b1c4..e98ca5b146 100644 --- a/src/model/ZoneMRTCalculation.hpp +++ b/src/model/ZoneMRTCalculation.hpp @@ -74,7 +74,7 @@ namespace model { unsigned int numberofMRTWeightingFactors() const; - boost::optional mrtWeightingFactorIndex(const MRTWeightingFactor& mrtWeightingFactor) const; + boost::optional mrtWeightingFactorIndex(const People& people) const; boost::optional getMRTWeightingFactor(unsigned groupIndex) const; diff --git a/src/model/ZoneMRTCalculation_Impl.hpp b/src/model/ZoneMRTCalculation_Impl.hpp index a7559d2141..56d241d42b 100644 --- a/src/model/ZoneMRTCalculation_Impl.hpp +++ b/src/model/ZoneMRTCalculation_Impl.hpp @@ -54,7 +54,7 @@ namespace model { unsigned int numberofMRTWeightingFactors() const; - boost::optional mrtWeightingFactorIndex(const MRTWeightingFactor& mrtWeightingFactor) const; + boost::optional mrtWeightingFactorIndex(const People& people) const; boost::optional getMRTWeightingFactor(unsigned groupIndex) const; diff --git a/src/model/ZonePropertyUserViewFactorsBySurfaceName.hpp b/src/model/ZonePropertyUserViewFactorsBySurfaceName.hpp index 8e55749776..b6c0402193 100644 --- a/src/model/ZonePropertyUserViewFactorsBySurfaceName.hpp +++ b/src/model/ZonePropertyUserViewFactorsBySurfaceName.hpp @@ -61,7 +61,7 @@ class InternalMass; */ //@{ /* - * This constructor shouldn't be used, ThermalZone::getZonePropertyUserViewFactorsBySurfaceName should be prefered + * This constructor shouldn't be used, ThermalZone::getZonePropertyUserViewFactorsBySurfaceName should be preferred * as it will either instantiate a new object or return the existing one. * A ThermalZone should not have more than one ZonePropertyUserViewFactorsBySurfaceName. If you try to instantiate a second one for a given * ThermalZone this constructor will throw diff --git a/src/model/test/ZoneMRTCalculation_GTest.cpp b/src/model/test/ZoneMRTCalculation_GTest.cpp index 29022f83cb..c30461fca1 100644 --- a/src/model/test/ZoneMRTCalculation_GTest.cpp +++ b/src/model/test/ZoneMRTCalculation_GTest.cpp @@ -11,6 +11,7 @@ #include "../People.hpp" #include "../PeopleDefinition.hpp" #include "../Space.hpp" +#include "../SpaceType.hpp" #include "../ThermalZone.hpp" #include "../ZoneMRTCalculation.hpp" #include "../ZoneMRTCalculation_Impl.hpp" @@ -25,9 +26,12 @@ TEST_F(ModelFixture, ZoneMRTCalculation_MRTWeightingFactor) { PeopleDefinition definition(model); People people(definition); + // Individual factors may use the full IDD range, including both endpoints. EXPECT_NO_THROW(MRTWeightingFactor(people, 0.5)); EXPECT_NO_THROW(MRTWeightingFactor(people, 0.0)); EXPECT_NO_THROW(MRTWeightingFactor(people, 1.0)); + + // Invalid and non-finite values are rejected by the helper wrapper before a model object is mutated. EXPECT_THROW(MRTWeightingFactor(people, -0.1), openstudio::Exception); EXPECT_THROW(MRTWeightingFactor(people, 1.1), openstudio::Exception); EXPECT_THROW(MRTWeightingFactor(people, std::numeric_limits::quiet_NaN()), openstudio::Exception); @@ -38,20 +42,25 @@ TEST_F(ModelFixture, ZoneMRTCalculation_Uniqueness) { ThermalZone thermalZone(model); auto size = model.modelObjects().size(); + // The preferred ThermalZone getter lazily creates the one allowed ZoneMRTCalculation object. ZoneMRTCalculation zoneMRTCalculation1 = thermalZone.getZoneMRTCalculation(); EXPECT_EQ(thermalZone.handle(), zoneMRTCalculation1.thermalZone().handle()); EXPECT_EQ(size + 1, model.modelObjects().size()); + // Calling the getter again should return the existing object, not create another one. ZoneMRTCalculation zoneMRTCalculation2 = thermalZone.getZoneMRTCalculation(); EXPECT_EQ(size + 1, model.modelObjects().size()); EXPECT_EQ(zoneMRTCalculation1, zoneMRTCalculation2); + // A newly created object starts with no extensible People/weight records. EXPECT_EQ(0u, zoneMRTCalculation2.numberofMRTWeightingFactors()); EXPECT_TRUE(zoneMRTCalculation2.mrtWeightingFactors().empty()); + // 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()); + // Cloning is disallowed so cloned objects cannot lose the required one-per-zone relationship. EXPECT_THROW(zoneMRTCalculation1.clone(model), openstudio::Exception); EXPECT_EQ(size + 1, model.modelObjects().size()); } @@ -64,8 +73,11 @@ TEST_F(ModelFixture, ZoneMRTCalculation_ThrowingAPIs) { PeopleDefinition definition(model); People people(definition); + // The overload taking People and double still constructs an MRTWeightingFactor internally, so bad values throw. EXPECT_THROW(zoneMRTCalculation.addMRTWeightingFactor(people, -0.1), openstudio::Exception); EXPECT_THROW(zoneMRTCalculation.addMRTWeightingFactor(people, 1.1), openstudio::Exception); + + // The explicit throwing paths remain covered here as a compact smoke test. EXPECT_THROW((ZoneMRTCalculation(thermalZone)), openstudio::Exception); EXPECT_THROW(zoneMRTCalculation.clone(model), openstudio::Exception); } @@ -83,9 +95,11 @@ TEST_F(ModelFixture, ZoneMRTCalculation_AddAndRemoveMRTWeightingFactors) { People people(definition); people.setName("People 1"); + // People must be assigned directly to a Space before they can be referenced. EXPECT_FALSE(zoneMRTCalculation.addMRTWeightingFactor(people, 0.5)); EXPECT_EQ(0u, zoneMRTCalculation.numberofMRTWeightingFactors()); + // The Space must also belong to the same ThermalZone as the ZoneMRTCalculation object. ThermalZone otherThermalZone(model); Space otherSpace(model); EXPECT_TRUE(otherSpace.setThermalZone(otherThermalZone)); @@ -96,6 +110,7 @@ TEST_F(ModelFixture, ZoneMRTCalculation_AddAndRemoveMRTWeightingFactors) { space.setThermalZone(thermalZone); EXPECT_TRUE(people.setSpace(space)); + // The PeopleDefinition must select at least one Thermal Comfort Model Type, matching the IDD contract. PeopleDefinition noComfortDefinition(model); noComfortDefinition.setNumberofPeople(1.0); People peopleWithoutThermalComfortModel(noComfortDefinition); @@ -104,50 +119,99 @@ TEST_F(ModelFixture, ZoneMRTCalculation_AddAndRemoveMRTWeightingFactors) { EXPECT_FALSE(zoneMRTCalculation.addMRTWeightingFactor(peopleWithoutThermalComfortModel, 0.5)); EXPECT_EQ(0u, zoneMRTCalculation.numberofMRTWeightingFactors()); + // A valid People/weight pair creates the first extensible group and can be found by People identity. EXPECT_TRUE(zoneMRTCalculation.addMRTWeightingFactor(people, 0.5)); EXPECT_EQ(1u, zoneMRTCalculation.numberofMRTWeightingFactors()); + EXPECT_EQ(0u, zoneMRTCalculation.mrtWeightingFactorIndex(people).get()); + // Indexed access returns the complete helper wrapper, while out-of-range access returns none. boost::optional mrtWeightingFactor = zoneMRTCalculation.getMRTWeightingFactor(0); ASSERT_TRUE(mrtWeightingFactor); EXPECT_EQ(people.handle(), mrtWeightingFactor->people().handle()); EXPECT_DOUBLE_EQ(0.5, mrtWeightingFactor->mrtWeightingFactor()); EXPECT_FALSE(zoneMRTCalculation.getMRTWeightingFactor(1)); + // Adding the same People again updates the existing group rather than creating a duplicate. EXPECT_TRUE(zoneMRTCalculation.addMRTWeightingFactor(people, 0.25)); EXPECT_EQ(1u, zoneMRTCalculation.numberofMRTWeightingFactors()); mrtWeightingFactor = zoneMRTCalculation.getMRTWeightingFactor(0); ASSERT_TRUE(mrtWeightingFactor); EXPECT_DOUBLE_EQ(0.25, mrtWeightingFactor->mrtWeightingFactor()); + // Bulk add succeeds when each new factor keeps the total weighting sum within the IDD maximum of 1.0. People people2(definition); people2.setName("People 2"); EXPECT_TRUE(people2.setSpace(space)); EXPECT_TRUE(zoneMRTCalculation.addMRTWeightingFactors({MRTWeightingFactor(people2, 0.75)})); EXPECT_EQ(2u, zoneMRTCalculation.numberofMRTWeightingFactors()); - People people3(definition); + // A third valid individual factor is rejected if the total weighting sum would exceed 1.0. + PeopleDefinition definition3(model); + definition3.setNumberofPeople(1.0); + EXPECT_TRUE(definition3.setThermalComfortModelType(0, "Fanger")); + People people3(definition3); people3.setName("People 3"); EXPECT_TRUE(people3.setSpace(space)); EXPECT_FALSE(zoneMRTCalculation.addMRTWeightingFactor(people3, 0.01)); EXPECT_EQ(2u, zoneMRTCalculation.numberofMRTWeightingFactors()); + // Replacing an existing People's weight recalculates the total without double-counting the old value. EXPECT_TRUE(zoneMRTCalculation.addMRTWeightingFactor(people, 0.20)); EXPECT_EQ(2u, zoneMRTCalculation.numberofMRTWeightingFactors()); mrtWeightingFactor = zoneMRTCalculation.getMRTWeightingFactor(0); ASSERT_TRUE(mrtWeightingFactor); EXPECT_DOUBLE_EQ(0.20, mrtWeightingFactor->mrtWeightingFactor()); + // Removing a referenced People object cleans up its ZoneMRTCalculation extensible group. people2.remove(); EXPECT_EQ(1u, zoneMRTCalculation.numberofMRTWeightingFactors()); EXPECT_TRUE(zoneMRTCalculation.addMRTWeightingFactor(people3, 0.80)); EXPECT_EQ(2u, zoneMRTCalculation.numberofMRTWeightingFactors()); + // The scenarios below would test more proactive cleanup than most existing extensible-reference patterns use. + // Existing examples generally remove extensible references from the referenced object's remove() path, while + // parent changes and other invalidating edits are handled through explicit remove APIs or read/translation-time + // filtering. + /* + EXPECT_TRUE(people3.setSpace(otherSpace)); + EXPECT_EQ(1u, zoneMRTCalculation.numberofMRTWeightingFactors()); + + EXPECT_TRUE(people3.setSpace(space)); + EXPECT_TRUE(zoneMRTCalculation.addMRTWeightingFactor(people3, 0.80)); + EXPECT_EQ(2u, zoneMRTCalculation.numberofMRTWeightingFactors()); + + people3.resetSpace(); + EXPECT_EQ(1u, zoneMRTCalculation.numberofMRTWeightingFactors()); + + EXPECT_TRUE(people3.setSpace(space)); + EXPECT_TRUE(zoneMRTCalculation.addMRTWeightingFactor(people3, 0.80)); + EXPECT_EQ(2u, zoneMRTCalculation.numberofMRTWeightingFactors()); + + SpaceType spaceType(model); + EXPECT_TRUE(people3.setSpaceType(spaceType)); + EXPECT_EQ(1u, zoneMRTCalculation.numberofMRTWeightingFactors()); + + EXPECT_TRUE(people3.setSpace(space)); + EXPECT_TRUE(zoneMRTCalculation.addMRTWeightingFactor(people3, 0.80)); + EXPECT_EQ(2u, zoneMRTCalculation.numberofMRTWeightingFactors()); + + EXPECT_TRUE(definition3.eraseThermalComfortModelType(0)); + EXPECT_EQ(1u, zoneMRTCalculation.numberofMRTWeightingFactors()); + mrtWeightingFactor = zoneMRTCalculation.getMRTWeightingFactor(0); + ASSERT_TRUE(mrtWeightingFactor); + EXPECT_EQ(people.handle(), mrtWeightingFactor->people().handle()); + + EXPECT_TRUE(definition3.setThermalComfortModelType(0, "Fanger")); + */ + + // Bulk add reports partial failure while keeping any valid groups that were accepted before/after the failure. zoneMRTCalculation.removeAllMRTWeightingFactors(); EXPECT_TRUE(zoneMRTCalculation.addMRTWeightingFactor(people, 0.5)); EXPECT_FALSE( zoneMRTCalculation.addMRTWeightingFactors({MRTWeightingFactor(peopleWithoutThermalComfortModel, 0.25), MRTWeightingFactor(people3, 0.5)})); EXPECT_EQ(2u, zoneMRTCalculation.numberofMRTWeightingFactors()); + // Single-group removal and remove-all leave the object with the expected remaining group count. zoneMRTCalculation.removeMRTWeightingFactor(0); EXPECT_EQ(1u, zoneMRTCalculation.numberofMRTWeightingFactors()); From 5ac4ad6f9c0771919c2fc465a45813f1128e0585 Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Wed, 2 Sep 2026 09:04:07 -0700 Subject: [PATCH 11/13] Remove alpha type from Thermal Zone Name field in idd. --- resources/model/OpenStudio.idd | 1 - 1 file changed, 1 deletion(-) diff --git a/resources/model/OpenStudio.idd b/resources/model/OpenStudio.idd index 70fa199e76..6e9a108cf2 100644 --- a/resources/model/OpenStudio.idd +++ b/resources/model/OpenStudio.idd @@ -6827,7 +6827,6 @@ OS:ZoneMRTCalculation, \type handle \required-field A2, \field Thermal Zone Name - \type alpha \type object-list \object-list ThermalZoneNames \required-field From 4b08232860fad3c245f212445b9c0a2eb215378e Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Wed, 2 Sep 2026 09:04:51 -0700 Subject: [PATCH 12/13] Clean typo in clone log and throw. --- src/model/ZoneMRTCalculation.cpp | 2 +- src/model/ZonePropertyUserViewFactorsBySurfaceName.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/model/ZoneMRTCalculation.cpp b/src/model/ZoneMRTCalculation.cpp index 9ccb9060f9..9bd840f043 100644 --- a/src/model/ZoneMRTCalculation.cpp +++ b/src/model/ZoneMRTCalculation.cpp @@ -82,7 +82,7 @@ namespace model { } ModelObject ZoneMRTCalculation_Impl::clone(Model /*model*/) const { - LOG_AND_THROW("Cloning isn't allowed for ZoneMRTCalculation in order to guarantee that every ZoneMRTCalculation has a thermal zone, and" + LOG_AND_THROW("Cloning isn't allowed for ZoneMRTCalculation in order to guarantee that every ZoneMRTCalculation has a thermal zone, and " "that a thermal zone must have only one ZoneMRTCalculation."); } diff --git a/src/model/ZonePropertyUserViewFactorsBySurfaceName.cpp b/src/model/ZonePropertyUserViewFactorsBySurfaceName.cpp index c9041a7ef2..9de9941e8c 100644 --- a/src/model/ZonePropertyUserViewFactorsBySurfaceName.cpp +++ b/src/model/ZonePropertyUserViewFactorsBySurfaceName.cpp @@ -119,7 +119,7 @@ namespace model { ModelObject ZonePropertyUserViewFactorsBySurfaceName_Impl::clone(Model /*model*/) const { LOG_AND_THROW("Cloning isn't allowed for ZonePropertyUserViewFactorsBySurfaceName in order to guarantee that every " - "ZonePropertyUserViewFactorsBySurfaceName has a thermal zone, and" + "ZonePropertyUserViewFactorsBySurfaceName has a thermal zone, and " "that a thermal zone must have only one ZonePropertyUserViewFactorsBySurfaceName."); } From 435380bf25b3589aec80bcfc8b62552ae1bf455c Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Wed, 2 Sep 2026 09:05:57 -0700 Subject: [PATCH 13/13] Update ThermalZone with ZoneMRT remove and new output variable. --- src/model/ThermalZone.cpp | 7 ++++++- src/model/test/ZoneMRTCalculation_GTest.cpp | 23 +++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/model/ThermalZone.cpp b/src/model/ThermalZone.cpp index bb6bbf7cb0..c266b1cf37 100644 --- a/src/model/ThermalZone.cpp +++ b/src/model/ThermalZone.cpp @@ -190,7 +190,7 @@ namespace model { // Temperatures and RH "Zone Mean Air Temperature", // Zone level "Zone Air Temperature", // HVAC level - "Zone Mean Air Dewpoint Temperature", "Zone Mean Radiant Temperature", "Zone Operative Temperature", "Zone Air Humidity Ratio", + "Zone Mean Air Dewpoint Temperature", "Zone Mean Radiant Temperature", "Zone Standard Mean Radiant Temperature", "Zone Operative Temperature", "Zone Air Humidity Ratio", "Zone Air Relative Humidity", // Heat Balance @@ -1673,6 +1673,11 @@ namespace model { mixing.remove(); } + // remove ZoneMRTCalculation objects before the required Thermal Zone pointer is cleared + for (auto& zoneMRTCalculation : thermalZone.getModelObjectSources(ZoneMRTCalculation::iddObjectType())) { + zoneMRTCalculation.remove(); + } + return HVACComponent_Impl::remove(); } diff --git a/src/model/test/ZoneMRTCalculation_GTest.cpp b/src/model/test/ZoneMRTCalculation_GTest.cpp index c30461fca1..81e4a4f4a5 100644 --- a/src/model/test/ZoneMRTCalculation_GTest.cpp +++ b/src/model/test/ZoneMRTCalculation_GTest.cpp @@ -82,6 +82,29 @@ TEST_F(ModelFixture, ZoneMRTCalculation_ThrowingAPIs) { EXPECT_THROW(zoneMRTCalculation.clone(model), openstudio::Exception); } +TEST_F(ModelFixture, ZoneMRTCalculation_ThermalZoneRemove) { + Model model; + ThermalZone thermalZone(model); + + Space space(model); + EXPECT_TRUE(space.setThermalZone(thermalZone)); + + PeopleDefinition definition(model); + definition.setNumberofPeople(1.0); + EXPECT_TRUE(definition.setThermalComfortModelType(0, "Fanger")); + + People people(definition); + EXPECT_TRUE(people.setSpace(space)); + + ZoneMRTCalculation zoneMRTCalculation = thermalZone.getZoneMRTCalculation(); + EXPECT_TRUE(zoneMRTCalculation.addMRTWeightingFactor(people, 1.0)); + ASSERT_EQ(1u, model.getConcreteModelObjects().size()); + + // Removing the ThermalZone should also remove the ZoneMRTCalculation that requires it. + thermalZone.remove(); + EXPECT_TRUE(model.getConcreteModelObjects().empty()); +} + TEST_F(ModelFixture, ZoneMRTCalculation_AddAndRemoveMRTWeightingFactors) { Model model; ThermalZone thermalZone(model);