From 848afbb66e0fb6292ca3a22639ffc7b895322443 Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Tue, 8 Sep 2026 10:28:18 -0700 Subject: [PATCH 01/20] Add ComfortViewFactorAngles model schema --- resources/model/OpenStudio.idd | 41 ++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/resources/model/OpenStudio.idd b/resources/model/OpenStudio.idd index 2e4c37e634..938fb8dc9b 100644 --- a/resources/model/OpenStudio.idd +++ b/resources/model/OpenStudio.idd @@ -3895,7 +3895,7 @@ OS:InternalMass:Definition, OS:People:Definition, \extensible:1 \min-fields 1 - \max-fields 16 + \max-fields 17 A1, \field Handle \type handle \required-field @@ -3960,7 +3960,10 @@ OS:People:Definition, \key EnclosureAveraged \key SurfaceWeighted \key AngleFactor - A6; \field Thermal Comfort Model Type + A6, \field Surface Name/Angle Factor List Name + \type object-list + \object-list AllHeatTranAngFacNames + A7; \field Thermal Comfort Model Type \note optional (only needed for people thermal comfort results reporting) \type choice \begin-extensible @@ -3972,6 +3975,24 @@ OS:People:Definition, \key CoolingEffectASH55 \key AnkleDraftASH55 +OS:ComfortViewFactorAngles, + \extensible:2 + A1, \field Handle + \type handle + \required-field + A2, \field Name + \type alpha + \required-field + \reference AllHeatTranAngFacNames + A3, \field Surface 1 Name + \type object-list + \object-list SurfaceNames + \begin-extensible + N1; \field Angle Factor 1 + \type real + \minimum 0.0 + \maximum 1.0 + OS:Lights:Definition, \min-fields 1 A1, \field Handle @@ -7079,31 +7100,27 @@ OS:People, \note units in schedule should be fraction applied to number of people (0.0 - 1.0) \type object-list \object-list ScheduleNames - A7, \field Surface Name/Angle Factor List Name - \note optional (only required for thermal comfort runs) - \type object-list - \object-list AllHeatTranAngFacNames - A8, \field Work Efficiency Schedule Name + A7, \field Work Efficiency Schedule Name \note units in schedule are 0.0 to 1.0 \note optional (only required for thermal comfort runs) \type object-list \object-list ScheduleNames - A9, \field Clothing Insulation Calculation Method + A8, \field Clothing Insulation Calculation Method \type choice \key ClothingInsulationSchedule \key DynamicClothingModelASHRAE55 \key CalculationMethodSchedule \required-field - A10, \field Clothing Insulation Calculation Method Schedule Name + A9, \field Clothing Insulation Calculation Method Schedule Name \type object-list \object-list ScheduleNames \note a schedule value of 1 for the Scheduled method, and 2 for the DynamicClothingModelASHRAE55 method - A11, \field Clothing Insulation Schedule Name + A10, \field Clothing Insulation Schedule Name \note use "Clo" from ASHRAE or Thermal Comfort guides \note optional (only required for thermal comfort runs) \type object-list \object-list ScheduleNames - A12, \field Air Velocity Schedule Name + A11, \field Air Velocity Schedule Name \note units in the schedule are m/s \note optional (only required for thermal comfort runs) \type object-list @@ -7112,7 +7129,7 @@ OS:People, \type real \minimum 0.0 \default 1.0 - A13, \field Ankle Level Air Velocity Schedule Name + A12, \field Ankle Level Air Velocity Schedule Name \type object-list \object-list ScheduleNames \note units in the schedule are m/s From 6c9beaa6c7dd2240f6406745ae5bed31a585ee91 Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Tue, 8 Sep 2026 10:28:28 -0700 Subject: [PATCH 02/20] Add ComfortViewFactorAngles model object --- src/model/CMakeLists.txt | 4 + src/model/ComfortViewFactorAngles.cpp | 158 ++++++++++++++++++ src/model/ComfortViewFactorAngles.hpp | 71 ++++++++ src/model/ComfortViewFactorAngles_Impl.hpp | 45 +++++ src/model/ConcreteModelObjects.hpp | 1 + src/model/Model.cpp | 1 + src/model/ModelGeometry.i | 2 + src/model/PeopleDefinition.cpp | 36 ++++ src/model/PeopleDefinition.hpp | 6 + src/model/PeopleDefinition_Impl.hpp | 6 + src/model/People_Impl.hpp | 2 - .../test/ComfortViewFactorAngles_GTest.cpp | 52 ++++++ src/model/test/People_GTest.cpp | 21 ++- 13 files changed, 402 insertions(+), 3 deletions(-) create mode 100644 src/model/ComfortViewFactorAngles.cpp create mode 100644 src/model/ComfortViewFactorAngles.hpp create mode 100644 src/model/ComfortViewFactorAngles_Impl.hpp create mode 100644 src/model/test/ComfortViewFactorAngles_GTest.cpp diff --git a/src/model/CMakeLists.txt b/src/model/CMakeLists.txt index c2e428202c..037db74e13 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 + ComfortViewFactorAngles.hpp + ComfortViewFactorAngles_Impl.hpp + ComfortViewFactorAngles.cpp ZoneMRTCalculation.hpp ZoneMRTCalculation_Impl.hpp ZoneMRTCalculation.cpp @@ -2044,6 +2047,7 @@ set(${target_name}_test_src test/CoilWaterHeatingAirToWaterHeatPumpVariableSpeedSpeedData_GTest.cpp test/CoilWaterHeatingAirToWaterHeatPumpWrapped_GTest.cpp test/CoilWaterHeatingDesuperheater_GTest.cpp + test/ComfortViewFactorAngles_GTest.cpp test/Component_GTest.cpp test/Connection_GTest.cpp test/Construction_GTest.cpp diff --git a/src/model/ComfortViewFactorAngles.cpp b/src/model/ComfortViewFactorAngles.cpp new file mode 100644 index 0000000000..3f311c0295 --- /dev/null +++ b/src/model/ComfortViewFactorAngles.cpp @@ -0,0 +1,158 @@ +/*********************************************************************************************************************** +* OpenStudio(R), Copyright (c) Alliance for Energy Innovation, LLC. +* See also https://openstudio.net/license +***********************************************************************************************************************/ + +#include "ComfortViewFactorAngles.hpp" +#include "ComfortViewFactorAngles_Impl.hpp" + +#include "Model.hpp" +#include "ModelExtensibleGroup.hpp" + +#include +#include + +#include "../utilities/core/Assert.hpp" + +namespace openstudio { +namespace model { + + ComfortViewFactorAngle::ComfortViewFactorAngle(const Surface& surface, double angleFactor) : m_surface(surface), m_angleFactor(angleFactor) { + if ((m_angleFactor < 0.0) || (m_angleFactor > 1.0)) { + LOG_AND_THROW("Angle Factor must be between 0 and 1."); + } + } + + Surface ComfortViewFactorAngle::surface() const { + return m_surface; + } + + double ComfortViewFactorAngle::angleFactor() const { + return m_angleFactor; + } + + namespace detail { + + ComfortViewFactorAngles_Impl::ComfortViewFactorAngles_Impl(const IdfObject& idfObject, Model_Impl* model, bool keepHandle) + : ModelObject_Impl(idfObject, model, keepHandle) { + OS_ASSERT(idfObject.iddObject().type() == ComfortViewFactorAngles::iddObjectType()); + } + + ComfortViewFactorAngles_Impl::ComfortViewFactorAngles_Impl(const openstudio::detail::WorkspaceObject_Impl& other, Model_Impl* model, + bool keepHandle) + : ModelObject_Impl(other, model, keepHandle) { + OS_ASSERT(other.iddObject().type() == ComfortViewFactorAngles::iddObjectType()); + } + + ComfortViewFactorAngles_Impl::ComfortViewFactorAngles_Impl(const ComfortViewFactorAngles_Impl& other, Model_Impl* model, bool keepHandle) + : ModelObject_Impl(other, model, keepHandle) {} + + const std::vector& ComfortViewFactorAngles_Impl::outputVariableNames() const { + static const std::vector result; + return result; + } + + IddObjectType ComfortViewFactorAngles_Impl::iddObjectType() const { + return ComfortViewFactorAngles::iddObjectType(); + } + + std::vector ComfortViewFactorAngles_Impl::comfortViewFactorAngles() const { + std::vector result; + for (unsigned i = 0; i < numberofComfortViewFactorAngles(); ++i) { + if (auto comfortViewFactorAngle = getComfortViewFactorAngle(i)) { + result.push_back(*comfortViewFactorAngle); + } + } + return result; + } + + unsigned int ComfortViewFactorAngles_Impl::numberofComfortViewFactorAngles() const { + return numExtensibleGroups(); + } + + boost::optional ComfortViewFactorAngles_Impl::getComfortViewFactorAngle(unsigned groupIndex) const { + if (groupIndex >= numberofComfortViewFactorAngles()) { + return boost::none; + } + auto group = getExtensibleGroup(groupIndex).cast(); + auto surface = group.getModelObjectTarget(OS_ComfortViewFactorAnglesExtensibleFields::SurfaceName); + auto angleFactor = group.getDouble(OS_ComfortViewFactorAnglesExtensibleFields::AngleFactor); + if (surface && angleFactor) { + return ComfortViewFactorAngle(*surface, *angleFactor); + } + return boost::none; + } + + bool ComfortViewFactorAngles_Impl::addComfortViewFactorAngle(const ComfortViewFactorAngle& comfortViewFactorAngle) { + if (comfortViewFactorAngle.surface().model() != model()) { + LOG(Error, "Cannot add a Surface from another Model to " << briefDescription() << "."); + return false; + } + auto group = pushExtensibleGroup({}, false).cast(); + bool surfaceSet = group.setPointer(OS_ComfortViewFactorAnglesExtensibleFields::SurfaceName, comfortViewFactorAngle.surface().handle(), false); + bool angleFactorSet = group.setDouble(OS_ComfortViewFactorAnglesExtensibleFields::AngleFactor, comfortViewFactorAngle.angleFactor()); + if (surfaceSet && angleFactorSet) { + return true; + } + getObject().eraseExtensibleGroup(group.groupIndex()); + return false; + } + + bool ComfortViewFactorAngles_Impl::addComfortViewFactorAngle(const Surface& surface, double angleFactor) { + return addComfortViewFactorAngle(ComfortViewFactorAngle(surface, angleFactor)); + } + + bool ComfortViewFactorAngles_Impl::removeComfortViewFactorAngle(unsigned groupIndex) { + if (groupIndex >= numberofComfortViewFactorAngles()) { + return false; + } + getObject().eraseExtensibleGroup(groupIndex); + return true; + } + + void ComfortViewFactorAngles_Impl::removeAllComfortViewFactorAngles() { + getObject().clearExtensibleGroups(); + } + + } // namespace detail + + ComfortViewFactorAngles::ComfortViewFactorAngles(const Model& model) : ModelObject(ComfortViewFactorAngles::iddObjectType(), model) { + OS_ASSERT(getImpl()); + } + + IddObjectType ComfortViewFactorAngles::iddObjectType() { + return IddObjectType::OS_ComfortViewFactorAngles; + } + + std::vector ComfortViewFactorAngles::comfortViewFactorAngles() const { + return getImpl()->comfortViewFactorAngles(); + } + + unsigned int ComfortViewFactorAngles::numberofComfortViewFactorAngles() const { + return getImpl()->numberofComfortViewFactorAngles(); + } + + boost::optional ComfortViewFactorAngles::getComfortViewFactorAngle(unsigned groupIndex) const { + return getImpl()->getComfortViewFactorAngle(groupIndex); + } + + bool ComfortViewFactorAngles::addComfortViewFactorAngle(const ComfortViewFactorAngle& comfortViewFactorAngle) { + return getImpl()->addComfortViewFactorAngle(comfortViewFactorAngle); + } + + bool ComfortViewFactorAngles::addComfortViewFactorAngle(const Surface& surface, double angleFactor) { + return getImpl()->addComfortViewFactorAngle(surface, angleFactor); + } + + void ComfortViewFactorAngles::removeComfortViewFactorAngle(int groupIndex) { + getImpl()->removeComfortViewFactorAngle(groupIndex); + } + + void ComfortViewFactorAngles::removeAllComfortViewFactorAngles() { + getImpl()->removeAllComfortViewFactorAngles(); + } + + ComfortViewFactorAngles::ComfortViewFactorAngles(std::shared_ptr impl) : ModelObject(std::move(impl)) {} + +} // namespace model +} // namespace openstudio \ No newline at end of file diff --git a/src/model/ComfortViewFactorAngles.hpp b/src/model/ComfortViewFactorAngles.hpp new file mode 100644 index 0000000000..e637318a2e --- /dev/null +++ b/src/model/ComfortViewFactorAngles.hpp @@ -0,0 +1,71 @@ +/*********************************************************************************************************************** +* OpenStudio(R), Copyright (c) Alliance for Energy Innovation, LLC. +* See also https://openstudio.net/license +***********************************************************************************************************************/ + +#ifndef MODEL_COMFORTVIEWFACTORANGLES_HPP +#define MODEL_COMFORTVIEWFACTORANGLES_HPP + +#include "ModelAPI.hpp" +#include "ModelObject.hpp" +#include "Surface.hpp" + +namespace openstudio { +namespace model { + + namespace detail { + class ComfortViewFactorAngles_Impl; + } + + class MODEL_API ComfortViewFactorAngle + { + public: + ComfortViewFactorAngle(const Surface& surface, double angleFactor); + + Surface surface() const; + double angleFactor() const; + + private: + Surface m_surface; + double m_angleFactor; + }; + + class MODEL_API ComfortViewFactorAngles : public ModelObject + { + public: + explicit ComfortViewFactorAngles(const Model& model); + + virtual ~ComfortViewFactorAngles() override = default; + + static IddObjectType iddObjectType(); + + std::vector comfortViewFactorAngles() const; + unsigned int numberofComfortViewFactorAngles() const; + boost::optional getComfortViewFactorAngle(unsigned groupIndex) const; + + bool addComfortViewFactorAngle(const ComfortViewFactorAngle& comfortViewFactorAngle); + bool addComfortViewFactorAngle(const Surface& surface, double angleFactor); + void removeComfortViewFactorAngle(int groupIndex); + void removeAllComfortViewFactorAngles(); + + protected: + using ImplType = detail::ComfortViewFactorAngles_Impl; + + explicit ComfortViewFactorAngles(std::shared_ptr impl); + + friend class detail::ComfortViewFactorAngles_Impl; + friend class Model; + friend class IdfObject; + friend class openstudio::detail::IdfObject_Impl; + + private: + REGISTER_LOGGER("openstudio.model.ComfortViewFactorAngles"); + }; + + using OptionalComfortViewFactorAngles = boost::optional; + using ComfortViewFactorAnglesVector = std::vector; + +} // namespace model +} // namespace openstudio + +#endif // MODEL_COMFORTVIEWFACTORANGLES_HPP \ No newline at end of file diff --git a/src/model/ComfortViewFactorAngles_Impl.hpp b/src/model/ComfortViewFactorAngles_Impl.hpp new file mode 100644 index 0000000000..7bffb64eb0 --- /dev/null +++ b/src/model/ComfortViewFactorAngles_Impl.hpp @@ -0,0 +1,45 @@ +/*********************************************************************************************************************** +* OpenStudio(R), Copyright (c) Alliance for Energy Innovation, LLC. +* See also https://openstudio.net/license +***********************************************************************************************************************/ + +#ifndef MODEL_COMFORTVIEWFACTORANGLES_IMPL_HPP +#define MODEL_COMFORTVIEWFACTORANGLES_IMPL_HPP + +#include "ModelAPI.hpp" +#include "ModelObject_Impl.hpp" + +namespace openstudio { +namespace model { + + class ComfortViewFactorAngle; + class Surface; + + namespace detail { + class MODEL_API ComfortViewFactorAngles_Impl : public ModelObject_Impl + { + public: + ComfortViewFactorAngles_Impl(const IdfObject& idfObject, Model_Impl* model, bool keepHandle); + ComfortViewFactorAngles_Impl(const openstudio::detail::WorkspaceObject_Impl& other, Model_Impl* model, bool keepHandle); + ComfortViewFactorAngles_Impl(const ComfortViewFactorAngles_Impl& other, Model_Impl* model, bool keepHandle); + + virtual ~ComfortViewFactorAngles_Impl() override = default; + + virtual const std::vector& outputVariableNames() const override; + virtual IddObjectType iddObjectType() const override; + + std::vector comfortViewFactorAngles() const; + unsigned int numberofComfortViewFactorAngles() const; + boost::optional getComfortViewFactorAngle(unsigned groupIndex) const; + + bool addComfortViewFactorAngle(const ComfortViewFactorAngle& comfortViewFactorAngle); + bool addComfortViewFactorAngle(const Surface& surface, double angleFactor); + bool removeComfortViewFactorAngle(unsigned groupIndex); + void removeAllComfortViewFactorAngles(); + }; + } + +} // namespace model +} // namespace openstudio + +#endif // MODEL_COMFORTVIEWFACTORANGLES_IMPL_HPP \ No newline at end of file diff --git a/src/model/ConcreteModelObjects.hpp b/src/model/ConcreteModelObjects.hpp index 2835b2addc..2f74bfab1e 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 "ComfortViewFactorAngles.hpp" #include "ZoneMRTCalculation.hpp" #include "ZonePropertyUserViewFactorsBySurfaceName.hpp" #include "ZoneVentilationDesignFlowRate.hpp" diff --git a/src/model/Model.cpp b/src/model/Model.cpp index 9e4ff763d4..33098bfa30 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(ComfortViewFactorAngles); REGISTER_CONSTRUCTOR(ZoneMRTCalculation); REGISTER_CONSTRUCTOR(ZonePropertyUserViewFactorsBySurfaceName); REGISTER_CONSTRUCTOR(ZoneVentilationDesignFlowRate); diff --git a/src/model/ModelGeometry.i b/src/model/ModelGeometry.i index 0068937e08..8e8d2ab426 100644 --- a/src/model/ModelGeometry.i +++ b/src/model/ModelGeometry.i @@ -254,6 +254,8 @@ MODELOBJECT_TEMPLATES(FoundationKiva); MODELOBJECT_TEMPLATES(SurfacePropertyExposedFoundationPerimeter); MODELOBJECT_TEMPLATES(ViewFactor); // Helper class defined in ZonePropertyUserViewFactorsBySurfaceName MODELOBJECT_TEMPLATES(ZonePropertyUserViewFactorsBySurfaceName); +MODELOBJECT_TEMPLATES(ComfortViewFactorAngle); +MODELOBJECT_TEMPLATES(ComfortViewFactorAngles); MODELOBJECT_TEMPLATES(MRTWeightingFactor); // Helper class defined in ZoneMRTCalculation MODELOBJECT_TEMPLATES(ZoneMRTCalculation); MODELOBJECT_TEMPLATES(ExteriorLoadInstance); diff --git a/src/model/PeopleDefinition.cpp b/src/model/PeopleDefinition.cpp index e55093b2f7..e0cd8579c1 100644 --- a/src/model/PeopleDefinition.cpp +++ b/src/model/PeopleDefinition.cpp @@ -6,6 +6,9 @@ #include "PeopleDefinition.hpp" #include "PeopleDefinition_Impl.hpp" +#include "ComfortViewFactorAngles.hpp" +#include "Surface.hpp" + #include "../utilities/idf/IdfExtensibleGroup.hpp" #include @@ -129,6 +132,10 @@ namespace model { return isEmpty(OS_People_DefinitionFields::MeanRadiantTemperatureCalculationType); } + boost::optional PeopleDefinition_Impl::surfaceNameAngleFactorListName() const { + return getObject().getModelObjectTarget(OS_People_DefinitionFields::SurfaceNameAngleFactorListName); + } + boost::optional PeopleDefinition_Impl::getThermalComfortModelType(int i) const { OptionalString result; if (i < numThermalComfortModelTypes()) { @@ -251,6 +258,9 @@ namespace model { const std::string mrtType = istringEqual("ZoneAveraged", meanRadiantTemperatureCalculationType) ? "EnclosureAveraged" : meanRadiantTemperatureCalculationType; bool result = setString(OS_People_DefinitionFields::MeanRadiantTemperatureCalculationType, mrtType); + if (result && istringEqual(mrtType, "EnclosureAveraged")) { + resetSurfaceNameAngleFactorListName(); + } return result; } @@ -259,6 +269,32 @@ namespace model { OS_ASSERT(result); } + bool PeopleDefinition_Impl::setSurfaceNameAngleFactorListName(const ModelObject& modelObject) { + if (modelObject.model() != model()) { + LOG(Error, "Surface Name/Angle Factor List Name must reference an object in the same Model."); + return false; + } + + std::string mrtType; + if (modelObject.optionalCast()) { + mrtType = "SurfaceWeighted"; + } else if (modelObject.optionalCast()) { + mrtType = "AngleFactor"; + } else { + LOG(Error, "Surface Name/Angle Factor List Name must reference a Surface or ComfortViewFactorAngles object."); + return false; + } + if (!setMeanRadiantTemperatureCalculationType(mrtType)) { + return false; + } + return setPointer(OS_People_DefinitionFields::SurfaceNameAngleFactorListName, modelObject.handle()); + } + + void PeopleDefinition_Impl::resetSurfaceNameAngleFactorListName() { + bool result = setString(OS_People_DefinitionFields::SurfaceNameAngleFactorListName, ""); + OS_ASSERT(result); + } + bool PeopleDefinition_Impl::pushThermalComfortModelType(const std::string& thermalComfortModelType) { return !pushExtensibleGroup(StringVector(1u, thermalComfortModelType)).empty(); } diff --git a/src/model/PeopleDefinition.hpp b/src/model/PeopleDefinition.hpp index 6fc4e83b64..f9ecae8baa 100644 --- a/src/model/PeopleDefinition.hpp +++ b/src/model/PeopleDefinition.hpp @@ -78,6 +78,8 @@ namespace model { bool isMeanRadiantTemperatureCalculationTypeDefaulted() const; + boost::optional surfaceNameAngleFactorListName() const; + /** Return the thermal comfort model type in extensible group i. Indexing starts at 0, and * i should be less than numThermalComfortModelTypes. */ boost::optional getThermalComfortModelType(int i) const; @@ -118,6 +120,10 @@ namespace model { void resetMeanRadiantTemperatureCalculationType(); + bool setSurfaceNameAngleFactorListName(const ModelObject& modelObject); + + void resetSurfaceNameAngleFactorListName(); + bool pushThermalComfortModelType(const std::string& thermalComfortModelType); bool setThermalComfortModelType(int i, const std::string& thermalComfortModelType); diff --git a/src/model/PeopleDefinition_Impl.hpp b/src/model/PeopleDefinition_Impl.hpp index b56bdc31f9..c230b4c0f0 100644 --- a/src/model/PeopleDefinition_Impl.hpp +++ b/src/model/PeopleDefinition_Impl.hpp @@ -72,6 +72,8 @@ namespace model { bool isMeanRadiantTemperatureCalculationTypeDefaulted() const; + boost::optional surfaceNameAngleFactorListName() const; + /** Return the thermal comfort model type in extensible group i. Indexing starts at 0, and * i should be less than numThermalComfortModelTypes. */ boost::optional getThermalComfortModelType(int i) const; @@ -109,6 +111,10 @@ namespace model { void resetMeanRadiantTemperatureCalculationType(); + bool setSurfaceNameAngleFactorListName(const ModelObject& modelObject); + + void resetSurfaceNameAngleFactorListName(); + bool pushThermalComfortModelType(const std::string& thermalComfortModelType); bool setThermalComfortModelType(int i, const std::string& thermalComfortModelType); diff --git a/src/model/People_Impl.hpp b/src/model/People_Impl.hpp index 1c2bbec154..33ac39ee53 100644 --- a/src/model/People_Impl.hpp +++ b/src/model/People_Impl.hpp @@ -78,8 +78,6 @@ namespace model { /** Returns true if this object does not specify an activity directly. */ bool isActivityLevelScheduleDefaulted() const; - // TODO: Handle Non-Extensible IddField Surface Name/Angle Factor List Name. - /** Returns the work efficiency schedule. */ boost::optional workEfficiencySchedule() const; diff --git a/src/model/test/ComfortViewFactorAngles_GTest.cpp b/src/model/test/ComfortViewFactorAngles_GTest.cpp new file mode 100644 index 0000000000..669a32dffc --- /dev/null +++ b/src/model/test/ComfortViewFactorAngles_GTest.cpp @@ -0,0 +1,52 @@ +/*********************************************************************************************************************** +* OpenStudio(R), Copyright (c) Alliance for Energy Innovation, LLC. +* See also https://openstudio.net/license +***********************************************************************************************************************/ + +#include + +#include "ModelFixture.hpp" + +#include "../ComfortViewFactorAngles.hpp" +#include "../Model.hpp" +#include "../PeopleDefinition.hpp" +#include "../Surface.hpp" + +using namespace openstudio; +using namespace openstudio::model; + +TEST_F(ModelFixture, ComfortViewFactorAngles) { + Model model; + Point3dVector points{{0, 0, 0}, {1, 0, 0}, {1, 1, 0}}; + Surface surface(points, model); + ComfortViewFactorAngles comfortViewFactorAngles(model); + + EXPECT_THROW(ComfortViewFactorAngle(surface, -0.01), openstudio::Exception); + EXPECT_THROW(ComfortViewFactorAngle(surface, 1.01), openstudio::Exception); + + EXPECT_TRUE(comfortViewFactorAngles.addComfortViewFactorAngle(surface, 0.25)); + EXPECT_EQ(1u, comfortViewFactorAngles.numberofComfortViewFactorAngles()); + + auto comfortViewFactorAngle = comfortViewFactorAngles.getComfortViewFactorAngle(0); + ASSERT_TRUE(comfortViewFactorAngle); + EXPECT_EQ(surface.handle(), comfortViewFactorAngle->surface().handle()); + EXPECT_DOUBLE_EQ(0.25, comfortViewFactorAngle->angleFactor()); + EXPECT_FALSE(comfortViewFactorAngles.getComfortViewFactorAngle(1)); + + Model otherModel; + Surface otherSurface(points, otherModel); + EXPECT_FALSE(comfortViewFactorAngles.addComfortViewFactorAngle(otherSurface, 0.75)); + EXPECT_EQ(1u, comfortViewFactorAngles.numberofComfortViewFactorAngles()); + + PeopleDefinition peopleDefinition(model); + EXPECT_FALSE(peopleDefinition.setSurfaceNameAngleFactorListName(otherSurface)); + EXPECT_EQ("EnclosureAveraged", peopleDefinition.meanRadiantTemperatureCalculationType()); + EXPECT_FALSE(peopleDefinition.surfaceNameAngleFactorListName()); + + comfortViewFactorAngles.removeComfortViewFactorAngle(0); + EXPECT_EQ(0u, comfortViewFactorAngles.numberofComfortViewFactorAngles()); + + EXPECT_TRUE(comfortViewFactorAngles.addComfortViewFactorAngle(surface, 1.0)); + comfortViewFactorAngles.removeAllComfortViewFactorAngles(); + EXPECT_TRUE(comfortViewFactorAngles.comfortViewFactorAngles().empty()); +} diff --git a/src/model/test/People_GTest.cpp b/src/model/test/People_GTest.cpp index 9aded7fdb9..6cb17b87a2 100644 --- a/src/model/test/People_GTest.cpp +++ b/src/model/test/People_GTest.cpp @@ -12,6 +12,8 @@ #include "../People_Impl.hpp" #include "../PeopleDefinition.hpp" #include "../PeopleDefinition_Impl.hpp" +#include "../ComfortViewFactorAngles.hpp" +#include "../Surface.hpp" #include "../ScheduleRuleset.hpp" #include "../ScheduleDay.hpp" @@ -48,12 +50,29 @@ TEST_F(ModelFixture, People_DefaultConstructor) { EXPECT_NE("ZoneAveraged", definition.meanRadiantTemperatureCalculationType()); EXPECT_EQ("EnclosureAveraged", definition.meanRadiantTemperatureCalculationType()); - EXPECT_TRUE(definition.setMeanRadiantTemperatureCalculationType("SurfaceWeighted")); + Point3dVector points{{0, 0, 0}, {1, 0, 0}, {1, 1, 0}}; + Surface surface(points, model); + + ComfortViewFactorAngles comfortViewFactorAngles(model); + EXPECT_TRUE(comfortViewFactorAngles.addComfortViewFactorAngle(surface, 1.0)); + EXPECT_EQ(1u, comfortViewFactorAngles.numberofComfortViewFactorAngles()); + auto comfortViewFactorAngle = comfortViewFactorAngles.getComfortViewFactorAngle(0); + ASSERT_TRUE(comfortViewFactorAngle); + EXPECT_EQ(surface.handle(), comfortViewFactorAngle->surface().handle()); + EXPECT_DOUBLE_EQ(1.0, comfortViewFactorAngle->angleFactor()); + + EXPECT_TRUE(definition.setSurfaceNameAngleFactorListName(comfortViewFactorAngles)); + EXPECT_EQ("AngleFactor", definition.meanRadiantTemperatureCalculationType()); + EXPECT_TRUE(definition.surfaceNameAngleFactorListName()); + + EXPECT_TRUE(definition.setSurfaceNameAngleFactorListName(surface)); EXPECT_EQ("SurfaceWeighted", definition.meanRadiantTemperatureCalculationType()); + EXPECT_TRUE(definition.surfaceNameAngleFactorListName()); // Backward compat EXPECT_TRUE(definition.setMeanRadiantTemperatureCalculationType("ZoneAveraged")); EXPECT_EQ("EnclosureAveraged", definition.meanRadiantTemperatureCalculationType()); + EXPECT_FALSE(definition.surfaceNameAngleFactorListName()); EXPECT_FALSE(definition.isMeanRadiantTemperatureCalculationTypeDefaulted()); EXPECT_TRUE(definition.setMeanRadiantTemperatureCalculationType("SurfaceWeighted")); From 2fe1fb57c94056e5105b46bd81c17595b39c98d5 Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Tue, 8 Sep 2026 10:28:42 -0700 Subject: [PATCH 03/20] Translate ComfortViewFactorAngles for People --- src/energyplus/ForwardTranslator.cpp | 5 ++ src/energyplus/ForwardTranslator.hpp | 3 + ...orwardTranslateComfortViewFactorAngles.cpp | 56 +++++++++++++++++++ .../ForwardTranslatePeople.cpp | 28 +++++++++- 4 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 src/energyplus/ForwardTranslator/ForwardTranslateComfortViewFactorAngles.cpp diff --git a/src/energyplus/ForwardTranslator.cpp b/src/energyplus/ForwardTranslator.cpp index 6a499239da..c2c70c85aa 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_ComfortViewFactorAngles: { + auto mo = modelObject.cast(); + retVal = translateComfortViewFactorAngles(mo); + break; + } case openstudio::IddObjectType::OS_ZoneMRTCalculation: { auto mo = modelObject.cast(); retVal = translateZoneMRTCalculation(mo); diff --git a/src/energyplus/ForwardTranslator.hpp b/src/energyplus/ForwardTranslator.hpp index d34ea9e744..068b2505e9 100644 --- a/src/energyplus/ForwardTranslator.hpp +++ b/src/energyplus/ForwardTranslator.hpp @@ -504,6 +504,7 @@ namespace model { class ZoneHVACWaterToAirHeatPump; class ZoneMixing; class ZoneMRTCalculation; + class ComfortViewFactorAngles; class ZonePropertyUserViewFactorsBySurfaceName; class ZoneVentilationDesignFlowRate; class ZoneVentilationWindandStackOpenArea; @@ -1619,6 +1620,8 @@ namespace energyplus { boost::optional translateZoneMRTCalculation(model::ZoneMRTCalculation& modelObject); + boost::optional translateComfortViewFactorAngles(model::ComfortViewFactorAngles& modelObject); + boost::optional translateZonePropertyUserViewFactorsBySurfaceName(model::ZonePropertyUserViewFactorsBySurfaceName& modelObject); boost::optional createAirLoopHVACSupplyPath(const model::Node& demandInletNode); diff --git a/src/energyplus/ForwardTranslator/ForwardTranslateComfortViewFactorAngles.cpp b/src/energyplus/ForwardTranslator/ForwardTranslateComfortViewFactorAngles.cpp new file mode 100644 index 0000000000..c6c66c1edd --- /dev/null +++ b/src/energyplus/ForwardTranslator/ForwardTranslateComfortViewFactorAngles.cpp @@ -0,0 +1,56 @@ +/*********************************************************************************************************************** +* OpenStudio(R), Copyright (c) Alliance for Energy Innovation, LLC. +* See also https://openstudio.net/license +***********************************************************************************************************************/ + +#include "../ForwardTranslator.hpp" + +#include "../../model/ComfortViewFactorAngles.hpp" + +#include "../../utilities/math/FloatCompare.hpp" + +#include + +using namespace openstudio::model; + +namespace openstudio { +namespace energyplus { + + boost::optional ForwardTranslator::translateComfortViewFactorAngles(ComfortViewFactorAngles& modelObject) { + const auto comfortViewFactorAngles = modelObject.comfortViewFactorAngles(); + if (comfortViewFactorAngles.empty()) { + LOG(Error, modelObject.briefDescription() << " does not have any angle factors and will not be translated."); + return boost::none; + } + + double sum = 0.0; + for (const auto& comfortViewFactorAngle : comfortViewFactorAngles) { + sum += comfortViewFactorAngle.angleFactor(); + } + constexpr double tolerance = 0.000001; + if (!equal(sum, 1.0, tolerance)) { + LOG(Error, modelObject.briefDescription() << " has angle factors that sum to " << sum << " instead of 1.0 and will not be translated."); + return boost::none; + } + + std::vector> translatedAngleFactors; + for (const auto& comfortViewFactorAngle : comfortViewFactorAngles) { + if (auto surface = translateAndMapModelObject(comfortViewFactorAngle.surface())) { + translatedAngleFactors.emplace_back(surface->nameString(), comfortViewFactorAngle.angleFactor()); + } else { + LOG(Error, "Could not translate a Surface for " << modelObject.briefDescription() << "."); + return boost::none; + } + } + + IdfObject idfObject = createRegisterAndNameIdfObject(IddObjectType::ComfortViewFactorAngles, modelObject); + for (const auto& [surfaceName, angleFactor] : translatedAngleFactors) { + auto group = idfObject.pushExtensibleGroup(); + group.setString(ComfortViewFactorAnglesExtensibleFields::SurfaceName, surfaceName); + group.setDouble(ComfortViewFactorAnglesExtensibleFields::AngleFactor, angleFactor); + } + return idfObject; + } + +} // namespace energyplus +} // namespace openstudio \ No newline at end of file diff --git a/src/energyplus/ForwardTranslator/ForwardTranslatePeople.cpp b/src/energyplus/ForwardTranslator/ForwardTranslatePeople.cpp index e002e5b8a2..ffeffb3749 100644 --- a/src/energyplus/ForwardTranslator/ForwardTranslatePeople.cpp +++ b/src/energyplus/ForwardTranslator/ForwardTranslatePeople.cpp @@ -10,6 +10,7 @@ #include "../../model/People_Impl.hpp" #include "../../model/PeopleDefinition.hpp" #include "../../model/PeopleDefinition_Impl.hpp" +#include "../../model/ComfortViewFactorAngles.hpp" #include "../../model/Space.hpp" #include "../../model/Space_Impl.hpp" #include "../../model/SpaceType.hpp" @@ -102,11 +103,32 @@ namespace energyplus { } } - if (!definition.isMeanRadiantTemperatureCalculationTypeDefaulted()) { - idfObject.setString(PeopleFields::MeanRadiantTemperatureCalculationType, definition.meanRadiantTemperatureCalculationType()); + std::string mrtType = definition.meanRadiantTemperatureCalculationType(); + if (auto target = definition.surfaceNameAngleFactorListName()) { + if (target->optionalCast()) { + if (auto idfTarget = translateAndMapModelObject(*target)) { + mrtType = "SurfaceWeighted"; + idfObject.setString(PeopleFields::SurfaceName_AngleFactorListName, idfTarget->nameString()); + } else { + mrtType = "EnclosureAveraged"; + } + } else if (target->optionalCast()) { + if (auto idfTarget = translateAndMapModelObject(*target)) { + mrtType = "AngleFactor"; + idfObject.setString(PeopleFields::SurfaceName_AngleFactorListName, idfTarget->nameString()); + } else { + mrtType = "EnclosureAveraged"; + } + } else { + mrtType = "EnclosureAveraged"; + } + } else if (!istringEqual(mrtType, "EnclosureAveraged")) { + mrtType = "EnclosureAveraged"; } - // TODO: Surface Name/Angle Factor List Name + if (!definition.isMeanRadiantTemperatureCalculationTypeDefaulted() || !istringEqual(mrtType, "EnclosureAveraged")) { + idfObject.setString(PeopleFields::MeanRadiantTemperatureCalculationType, mrtType); + } if (boost::optional schedule_ = modelObject.workEfficiencySchedule()) { if (auto idf_schedule_ = translateAndMapModelObject(schedule_.get())) { From f6747776eb31f8a552b8c8e8e2dcea20f92b3d29 Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Tue, 8 Sep 2026 10:28:49 -0700 Subject: [PATCH 04/20] Import ComfortViewFactorAngles from EnergyPlus --- src/energyplus/ReverseTranslator.cpp | 4 ++ src/energyplus/ReverseTranslator.hpp | 2 + ...everseTranslateComfortViewFactorAngles.cpp | 49 +++++++++++++++++++ .../ReverseTranslatePeople.cpp | 5 +- 4 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 src/energyplus/ReverseTranslator/ReverseTranslateComfortViewFactorAngles.cpp diff --git a/src/energyplus/ReverseTranslator.cpp b/src/energyplus/ReverseTranslator.cpp index 34a708a991..3669c21f37 100644 --- a/src/energyplus/ReverseTranslator.cpp +++ b/src/energyplus/ReverseTranslator.cpp @@ -1054,6 +1054,10 @@ namespace energyplus { modelObject = translateZoneMixing(workspaceObject); break; } + case openstudio::IddObjectType::ComfortViewFactorAngles: { + modelObject = translateComfortViewFactorAngles(workspaceObject); + break; + } case openstudio::IddObjectType::ZoneMRTCalculation: { modelObject = translateZoneMRTCalculation(workspaceObject); break; diff --git a/src/energyplus/ReverseTranslator.hpp b/src/energyplus/ReverseTranslator.hpp index 2f30bcf542..9d62341d92 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 translateComfortViewFactorAngles(const WorkspaceObject& workspaceObject); + boost::optional translateZoneMRTCalculation(const WorkspaceObject& workspaceObject); boost::optional translateZonePropertyUserViewFactorsBySurfaceName(const WorkspaceObject& workspaceObject); diff --git a/src/energyplus/ReverseTranslator/ReverseTranslateComfortViewFactorAngles.cpp b/src/energyplus/ReverseTranslator/ReverseTranslateComfortViewFactorAngles.cpp new file mode 100644 index 0000000000..c1f7198dd5 --- /dev/null +++ b/src/energyplus/ReverseTranslator/ReverseTranslateComfortViewFactorAngles.cpp @@ -0,0 +1,49 @@ +/*********************************************************************************************************************** +* OpenStudio(R), Copyright (c) Alliance for Energy Innovation, LLC. +* See also https://openstudio.net/license +***********************************************************************************************************************/ + +#include "../ReverseTranslator.hpp" + +#include "../../model/ComfortViewFactorAngles.hpp" +#include "../../model/Surface.hpp" + +#include "../../utilities/idf/WorkspaceExtensibleGroup.hpp" + +#include + +using namespace openstudio::model; + +namespace openstudio { +namespace energyplus { + + OptionalModelObject ReverseTranslator::translateComfortViewFactorAngles(const WorkspaceObject& workspaceObject) { + if (workspaceObject.iddObject().type() != IddObjectType::ComfortViewFactorAngles) { + LOG(Error, "WorkspaceObject is not IddObjectType: ComfortViewFactorAngles"); + return boost::none; + } + + ComfortViewFactorAngles comfortViewFactorAngles(m_model); + if (auto name = workspaceObject.name()) { + comfortViewFactorAngles.setName(*name); + } + + for (const auto& idfGroup : workspaceObject.extensibleGroups()) { + auto group = idfGroup.cast(); + auto surfaceTarget = group.getTarget(ComfortViewFactorAnglesExtensibleFields::SurfaceName); + auto angleFactor = group.getDouble(ComfortViewFactorAnglesExtensibleFields::AngleFactor); + if (!surfaceTarget || !angleFactor) { + continue; + } + if (auto modelObject = translateAndMapWorkspaceObject(*surfaceTarget)) { + if (auto surface = modelObject->optionalCast()) { + comfortViewFactorAngles.addComfortViewFactorAngle(*surface, *angleFactor); + } + } + } + + return comfortViewFactorAngles; + } + +} // namespace energyplus +} // namespace openstudio \ No newline at end of file diff --git a/src/energyplus/ReverseTranslator/ReverseTranslatePeople.cpp b/src/energyplus/ReverseTranslator/ReverseTranslatePeople.cpp index 3b48d693f5..19d0f1e309 100644 --- a/src/energyplus/ReverseTranslator/ReverseTranslatePeople.cpp +++ b/src/energyplus/ReverseTranslator/ReverseTranslatePeople.cpp @@ -7,6 +7,7 @@ #include "../../model/People.hpp" #include "../../model/People_Impl.hpp" +#include "../../model/ComfortViewFactorAngles.hpp" #include "../../model/PeopleDefinition.hpp" #include "../../model/PeopleDefinition_Impl.hpp" #include "../../model/Space.hpp" @@ -104,7 +105,9 @@ namespace energyplus { OptionalWorkspaceObject target = workspaceObject.getTarget(openstudio::PeopleFields::SurfaceName_AngleFactorListName); if (target) { - LOG(Error, "SurfaceName_AngleFactorListName not currently imported"); + if (auto modelObject = translateAndMapWorkspaceObject(*target)) { + definition.setSurfaceNameAngleFactorListName(*modelObject); + } } // As of 22.2.0, this is no longer possible to make this an extensible field From 54530d73ded89496a3acb68ed0c7b65734f03220 Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Tue, 8 Sep 2026 10:28:59 -0700 Subject: [PATCH 05/20] Translate People MRT target schema --- src/osversion/VersionTranslator.cpp | 44 ++++++++++++++++++- src/osversion/test/3_12_0/test_vt_People.osm | 35 +++++++++++++++ src/osversion/test/3_12_0/test_vt_People.rb | 12 +++++ .../test/VersionTranslator_GTest.cpp | 21 +++++++++ 4 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 src/osversion/test/3_12_0/test_vt_People.osm create mode 100644 src/osversion/test/3_12_0/test_vt_People.rb diff --git a/src/osversion/VersionTranslator.cpp b/src/osversion/VersionTranslator.cpp index ebf0817191..5fb4f063e1 100644 --- a/src/osversion/VersionTranslator.cpp +++ b/src/osversion/VersionTranslator.cpp @@ -10309,7 +10309,49 @@ namespace osversion { ss << newObject; m_refactored.emplace_back(std::move(object), std::move(newObject)); - // No-op + } else if (iddname == "OS:People:Definition") { + + // 1 Field has been added from 3.11.0 to 3.12.0: + // ------------------------------------------------ + // * Surface Name/Angle Factor List Name * 11 + + auto iddObject = idd_3_12_0.getObject(iddname); + IdfObject newObject(iddObject.get()); + + for (size_t i = 0; i < object.numFields(); ++i) { + if ((value = object.getString(i))) { + if (i < 11) { + newObject.setString(i, value.get()); + } else { + newObject.setString(i + 1, value.get()); + } + } + } + + ss << newObject; + m_refactored.emplace_back(std::move(object), std::move(newObject)); + + } else if (iddname == "OS:People") { + + // 1 Field has been removed from 3.11.0 to 3.12.0: + // -------------------------------------------------- + // * Surface Name/Angle Factor List Name * 6 + + auto iddObject = idd_3_12_0.getObject(iddname); + IdfObject newObject(iddObject.get()); + + for (size_t i = 0; i < object.numFields(); ++i) { + if ((value = object.getString(i))) { + if (i < 6) { + newObject.setString(i, value.get()); + } else if (i > 6) { + newObject.setString(i - 1, value.get()); + } + } + } + + ss << newObject; + m_refactored.emplace_back(std::move(object), std::move(newObject)); } else { ss << object; } diff --git a/src/osversion/test/3_12_0/test_vt_People.osm b/src/osversion/test/3_12_0/test_vt_People.osm new file mode 100644 index 0000000000..5797f4dda9 --- /dev/null +++ b/src/osversion/test/3_12_0/test_vt_People.osm @@ -0,0 +1,35 @@ +OS:Version, + {b3d95565-1790-4500-aab1-06a7e08376b0}, !- Handle + 3.11.0; !- Version Identifier + +OS:People:Definition, + {4af0ce1e-eaa2-4451-b35d-777777777777}, !- Handle + People Definition 1, !- Name + People, !- Number of People Calculation Method + 1, !- Number of People + , !- People per Space Floor Area + , !- Space Floor Area per Person + 0.3, !- Fraction Radiant + autocalculate, !- Sensible Heat Fraction + 3.82e-08, !- Carbon Dioxide Generation Rate + No, !- Enable ASHRAE 55 Comfort Warnings + SurfaceWeighted, !- Mean Radiant Temperature Calculation Type + Fanger; !- Thermal Comfort Model 1 Type + +OS:People, + {c69a6d60-b110-4322-8888-999999999999}, !- Handle + People 1, !- Name + {4af0ce1e-eaa2-4451-b35d-777777777777}, !- People Definition Name + , !- Space or SpaceType Name + , !- Number of People Schedule Name + , !- Activity Level Schedule Name + , !- Surface Name/Angle Factor List Name + , !- Work Efficiency Schedule Name + ClothingInsulationSchedule, !- Clothing Insulation Calculation Method + , !- Clothing Insulation Calculation Method Schedule Name + , !- Clothing Insulation Schedule Name + , !- Air Velocity Schedule Name + 1, !- Multiplier + , !- Ankle Level Air Velocity Schedule Name + 15.56, !- Cold Stress Temperature Threshold + 30; !- Heat Stress Temperature Threshold diff --git a/src/osversion/test/3_12_0/test_vt_People.rb b/src/osversion/test/3_12_0/test_vt_People.rb new file mode 100644 index 0000000000..c0301810f0 --- /dev/null +++ b/src/osversion/test/3_12_0/test_vt_People.rb @@ -0,0 +1,12 @@ +#require '/usr/local/openstudio-3.11.0/Ruby/openstudio' + +include OpenStudio::Model + +m = Model.new + +people_def = PeopleDefinition.new(m) +people_def.setMeanRadiantTemperatureCalculationType('SurfaceWeighted') +people = People.new(people_def) +people.setClothingInsulationCalculationMethod('ClothingInsulationSchedule') + +m.save('test_vt_People.osm', true) diff --git a/src/osversion/test/VersionTranslator_GTest.cpp b/src/osversion/test/VersionTranslator_GTest.cpp index 3d34a8b3ee..385b3b5133 100644 --- a/src/osversion/test/VersionTranslator_GTest.cpp +++ b/src/osversion/test/VersionTranslator_GTest.cpp @@ -5102,3 +5102,24 @@ TEST_F(OSVersionFixture, update_3_11_0_to_3_12_0_CoilCoolingDXCurveFitOperatingM EXPECT_EQ("No", opMode.getString(9).get()); // Apply Part Load Fraction to Speeds Greater than 1 EXPECT_EQ("Yes", opMode.getString(10).get()); // Apply Latent Degradation to Speeds Greater than 1 } + +TEST_F(OSVersionFixture, update_3_11_0_to_3_12_0_People) { + openstudio::path path = resourcesPath() / toPath("osversion/3_12_0/test_vt_People.osm"); + osversion::VersionTranslator vt; + boost::optional model = vt.loadModel(path); + ASSERT_TRUE(model) << "Failed to load " << path; + + std::vector definitions = model->getObjectsByType("OS:People:Definition"); + ASSERT_EQ(1u, definitions.size()); + const auto& definition = definitions.front(); + EXPECT_EQ("SurfaceWeighted", definition.getString(10).get()); // Mean Radiant Temperature Calculation Type + EXPECT_TRUE(definition.isEmpty(11)); // Surface Name/Angle Factor List Name + EXPECT_EQ("Fanger", definition.getString(12).get()); // Thermal Comfort Model 1 Type + + std::vector people = model->getObjectsByType("OS:People"); + ASSERT_EQ(1u, people.size()); + const auto& person = people.front(); + EXPECT_TRUE(person.isEmpty(6)); // Work Efficiency Schedule Name + EXPECT_EQ("ClothingInsulationSchedule", person.getString(7).get()); // Clothing Insulation Calculation Method + EXPECT_EQ(1.0, person.getDouble(11).get()); // Multiplier +} From 1c973026872706212a454783fe9b730e70f6fbd2 Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Tue, 8 Sep 2026 10:29:14 -0700 Subject: [PATCH 06/20] Test ComfortViewFactorAngles translation --- .../Test/ComfortViewFactorAngles_GTest.cpp | 126 ++++++++++++++++++ src/energyplus/Test/People_GTest.cpp | 83 +++++++++++- 2 files changed, 205 insertions(+), 4 deletions(-) create mode 100644 src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp diff --git a/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp b/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp new file mode 100644 index 0000000000..28b4319c44 --- /dev/null +++ b/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp @@ -0,0 +1,126 @@ +/*********************************************************************************************************************** +* 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/ComfortViewFactorAngles.hpp" +#include "../../model/Model.hpp" +#include "../../model/Space.hpp" +#include "../../model/Surface.hpp" +#include "../../model/ThermalZone.hpp" + +#include +#include + +using namespace openstudio::energyplus; +using namespace openstudio::model; +using namespace openstudio; + +TEST_F(EnergyPlusFixture, ForwardTranslator_ComfortViewFactorAngles) { + Model model; + ThermalZone zone(model); + Space space(model); + EXPECT_TRUE(space.setThermalZone(zone)); + + Point3dVector points{{0, 0, 0}, {1, 0, 0}, {1, 1, 0}}; + Surface surface(points, model); + surface.setName("Radiant Surface"); + EXPECT_TRUE(surface.setSpace(space)); + + ComfortViewFactorAngles comfortViewFactorAngles(model); + comfortViewFactorAngles.setName("Angle Factors"); + EXPECT_TRUE(comfortViewFactorAngles.addComfortViewFactorAngle(surface, 1.0)); + + ForwardTranslator forwardTranslator; + Workspace workspace = forwardTranslator.translateModelObject(comfortViewFactorAngles); + + const auto angleFactorObjects = workspace.getObjectsByType(IddObjectType::ComfortViewFactorAngles); + ASSERT_EQ(1u, angleFactorObjects.size()); + const auto& angleFactorObject = angleFactorObjects.front(); + EXPECT_EQ("Angle Factors", angleFactorObject.nameString()); + ASSERT_EQ(1u, angleFactorObject.numExtensibleGroups()); + const auto group = angleFactorObject.extensibleGroups().front(); + EXPECT_EQ("Radiant Surface", group.getString(0).get()); + EXPECT_DOUBLE_EQ(1.0, group.getDouble(1).get()); +} + +TEST_F(EnergyPlusFixture, ForwardTranslator_ComfortViewFactorAngles_InvalidSum) { + Model model; + ThermalZone zone(model); + Space space(model); + EXPECT_TRUE(space.setThermalZone(zone)); + + Point3dVector points{{0, 0, 0}, {1, 0, 0}, {1, 1, 0}}; + Surface surface(points, model); + EXPECT_TRUE(surface.setSpace(space)); + + ComfortViewFactorAngles comfortViewFactorAngles(model); + EXPECT_TRUE(comfortViewFactorAngles.addComfortViewFactorAngle(surface, 0.5)); + + ForwardTranslator forwardTranslator; + Workspace workspace = forwardTranslator.translateModelObject(comfortViewFactorAngles); + EXPECT_TRUE(workspace.getObjectsByType(IddObjectType::ComfortViewFactorAngles).empty()); +} + +TEST_F(EnergyPlusFixture, ForwardTranslator_ComfortViewFactorAngles_UntranslatedSurface) { + Model model; + Point3dVector points{{0, 0, 0}, {1, 0, 0}, {1, 1, 0}}; + Surface surface(points, model); + + ComfortViewFactorAngles comfortViewFactorAngles(model); + EXPECT_TRUE(comfortViewFactorAngles.addComfortViewFactorAngle(surface, 1.0)); + + ForwardTranslator forwardTranslator; + Workspace workspace = forwardTranslator.translateModelObject(comfortViewFactorAngles); + EXPECT_TRUE(workspace.getObjectsByType(IddObjectType::ComfortViewFactorAngles).empty()); +} + +TEST_F(EnergyPlusFixture, ReverseTranslator_ComfortViewFactorAngles) { + Workspace workspace(StrictnessLevel::Minimal, IddFileType::EnergyPlus); + + auto zone = workspace.addObject(IdfObject(IddObjectType::Zone)).get(); + zone.setName("Thermal Zone 1"); + + auto surface = workspace.addObject(IdfObject(IddObjectType::BuildingSurface_Detailed)).get(); + surface.setName("Surface 1"); + EXPECT_TRUE(surface.setString(BuildingSurface_DetailedFields::SurfaceType, "Wall")); + EXPECT_TRUE(surface.setString(BuildingSurface_DetailedFields::ConstructionName, "")); + EXPECT_TRUE(surface.setPointer(BuildingSurface_DetailedFields::ZoneName, zone.handle())); + EXPECT_TRUE(surface.setString(BuildingSurface_DetailedFields::OutsideBoundaryCondition, "Outdoors")); + EXPECT_TRUE(surface.setString(BuildingSurface_DetailedFields::OutsideBoundaryConditionObject, "")); + EXPECT_TRUE(surface.setString(BuildingSurface_DetailedFields::SunExposure, "SunExposed")); + EXPECT_TRUE(surface.setString(BuildingSurface_DetailedFields::WindExposure, "WindExposed")); + EXPECT_TRUE(surface.setString(BuildingSurface_DetailedFields::ViewFactortoGround, "")); + EXPECT_TRUE(surface.setString(BuildingSurface_DetailedFields::NumberofVertices, "")); + for (const Point3d& point : Point3dVector{{0, 0, 0}, {1, 0, 0}, {1, 1, 0}}) { + auto group = surface.pushExtensibleGroup(); + group.setDouble(0, point.x()); + group.setDouble(1, point.y()); + group.setDouble(2, point.z()); + } + + auto angleFactorObject = workspace.addObject(IdfObject(IddObjectType::ComfortViewFactorAngles)).get(); + angleFactorObject.setName("Angle Factors"); + auto group = angleFactorObject.pushExtensibleGroup(); + EXPECT_TRUE(group.setPointer(0, surface.handle())); + EXPECT_TRUE(group.setDouble(1, 1.0)); + + ReverseTranslator reverseTranslator; + Model model = reverseTranslator.translateWorkspace(workspace); + + const auto angleFactors = model.getConcreteModelObjects(); + ASSERT_EQ(1u, angleFactors.size()); + const auto& comfortViewFactorAngles = angleFactors.front(); + EXPECT_EQ("Angle Factors", comfortViewFactorAngles.nameString()); + ASSERT_EQ(1u, comfortViewFactorAngles.numberofComfortViewFactorAngles()); + const auto comfortViewFactorAngle = comfortViewFactorAngles.getComfortViewFactorAngle(0); + ASSERT_TRUE(comfortViewFactorAngle); + EXPECT_EQ("Surface 1", comfortViewFactorAngle->surface().nameString()); + EXPECT_DOUBLE_EQ(1.0, comfortViewFactorAngle->angleFactor()); +} \ No newline at end of file diff --git a/src/energyplus/Test/People_GTest.cpp b/src/energyplus/Test/People_GTest.cpp index 3d68348e51..e225db428c 100644 --- a/src/energyplus/Test/People_GTest.cpp +++ b/src/energyplus/Test/People_GTest.cpp @@ -16,9 +16,12 @@ #include "../../model/People_Impl.hpp" #include "../../model/PeopleDefinition.hpp" #include "../../model/PeopleDefinition_Impl.hpp" +#include "../../model/ComfortViewFactorAngles.hpp" #include "../../model/ScheduleConstant.hpp" +#include "../../model/Surface.hpp" #include +#include #include #include @@ -118,7 +121,7 @@ TEST_F(EnergyPlusFixture, ForwardTranslator_People) { EXPECT_EQ("ActivitySchedule", peopleObject.getString(PeopleFields::ActivityLevelScheduleName).get()); EXPECT_EQ(3.6e-08, peopleObject.getDouble(PeopleFields::CarbonDioxideGenerationRate).get()); EXPECT_EQ("Yes", peopleObject.getString(PeopleFields::EnableASHRAE55ComfortWarnings).get()); - EXPECT_EQ("SurfaceWeighted", peopleObject.getString(PeopleFields::MeanRadiantTemperatureCalculationType).get()); + EXPECT_EQ("EnclosureAveraged", peopleObject.getString(PeopleFields::MeanRadiantTemperatureCalculationType).get()); EXPECT_TRUE(peopleObject.isEmpty(PeopleFields::SurfaceName_AngleFactorListName)); EXPECT_EQ("WorkEfficiencySchedule", peopleObject.getString(PeopleFields::WorkEfficiencyScheduleName).get()); @@ -135,6 +138,70 @@ TEST_F(EnergyPlusFixture, ForwardTranslator_People) { EXPECT_EQ(31.0, peopleObject.getDouble(PeopleFields::HeatStressTemperatureThreshold).get()); } +TEST_F(EnergyPlusFixture, ForwardTranslator_People_AngleFactor) { + Model model; + ThermalZone zone(model); + Space space(model); + EXPECT_TRUE(space.setThermalZone(zone)); + + Point3dVector points{{0, 0, 0}, {1, 0, 0}, {1, 1, 0}}; + Surface surface(points, model); + EXPECT_TRUE(surface.setSpace(space)); + + ComfortViewFactorAngles comfortViewFactorAngles(model); + comfortViewFactorAngles.setName("Angle Factors"); + EXPECT_TRUE(comfortViewFactorAngles.addComfortViewFactorAngle(surface, 1.0)); + + PeopleDefinition definition(model); + EXPECT_TRUE(definition.setSurfaceNameAngleFactorListName(comfortViewFactorAngles)); + People people(definition); + EXPECT_TRUE(people.setSpace(space)); + + ForwardTranslator forwardTranslator; + Workspace workspace = forwardTranslator.translateModel(model); + + const auto peopleObjects = workspace.getObjectsByType(IddObjectType::People); + ASSERT_EQ(1u, peopleObjects.size()); + const auto& peopleObject = peopleObjects.front(); + EXPECT_EQ("AngleFactor", peopleObject.getString(PeopleFields::MeanRadiantTemperatureCalculationType).get()); + EXPECT_EQ("Angle Factors", peopleObject.getString(PeopleFields::SurfaceName_AngleFactorListName).get()); + + const auto angleFactorObjects = workspace.getObjectsByType(IddObjectType::ComfortViewFactorAngles); + ASSERT_EQ(1u, angleFactorObjects.size()); + const auto& angleFactorObject = angleFactorObjects.front(); + EXPECT_EQ("Angle Factors", angleFactorObject.nameString()); + ASSERT_EQ(1u, angleFactorObject.numExtensibleGroups()); + const auto group = angleFactorObject.extensibleGroups().front(); + EXPECT_EQ(surface.nameString(), group.getString(0).get()); + EXPECT_DOUBLE_EQ(1.0, group.getDouble(1).get()); +} + +TEST_F(EnergyPlusFixture, ForwardTranslator_People_SurfaceWeighted) { + Model model; + ThermalZone zone(model); + Space space(model); + EXPECT_TRUE(space.setThermalZone(zone)); + + Point3dVector points{{0, 0, 0}, {1, 0, 0}, {1, 1, 0}}; + Surface surface(points, model); + surface.setName("Radiant Surface"); + EXPECT_TRUE(surface.setSpace(space)); + + PeopleDefinition definition(model); + EXPECT_TRUE(definition.setSurfaceNameAngleFactorListName(surface)); + People people(definition); + EXPECT_TRUE(people.setSpace(space)); + + ForwardTranslator forwardTranslator; + Workspace workspace = forwardTranslator.translateModel(model); + + const auto peopleObjects = workspace.getObjectsByType(IddObjectType::People); + ASSERT_EQ(1u, peopleObjects.size()); + const auto& peopleObject = peopleObjects.front(); + EXPECT_EQ("SurfaceWeighted", peopleObject.getString(PeopleFields::MeanRadiantTemperatureCalculationType).get()); + EXPECT_EQ("Radiant Surface", peopleObject.getString(PeopleFields::SurfaceName_AngleFactorListName).get()); +} + TEST_F(EnergyPlusFixture, ReverseTranslator_People) { ReverseTranslator rt; @@ -148,6 +215,10 @@ TEST_F(EnergyPlusFixture, ReverseTranslator_People) { OptionalWorkspaceObject _i_people = w.addObject(IdfObject(IddObjectType::People)); ASSERT_TRUE(_i_people); + OptionalWorkspaceObject _i_comfortViewFactorAngles = w.addObject(IdfObject(IddObjectType::ComfortViewFactorAngles)); + ASSERT_TRUE(_i_comfortViewFactorAngles); + _i_comfortViewFactorAngles->setName("Angle Factors"); + EXPECT_TRUE(_i_people->setPointer(PeopleFields::ZoneorZoneListorSpaceorSpaceListName, _i_zone->handle())); auto assignSchedule = [&_i_people, &w](unsigned index, const std::string& scheduleName) { @@ -169,8 +240,8 @@ TEST_F(EnergyPlusFixture, ReverseTranslator_People) { assignSchedule(PeopleFields::ActivityLevelScheduleName, "ActivitySchedule"); EXPECT_TRUE(_i_people->setDouble(PeopleFields::CarbonDioxideGenerationRate, 3.6e-08)); EXPECT_TRUE(_i_people->setString(PeopleFields::EnableASHRAE55ComfortWarnings, "Yes")); - EXPECT_TRUE(_i_people->setString(PeopleFields::MeanRadiantTemperatureCalculationType, "SurfaceWeighted")); - EXPECT_TRUE(_i_people->setString(PeopleFields::SurfaceName_AngleFactorListName, "")); + EXPECT_TRUE(_i_people->setString(PeopleFields::MeanRadiantTemperatureCalculationType, "AngleFactor")); + EXPECT_TRUE(_i_people->setPointer(PeopleFields::SurfaceName_AngleFactorListName, _i_comfortViewFactorAngles->handle())); assignSchedule(PeopleFields::WorkEfficiencyScheduleName, "WorkEfficiencySchedule"); EXPECT_TRUE(_i_people->setString(PeopleFields::ClothingInsulationCalculationMethod, "ClothingInsulationSchedule")); EXPECT_TRUE(_i_people->setString(PeopleFields::ClothingInsulationCalculationMethodScheduleName, "")); @@ -218,6 +289,10 @@ TEST_F(EnergyPlusFixture, ReverseTranslator_People) { auto pd = p.definition().cast(); EXPECT_EQ("Area/Person", pd.numberofPeopleCalculationMethod()); + EXPECT_EQ("AngleFactor", pd.meanRadiantTemperatureCalculationType()); + auto mrtTarget = pd.surfaceNameAngleFactorListName(); + ASSERT_TRUE(mrtTarget); + EXPECT_EQ("Angle Factors", mrtTarget->nameString()); EXPECT_FALSE(pd.numberofPeople()); EXPECT_FALSE(pd.peopleperSpaceFloorArea()); ASSERT_TRUE(pd.spaceFloorAreaperPerson()); @@ -227,7 +302,7 @@ TEST_F(EnergyPlusFixture, ReverseTranslator_People) { EXPECT_EQ(0.2, pd.sensibleHeatFraction().get()); EXPECT_EQ(3.6e-08, pd.carbonDioxideGenerationRate()); EXPECT_TRUE(pd.enableASHRAE55ComfortWarnings()); - EXPECT_EQ("SurfaceWeighted", pd.meanRadiantTemperatureCalculationType()); + EXPECT_EQ("AngleFactor", pd.meanRadiantTemperatureCalculationType()); ASSERT_EQ(2, pd.numThermalComfortModelTypes()); ASSERT_EQ(2, pd.numExtensibleGroups()); From eb7fe45f7a7334c6d6959fb7def79aed83e8af14 Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Tue, 8 Sep 2026 10:29:29 -0700 Subject: [PATCH 07/20] Register ComfortViewFactorAngles translation --- src/energyplus/CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/energyplus/CMakeLists.txt b/src/energyplus/CMakeLists.txt index 70ddec8be4..fa9be6c184 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/ForwardTranslateComfortViewFactorAngles.cpp ForwardTranslator/ForwardTranslateZoneMRTCalculation.cpp ForwardTranslator/ForwardTranslateZonePropertyUserViewFactorsBySurfaceName.cpp ForwardTranslator/ForwardTranslateZoneVentilationDesignFlowRate.cpp @@ -651,6 +652,7 @@ set(${target_name}_src ReverseTranslator/ReverseTranslateZoneInfiltrationFlowCoefficient.cpp ReverseTranslator/ReverseTranslateZoneList.cpp ReverseTranslator/ReverseTranslateZoneMixing.cpp + ReverseTranslator/ReverseTranslateComfortViewFactorAngles.cpp ReverseTranslator/ReverseTranslateZoneMRTCalculation.cpp ReverseTranslator/ReverseTranslateZonePropertyUserViewFactorsBySurfaceName.cpp ReverseTranslator/ReverseTranslateZoneVentilationDesignFlowRate.cpp @@ -737,6 +739,7 @@ set(${target_name}_test_src Test/CoilWaterHeatingAirToWaterHeatPumpWrapped_GTest.cpp Test/CoilWaterHeatingAirToWaterHeatPumpVariableSpeed_GTest.cpp Test/CoilWaterHeatingDesuperheater_GTest.cpp + Test/ComfortViewFactorAngles_GTest.cpp Test/Construction_GTest.cpp Test/ConstructionWithInternalSource_GTest.cpp Test/ControllerOutdoorAir_GTest.cpp From 96adc29d9bef98b5a543ebba278022c9f975f12e Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Tue, 8 Sep 2026 13:04:35 -0700 Subject: [PATCH 08/20] Clean up angle factor name. --- ...everseTranslateComfortViewFactorAngles.cpp | 2 +- .../Test/ComfortViewFactorAngles_GTest.cpp | 6 +-- src/energyplus/Test/People_GTest.cpp | 2 +- src/model/ComfortViewFactorAngles.cpp | 49 ++++++++++++------- src/model/ComfortViewFactorAngles.hpp | 19 ++++--- src/model/ComfortViewFactorAngles_Impl.hpp | 10 ++-- src/model/ConcreteModelObjects.hpp | 3 +- src/model/Model.cpp | 3 +- src/model/ModelGeometry.i | 11 ++++- src/model/PeopleDefinition.cpp | 8 +-- .../test/ComfortViewFactorAngles_GTest.cpp | 12 +++-- src/model/test/People_GTest.cpp | 2 +- 12 files changed, 80 insertions(+), 47 deletions(-) diff --git a/src/energyplus/ReverseTranslator/ReverseTranslateComfortViewFactorAngles.cpp b/src/energyplus/ReverseTranslator/ReverseTranslateComfortViewFactorAngles.cpp index c1f7198dd5..7c24fd8f83 100644 --- a/src/energyplus/ReverseTranslator/ReverseTranslateComfortViewFactorAngles.cpp +++ b/src/energyplus/ReverseTranslator/ReverseTranslateComfortViewFactorAngles.cpp @@ -37,7 +37,7 @@ namespace energyplus { } if (auto modelObject = translateAndMapWorkspaceObject(*surfaceTarget)) { if (auto surface = modelObject->optionalCast()) { - comfortViewFactorAngles.addComfortViewFactorAngle(*surface, *angleFactor); + comfortViewFactorAngles.addAngleFactor(*surface, *angleFactor); } } } diff --git a/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp b/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp index 28b4319c44..43a25c6ac0 100644 --- a/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp +++ b/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp @@ -35,7 +35,7 @@ TEST_F(EnergyPlusFixture, ForwardTranslator_ComfortViewFactorAngles) { ComfortViewFactorAngles comfortViewFactorAngles(model); comfortViewFactorAngles.setName("Angle Factors"); - EXPECT_TRUE(comfortViewFactorAngles.addComfortViewFactorAngle(surface, 1.0)); + EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(surface, 1.0)); ForwardTranslator forwardTranslator; Workspace workspace = forwardTranslator.translateModelObject(comfortViewFactorAngles); @@ -61,7 +61,7 @@ TEST_F(EnergyPlusFixture, ForwardTranslator_ComfortViewFactorAngles_InvalidSum) EXPECT_TRUE(surface.setSpace(space)); ComfortViewFactorAngles comfortViewFactorAngles(model); - EXPECT_TRUE(comfortViewFactorAngles.addComfortViewFactorAngle(surface, 0.5)); + EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(surface, 0.5)); ForwardTranslator forwardTranslator; Workspace workspace = forwardTranslator.translateModelObject(comfortViewFactorAngles); @@ -74,7 +74,7 @@ TEST_F(EnergyPlusFixture, ForwardTranslator_ComfortViewFactorAngles_Untranslated Surface surface(points, model); ComfortViewFactorAngles comfortViewFactorAngles(model); - EXPECT_TRUE(comfortViewFactorAngles.addComfortViewFactorAngle(surface, 1.0)); + EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(surface, 1.0)); ForwardTranslator forwardTranslator; Workspace workspace = forwardTranslator.translateModelObject(comfortViewFactorAngles); diff --git a/src/energyplus/Test/People_GTest.cpp b/src/energyplus/Test/People_GTest.cpp index e225db428c..d776e65c51 100644 --- a/src/energyplus/Test/People_GTest.cpp +++ b/src/energyplus/Test/People_GTest.cpp @@ -150,7 +150,7 @@ TEST_F(EnergyPlusFixture, ForwardTranslator_People_AngleFactor) { ComfortViewFactorAngles comfortViewFactorAngles(model); comfortViewFactorAngles.setName("Angle Factors"); - EXPECT_TRUE(comfortViewFactorAngles.addComfortViewFactorAngle(surface, 1.0)); + EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(surface, 1.0)); PeopleDefinition definition(model); EXPECT_TRUE(definition.setSurfaceNameAngleFactorListName(comfortViewFactorAngles)); diff --git a/src/model/ComfortViewFactorAngles.cpp b/src/model/ComfortViewFactorAngles.cpp index 3f311c0295..15ea31d08f 100644 --- a/src/model/ComfortViewFactorAngles.cpp +++ b/src/model/ComfortViewFactorAngles.cpp @@ -7,6 +7,8 @@ #include "ComfortViewFactorAngles_Impl.hpp" #include "Model.hpp" +#include "Model_Impl.hpp" + #include "ModelExtensibleGroup.hpp" #include @@ -17,20 +19,29 @@ namespace openstudio { namespace model { - ComfortViewFactorAngle::ComfortViewFactorAngle(const Surface& surface, double angleFactor) : m_surface(surface), m_angleFactor(angleFactor) { + AngleFactor::AngleFactor(const Surface& surface, double angleFactor) : m_surface(surface), m_angleFactor(angleFactor) { if ((m_angleFactor < 0.0) || (m_angleFactor > 1.0)) { LOG_AND_THROW("Angle Factor must be between 0 and 1."); } } - Surface ComfortViewFactorAngle::surface() const { + Surface AngleFactor::surface() const { return m_surface; } - double ComfortViewFactorAngle::angleFactor() const { + double AngleFactor::angleFactor() const { return m_angleFactor; } + bool AngleFactor::operator!=(const AngleFactor& other) const { + return !operator==(other); + } + + std::ostream& operator<<(std::ostream& out, const openstudio::model::AngleFactor& angleFactor) { + out << "(surface='" << angleFactor.surface().nameString() << "', angle factor=" << angleFactor.angleFactor() << ")"; + return out; + } + namespace detail { ComfortViewFactorAngles_Impl::ComfortViewFactorAngles_Impl(const IdfObject& idfObject, Model_Impl* model, bool keepHandle) @@ -56,8 +67,8 @@ namespace model { return ComfortViewFactorAngles::iddObjectType(); } - std::vector ComfortViewFactorAngles_Impl::comfortViewFactorAngles() const { - std::vector result; + std::vector ComfortViewFactorAngles_Impl::comfortViewFactorAngles() const { + std::vector result; for (unsigned i = 0; i < numberofComfortViewFactorAngles(); ++i) { if (auto comfortViewFactorAngle = getComfortViewFactorAngle(i)) { result.push_back(*comfortViewFactorAngle); @@ -70,7 +81,7 @@ namespace model { return numExtensibleGroups(); } - boost::optional ComfortViewFactorAngles_Impl::getComfortViewFactorAngle(unsigned groupIndex) const { + boost::optional ComfortViewFactorAngles_Impl::getComfortViewFactorAngle(unsigned groupIndex) const { if (groupIndex >= numberofComfortViewFactorAngles()) { return boost::none; } @@ -78,19 +89,19 @@ namespace model { auto surface = group.getModelObjectTarget(OS_ComfortViewFactorAnglesExtensibleFields::SurfaceName); auto angleFactor = group.getDouble(OS_ComfortViewFactorAnglesExtensibleFields::AngleFactor); if (surface && angleFactor) { - return ComfortViewFactorAngle(*surface, *angleFactor); + return AngleFactor(*surface, *angleFactor); } return boost::none; } - bool ComfortViewFactorAngles_Impl::addComfortViewFactorAngle(const ComfortViewFactorAngle& comfortViewFactorAngle) { - if (comfortViewFactorAngle.surface().model() != model()) { + bool ComfortViewFactorAngles_Impl::addAngleFactor(const AngleFactor& angleFactor) { + if (angleFactor.surface().model() != model()) { LOG(Error, "Cannot add a Surface from another Model to " << briefDescription() << "."); return false; } auto group = pushExtensibleGroup({}, false).cast(); - bool surfaceSet = group.setPointer(OS_ComfortViewFactorAnglesExtensibleFields::SurfaceName, comfortViewFactorAngle.surface().handle(), false); - bool angleFactorSet = group.setDouble(OS_ComfortViewFactorAnglesExtensibleFields::AngleFactor, comfortViewFactorAngle.angleFactor()); + bool surfaceSet = group.setPointer(OS_ComfortViewFactorAnglesExtensibleFields::SurfaceName, angleFactor.surface().handle(), false); + bool angleFactorSet = group.setDouble(OS_ComfortViewFactorAnglesExtensibleFields::AngleFactor, angleFactor.angleFactor()); if (surfaceSet && angleFactorSet) { return true; } @@ -98,8 +109,8 @@ namespace model { return false; } - bool ComfortViewFactorAngles_Impl::addComfortViewFactorAngle(const Surface& surface, double angleFactor) { - return addComfortViewFactorAngle(ComfortViewFactorAngle(surface, angleFactor)); + bool ComfortViewFactorAngles_Impl::addAngleFactor(const Surface& surface, double angleFactor) { + return addAngleFactor(AngleFactor(surface, angleFactor)); } bool ComfortViewFactorAngles_Impl::removeComfortViewFactorAngle(unsigned groupIndex) { @@ -124,7 +135,7 @@ namespace model { return IddObjectType::OS_ComfortViewFactorAngles; } - std::vector ComfortViewFactorAngles::comfortViewFactorAngles() const { + std::vector ComfortViewFactorAngles::comfortViewFactorAngles() const { return getImpl()->comfortViewFactorAngles(); } @@ -132,16 +143,16 @@ namespace model { return getImpl()->numberofComfortViewFactorAngles(); } - boost::optional ComfortViewFactorAngles::getComfortViewFactorAngle(unsigned groupIndex) const { + boost::optional ComfortViewFactorAngles::getComfortViewFactorAngle(unsigned groupIndex) const { return getImpl()->getComfortViewFactorAngle(groupIndex); } - bool ComfortViewFactorAngles::addComfortViewFactorAngle(const ComfortViewFactorAngle& comfortViewFactorAngle) { - return getImpl()->addComfortViewFactorAngle(comfortViewFactorAngle); + bool ComfortViewFactorAngles::addAngleFactor(const AngleFactor& angleFactor) { + return getImpl()->addAngleFactor(angleFactor); } - bool ComfortViewFactorAngles::addComfortViewFactorAngle(const Surface& surface, double angleFactor) { - return getImpl()->addComfortViewFactorAngle(surface, angleFactor); + bool ComfortViewFactorAngles::addAngleFactor(const Surface& surface, double angleFactor) { + return getImpl()->addAngleFactor(surface, angleFactor); } void ComfortViewFactorAngles::removeComfortViewFactorAngle(int groupIndex) { diff --git a/src/model/ComfortViewFactorAngles.hpp b/src/model/ComfortViewFactorAngles.hpp index e637318a2e..b73260fb8e 100644 --- a/src/model/ComfortViewFactorAngles.hpp +++ b/src/model/ComfortViewFactorAngles.hpp @@ -17,19 +17,26 @@ namespace model { class ComfortViewFactorAngles_Impl; } - class MODEL_API ComfortViewFactorAngle + class MODEL_API AngleFactor { public: - ComfortViewFactorAngle(const Surface& surface, double angleFactor); + AngleFactor(const Surface& surface, double angleFactor); Surface surface() const; double angleFactor() const; + bool operator==(const AngleFactor& other) const = default; + bool operator!=(const AngleFactor& other) const; + private: Surface m_surface; double m_angleFactor; + REGISTER_LOGGER("openstudio.model.AngleFactor"); }; + /** \relates AngleFactor */ + MODEL_API std::ostream& operator<<(std::ostream& out, const openstudio::model::AngleFactor& angleFactor); + class MODEL_API ComfortViewFactorAngles : public ModelObject { public: @@ -39,12 +46,12 @@ namespace model { static IddObjectType iddObjectType(); - std::vector comfortViewFactorAngles() const; + std::vector comfortViewFactorAngles() const; unsigned int numberofComfortViewFactorAngles() const; - boost::optional getComfortViewFactorAngle(unsigned groupIndex) const; + boost::optional getComfortViewFactorAngle(unsigned groupIndex) const; - bool addComfortViewFactorAngle(const ComfortViewFactorAngle& comfortViewFactorAngle); - bool addComfortViewFactorAngle(const Surface& surface, double angleFactor); + bool addAngleFactor(const AngleFactor& angleFactor); + bool addAngleFactor(const Surface& surface, double angleFactor); void removeComfortViewFactorAngle(int groupIndex); void removeAllComfortViewFactorAngles(); diff --git a/src/model/ComfortViewFactorAngles_Impl.hpp b/src/model/ComfortViewFactorAngles_Impl.hpp index 7bffb64eb0..d509d7e085 100644 --- a/src/model/ComfortViewFactorAngles_Impl.hpp +++ b/src/model/ComfortViewFactorAngles_Impl.hpp @@ -12,7 +12,7 @@ namespace openstudio { namespace model { - class ComfortViewFactorAngle; + class AngleFactor; class Surface; namespace detail { @@ -28,12 +28,12 @@ namespace model { virtual const std::vector& outputVariableNames() const override; virtual IddObjectType iddObjectType() const override; - std::vector comfortViewFactorAngles() const; + std::vector comfortViewFactorAngles() const; unsigned int numberofComfortViewFactorAngles() const; - boost::optional getComfortViewFactorAngle(unsigned groupIndex) const; + boost::optional getComfortViewFactorAngle(unsigned groupIndex) const; - bool addComfortViewFactorAngle(const ComfortViewFactorAngle& comfortViewFactorAngle); - bool addComfortViewFactorAngle(const Surface& surface, double angleFactor); + bool addAngleFactor(const AngleFactor& angleFactor); + bool addAngleFactor(const Surface& surface, double angleFactor); bool removeComfortViewFactorAngle(unsigned groupIndex); void removeAllComfortViewFactorAngles(); }; diff --git a/src/model/ConcreteModelObjects.hpp b/src/model/ConcreteModelObjects.hpp index 2f74bfab1e..2191203e12 100644 --- a/src/model/ConcreteModelObjects.hpp +++ b/src/model/ConcreteModelObjects.hpp @@ -145,6 +145,7 @@ #include "CoilWaterHeatingAirToWaterHeatPumpVariableSpeedSpeedData.hpp" #include "CoilWaterHeatingAirToWaterHeatPumpWrapped.hpp" #include "CoilWaterHeatingDesuperheater.hpp" +#include "ComfortViewFactorAngles.hpp" #include "ComponentCostAdjustments.hpp" #include "ComponentData.hpp" #include "Connection.hpp" @@ -565,7 +566,6 @@ #include "ZoneHVACUnitVentilator.hpp" #include "ZoneHVACWaterToAirHeatPump.hpp" #include "ZoneMixing.hpp" -#include "ComfortViewFactorAngles.hpp" #include "ZoneMRTCalculation.hpp" #include "ZonePropertyUserViewFactorsBySurfaceName.hpp" #include "ZoneVentilationDesignFlowRate.hpp" @@ -711,6 +711,7 @@ #include "CoilWaterHeatingAirToWaterHeatPumpVariableSpeedSpeedData_Impl.hpp" #include "CoilWaterHeatingAirToWaterHeatPumpWrapped_Impl.hpp" #include "CoilWaterHeatingDesuperheater_Impl.hpp" +#include "ComfortViewFactorAngles_Impl.hpp" #include "ComponentCostAdjustments_Impl.hpp" #include "ComponentData_Impl.hpp" #include "Connection_Impl.hpp" diff --git a/src/model/Model.cpp b/src/model/Model.cpp index 33098bfa30..6215c9e0a0 100644 --- a/src/model/Model.cpp +++ b/src/model/Model.cpp @@ -4048,6 +4048,7 @@ namespace model { REGISTER_CONSTRUCTOR(CoilWaterHeatingAirToWaterHeatPumpVariableSpeedSpeedData); REGISTER_CONSTRUCTOR(CoilWaterHeatingAirToWaterHeatPumpWrapped); REGISTER_CONSTRUCTOR(CoilWaterHeatingDesuperheater); + REGISTER_CONSTRUCTOR(ComfortViewFactorAngles); REGISTER_CONSTRUCTOR(ComponentCostAdjustments); REGISTER_CONSTRUCTOR(ComponentData); REGISTER_CONSTRUCTOR(Connection); @@ -4479,7 +4480,6 @@ namespace model { REGISTER_CONSTRUCTOR(ZoneHVACEnergyRecoveryVentilator); REGISTER_CONSTRUCTOR(ZoneHVACUnitVentilator); REGISTER_CONSTRUCTOR(ZoneMixing); - REGISTER_CONSTRUCTOR(ComfortViewFactorAngles); REGISTER_CONSTRUCTOR(ZoneMRTCalculation); REGISTER_CONSTRUCTOR(ZonePropertyUserViewFactorsBySurfaceName); REGISTER_CONSTRUCTOR(ZoneVentilationDesignFlowRate); @@ -4634,6 +4634,7 @@ namespace model { REGISTER_COPYCONSTRUCTORS(CoilWaterHeatingAirToWaterHeatPumpVariableSpeedSpeedData); REGISTER_COPYCONSTRUCTORS(CoilWaterHeatingAirToWaterHeatPumpWrapped); REGISTER_COPYCONSTRUCTORS(CoilWaterHeatingDesuperheater); + REGISTER_COPYCONSTRUCTORS(ComfortViewFactorAngles); REGISTER_COPYCONSTRUCTORS(ComponentCostAdjustments); REGISTER_COPYCONSTRUCTORS(ComponentData); REGISTER_COPYCONSTRUCTORS(Connection); diff --git a/src/model/ModelGeometry.i b/src/model/ModelGeometry.i index 8e8d2ab426..844fc60fd8 100644 --- a/src/model/ModelGeometry.i +++ b/src/model/ModelGeometry.i @@ -199,6 +199,15 @@ class ExteriorLoadInstance; } }; +%extend openstudio::model::AngleFactor { + // Use the overloaded operator<< for string representation + std::string __str__() { + std::ostringstream os; + os << *$self; + return os.str(); + } +}; + UNIQUEMODELOBJECT_TEMPLATES(Site); UNIQUEMODELOBJECT_TEMPLATES(Facility); UNIQUEMODELOBJECT_TEMPLATES(Building); @@ -254,7 +263,7 @@ MODELOBJECT_TEMPLATES(FoundationKiva); MODELOBJECT_TEMPLATES(SurfacePropertyExposedFoundationPerimeter); MODELOBJECT_TEMPLATES(ViewFactor); // Helper class defined in ZonePropertyUserViewFactorsBySurfaceName MODELOBJECT_TEMPLATES(ZonePropertyUserViewFactorsBySurfaceName); -MODELOBJECT_TEMPLATES(ComfortViewFactorAngle); +MODELOBJECT_TEMPLATES(AngleFactor); MODELOBJECT_TEMPLATES(ComfortViewFactorAngles); MODELOBJECT_TEMPLATES(MRTWeightingFactor); // Helper class defined in ZoneMRTCalculation MODELOBJECT_TEMPLATES(ZoneMRTCalculation); diff --git a/src/model/PeopleDefinition.cpp b/src/model/PeopleDefinition.cpp index e0cd8579c1..370c76964b 100644 --- a/src/model/PeopleDefinition.cpp +++ b/src/model/PeopleDefinition.cpp @@ -7,7 +7,9 @@ #include "PeopleDefinition_Impl.hpp" #include "ComfortViewFactorAngles.hpp" +#include "ComfortViewFactorAngles_Impl.hpp" #include "Surface.hpp" +#include "Surface_Impl.hpp" #include "../utilities/idf/IdfExtensibleGroup.hpp" @@ -133,7 +135,7 @@ namespace model { } boost::optional PeopleDefinition_Impl::surfaceNameAngleFactorListName() const { - return getObject().getModelObjectTarget(OS_People_DefinitionFields::SurfaceNameAngleFactorListName); + return getObject().getModelObjectTarget(OS_People_DefinitionFields::SurfaceName_AngleFactorListName); } boost::optional PeopleDefinition_Impl::getThermalComfortModelType(int i) const { @@ -287,11 +289,11 @@ namespace model { if (!setMeanRadiantTemperatureCalculationType(mrtType)) { return false; } - return setPointer(OS_People_DefinitionFields::SurfaceNameAngleFactorListName, modelObject.handle()); + return setPointer(OS_People_DefinitionFields::SurfaceName_AngleFactorListName, modelObject.handle()); } void PeopleDefinition_Impl::resetSurfaceNameAngleFactorListName() { - bool result = setString(OS_People_DefinitionFields::SurfaceNameAngleFactorListName, ""); + bool result = setString(OS_People_DefinitionFields::SurfaceName_AngleFactorListName, ""); OS_ASSERT(result); } diff --git a/src/model/test/ComfortViewFactorAngles_GTest.cpp b/src/model/test/ComfortViewFactorAngles_GTest.cpp index 669a32dffc..0053e65bb2 100644 --- a/src/model/test/ComfortViewFactorAngles_GTest.cpp +++ b/src/model/test/ComfortViewFactorAngles_GTest.cpp @@ -21,21 +21,23 @@ TEST_F(ModelFixture, ComfortViewFactorAngles) { Surface surface(points, model); ComfortViewFactorAngles comfortViewFactorAngles(model); - EXPECT_THROW(ComfortViewFactorAngle(surface, -0.01), openstudio::Exception); - EXPECT_THROW(ComfortViewFactorAngle(surface, 1.01), openstudio::Exception); + EXPECT_THROW(AngleFactor(surface, -0.01), openstudio::Exception); + EXPECT_THROW(AngleFactor(surface, 1.01), openstudio::Exception); - EXPECT_TRUE(comfortViewFactorAngles.addComfortViewFactorAngle(surface, 0.25)); + EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(surface, 0.25)); EXPECT_EQ(1u, comfortViewFactorAngles.numberofComfortViewFactorAngles()); auto comfortViewFactorAngle = comfortViewFactorAngles.getComfortViewFactorAngle(0); ASSERT_TRUE(comfortViewFactorAngle); EXPECT_EQ(surface.handle(), comfortViewFactorAngle->surface().handle()); EXPECT_DOUBLE_EQ(0.25, comfortViewFactorAngle->angleFactor()); + EXPECT_EQ(AngleFactor(surface, 0.25), *comfortViewFactorAngle); + EXPECT_NE(AngleFactor(surface, 0.5), *comfortViewFactorAngle); EXPECT_FALSE(comfortViewFactorAngles.getComfortViewFactorAngle(1)); Model otherModel; Surface otherSurface(points, otherModel); - EXPECT_FALSE(comfortViewFactorAngles.addComfortViewFactorAngle(otherSurface, 0.75)); + EXPECT_FALSE(comfortViewFactorAngles.addAngleFactor(otherSurface, 0.75)); EXPECT_EQ(1u, comfortViewFactorAngles.numberofComfortViewFactorAngles()); PeopleDefinition peopleDefinition(model); @@ -46,7 +48,7 @@ TEST_F(ModelFixture, ComfortViewFactorAngles) { comfortViewFactorAngles.removeComfortViewFactorAngle(0); EXPECT_EQ(0u, comfortViewFactorAngles.numberofComfortViewFactorAngles()); - EXPECT_TRUE(comfortViewFactorAngles.addComfortViewFactorAngle(surface, 1.0)); + EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(surface, 1.0)); comfortViewFactorAngles.removeAllComfortViewFactorAngles(); EXPECT_TRUE(comfortViewFactorAngles.comfortViewFactorAngles().empty()); } diff --git a/src/model/test/People_GTest.cpp b/src/model/test/People_GTest.cpp index 6cb17b87a2..69da6ef5fe 100644 --- a/src/model/test/People_GTest.cpp +++ b/src/model/test/People_GTest.cpp @@ -54,7 +54,7 @@ TEST_F(ModelFixture, People_DefaultConstructor) { Surface surface(points, model); ComfortViewFactorAngles comfortViewFactorAngles(model); - EXPECT_TRUE(comfortViewFactorAngles.addComfortViewFactorAngle(surface, 1.0)); + EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(surface, 1.0)); EXPECT_EQ(1u, comfortViewFactorAngles.numberofComfortViewFactorAngles()); auto comfortViewFactorAngle = comfortViewFactorAngles.getComfortViewFactorAngle(0); ASSERT_TRUE(comfortViewFactorAngle); From d536120c8622c8a5a7a5f76db02201c1a0ae0940 Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Tue, 8 Sep 2026 14:03:55 -0700 Subject: [PATCH 09/20] Update ComfortViewFactorAngles schema --- resources/model/OpenStudio.idd | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/resources/model/OpenStudio.idd b/resources/model/OpenStudio.idd index 938fb8dc9b..d063f3bc2b 100644 --- a/resources/model/OpenStudio.idd +++ b/resources/model/OpenStudio.idd @@ -3976,22 +3976,25 @@ OS:People:Definition, \key AnkleDraftASH55 OS:ComfortViewFactorAngles, + \min-fields 4 \extensible:2 - A1, \field Handle + A1, \field Handle \type handle \required-field - A2, \field Name + A2, \field Name \type alpha \required-field \reference AllHeatTranAngFacNames - A3, \field Surface 1 Name + A3, \field Surface 1 Name \type object-list \object-list SurfaceNames \begin-extensible - N1; \field Angle Factor 1 + \required-field + N1; \field Angle Factor 1 \type real \minimum 0.0 \maximum 1.0 + \required-field OS:Lights:Definition, \min-fields 1 From 75bd4dde4b48e18dd3606f1e5afed76ada5ddcb6 Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Tue, 8 Sep 2026 14:04:10 -0700 Subject: [PATCH 10/20] Refine ComfortViewFactorAngles model API --- src/model/ComfortViewFactorAngles.cpp | 110 ++++++++++++++---- src/model/ComfortViewFactorAngles.hpp | 14 ++- src/model/ComfortViewFactorAngles_Impl.hpp | 20 +++- src/model/ModelGeometry.i | 3 +- .../test/ComfortViewFactorAngles_GTest.cpp | 51 +++++--- src/model/test/People_GTest.cpp | 12 +- 6 files changed, 153 insertions(+), 57 deletions(-) diff --git a/src/model/ComfortViewFactorAngles.cpp b/src/model/ComfortViewFactorAngles.cpp index 15ea31d08f..f47abd9bd8 100644 --- a/src/model/ComfortViewFactorAngles.cpp +++ b/src/model/ComfortViewFactorAngles.cpp @@ -6,6 +6,8 @@ #include "ComfortViewFactorAngles.hpp" #include "ComfortViewFactorAngles_Impl.hpp" +#include "Surface.hpp" +#include "Surface_Impl.hpp" #include "Model.hpp" #include "Model_Impl.hpp" @@ -67,27 +69,63 @@ namespace model { return ComfortViewFactorAngles::iddObjectType(); } - std::vector ComfortViewFactorAngles_Impl::comfortViewFactorAngles() const { + std::vector ComfortViewFactorAngles_Impl::angleFactors() const { std::vector result; - for (unsigned i = 0; i < numberofComfortViewFactorAngles(); ++i) { - if (auto comfortViewFactorAngle = getComfortViewFactorAngle(i)) { - result.push_back(*comfortViewFactorAngle); + for (unsigned i = 0; i < numberofAngleFactors(); ++i) { + if (auto angleFactor = getAngleFactor(i)) { + result.push_back(*angleFactor); } } return result; } - unsigned int ComfortViewFactorAngles_Impl::numberofComfortViewFactorAngles() const { + unsigned int ComfortViewFactorAngles_Impl::numberofAngleFactors() const { return numExtensibleGroups(); } - boost::optional ComfortViewFactorAngles_Impl::getComfortViewFactorAngle(unsigned groupIndex) const { - if (groupIndex >= numberofComfortViewFactorAngles()) { + boost::optional ComfortViewFactorAngles_Impl::angleFactorIndex(const Surface& surface) const { + for (unsigned i = 0; i < numberofAngleFactors(); ++i) { + if (auto existingSurface = getSurface(i); existingSurface && (existingSurface->handle() == surface.handle())) { + return i; + } + } + return boost::none; + } + + boost::optional ComfortViewFactorAngles_Impl::getSurface(unsigned groupIndex) const { + boost::optional result; + + if (groupIndex >= numberofAngleFactors()) { + LOG(Error, "Asked to get Surface with index " << groupIndex << ", but " << briefDescription() << " has just " << numberofAngleFactors() + << " angle factors."); + return result; + } + auto group = getExtensibleGroup(groupIndex).cast(); + result = group.getModelObjectTarget(OS_ComfortViewFactorAnglesExtensibleFields::SurfaceName); + + if (!result) { + LOG(Error, "Could not retrieve Surface Name for extensible group " << group.groupIndex() << "."); + } + return result; + } + + boost::optional ComfortViewFactorAngles_Impl::getAngleFactorValue(unsigned groupIndex) const { + if (groupIndex >= numberofAngleFactors()) { + LOG(Error, "Asked to get Angle Factor with index " << groupIndex << ", but " << briefDescription() << " has just " << numberofAngleFactors() + << " angle factors."); return boost::none; } auto group = getExtensibleGroup(groupIndex).cast(); - auto surface = group.getModelObjectTarget(OS_ComfortViewFactorAnglesExtensibleFields::SurfaceName); - auto angleFactor = group.getDouble(OS_ComfortViewFactorAnglesExtensibleFields::AngleFactor); + auto result = group.getDouble(OS_ComfortViewFactorAnglesExtensibleFields::AngleFactor); + if (!result) { + LOG(Error, "Could not retrieve Angle Factor for extensible group " << group.groupIndex() << "."); + } + return result; + } + + boost::optional ComfortViewFactorAngles_Impl::getAngleFactor(unsigned groupIndex) const { + auto surface = getSurface(groupIndex); + auto angleFactor = getAngleFactorValue(groupIndex); if (surface && angleFactor) { return AngleFactor(*surface, *angleFactor); } @@ -99,13 +137,17 @@ namespace model { LOG(Error, "Cannot add a Surface from another Model to " << briefDescription() << "."); return false; } - auto group = pushExtensibleGroup({}, false).cast(); + const auto existingIndex = angleFactorIndex(angleFactor.surface()); + auto group = (existingIndex ? getExtensibleGroup(*existingIndex).cast() + : pushExtensibleGroup({}, false).cast()); bool surfaceSet = group.setPointer(OS_ComfortViewFactorAnglesExtensibleFields::SurfaceName, angleFactor.surface().handle(), false); bool angleFactorSet = group.setDouble(OS_ComfortViewFactorAnglesExtensibleFields::AngleFactor, angleFactor.angleFactor()); if (surfaceSet && angleFactorSet) { return true; } - getObject().eraseExtensibleGroup(group.groupIndex()); + if (!existingIndex) { + getObject().eraseExtensibleGroup(group.groupIndex()); + } return false; } @@ -113,15 +155,25 @@ namespace model { return addAngleFactor(AngleFactor(surface, angleFactor)); } - bool ComfortViewFactorAngles_Impl::removeComfortViewFactorAngle(unsigned groupIndex) { - if (groupIndex >= numberofComfortViewFactorAngles()) { + bool ComfortViewFactorAngles_Impl::addAngleFactors(const std::vector& angleFactors) { + bool result = true; + for (const auto& angleFactor : angleFactors) { + if (!addAngleFactor(angleFactor)) { + result = false; + } + } + return result; + } + + bool ComfortViewFactorAngles_Impl::removeAngleFactor(unsigned groupIndex) { + if (groupIndex >= numberofAngleFactors()) { return false; } getObject().eraseExtensibleGroup(groupIndex); return true; } - void ComfortViewFactorAngles_Impl::removeAllComfortViewFactorAngles() { + void ComfortViewFactorAngles_Impl::removeAllAngleFactors() { getObject().clearExtensibleGroups(); } @@ -135,16 +187,20 @@ namespace model { return IddObjectType::OS_ComfortViewFactorAngles; } - std::vector ComfortViewFactorAngles::comfortViewFactorAngles() const { - return getImpl()->comfortViewFactorAngles(); + std::vector ComfortViewFactorAngles::angleFactors() const { + return getImpl()->angleFactors(); + } + + unsigned int ComfortViewFactorAngles::numberofAngleFactors() const { + return getImpl()->numberofAngleFactors(); } - unsigned int ComfortViewFactorAngles::numberofComfortViewFactorAngles() const { - return getImpl()->numberofComfortViewFactorAngles(); + boost::optional ComfortViewFactorAngles::angleFactorIndex(const Surface& surface) const { + return getImpl()->angleFactorIndex(surface); } - boost::optional ComfortViewFactorAngles::getComfortViewFactorAngle(unsigned groupIndex) const { - return getImpl()->getComfortViewFactorAngle(groupIndex); + boost::optional ComfortViewFactorAngles::getAngleFactor(unsigned groupIndex) const { + return getImpl()->getAngleFactor(groupIndex); } bool ComfortViewFactorAngles::addAngleFactor(const AngleFactor& angleFactor) { @@ -155,15 +211,19 @@ namespace model { return getImpl()->addAngleFactor(surface, angleFactor); } - void ComfortViewFactorAngles::removeComfortViewFactorAngle(int groupIndex) { - getImpl()->removeComfortViewFactorAngle(groupIndex); + bool ComfortViewFactorAngles::addAngleFactors(const std::vector& angleFactors) { + return getImpl()->addAngleFactors(angleFactors); + } + + void ComfortViewFactorAngles::removeAngleFactor(int groupIndex) { + getImpl()->removeAngleFactor(groupIndex); } - void ComfortViewFactorAngles::removeAllComfortViewFactorAngles() { - getImpl()->removeAllComfortViewFactorAngles(); + void ComfortViewFactorAngles::removeAllAngleFactors() { + getImpl()->removeAllAngleFactors(); } ComfortViewFactorAngles::ComfortViewFactorAngles(std::shared_ptr impl) : ModelObject(std::move(impl)) {} } // namespace model -} // namespace openstudio \ No newline at end of file +} // namespace openstudio diff --git a/src/model/ComfortViewFactorAngles.hpp b/src/model/ComfortViewFactorAngles.hpp index b73260fb8e..789fa8085e 100644 --- a/src/model/ComfortViewFactorAngles.hpp +++ b/src/model/ComfortViewFactorAngles.hpp @@ -46,14 +46,16 @@ namespace model { static IddObjectType iddObjectType(); - std::vector comfortViewFactorAngles() const; - unsigned int numberofComfortViewFactorAngles() const; - boost::optional getComfortViewFactorAngle(unsigned groupIndex) const; + std::vector angleFactors() const; + unsigned int numberofAngleFactors() const; + boost::optional angleFactorIndex(const Surface& surface) const; + boost::optional getAngleFactor(unsigned groupIndex) const; bool addAngleFactor(const AngleFactor& angleFactor); bool addAngleFactor(const Surface& surface, double angleFactor); - void removeComfortViewFactorAngle(int groupIndex); - void removeAllComfortViewFactorAngles(); + bool addAngleFactors(const std::vector& angleFactors); + void removeAngleFactor(int groupIndex); + void removeAllAngleFactors(); protected: using ImplType = detail::ComfortViewFactorAngles_Impl; @@ -75,4 +77,4 @@ namespace model { } // namespace model } // namespace openstudio -#endif // MODEL_COMFORTVIEWFACTORANGLES_HPP \ No newline at end of file +#endif // MODEL_COMFORTVIEWFACTORANGLES_HPP diff --git a/src/model/ComfortViewFactorAngles_Impl.hpp b/src/model/ComfortViewFactorAngles_Impl.hpp index d509d7e085..1dce8ffb6f 100644 --- a/src/model/ComfortViewFactorAngles_Impl.hpp +++ b/src/model/ComfortViewFactorAngles_Impl.hpp @@ -28,18 +28,26 @@ namespace model { virtual const std::vector& outputVariableNames() const override; virtual IddObjectType iddObjectType() const override; - std::vector comfortViewFactorAngles() const; - unsigned int numberofComfortViewFactorAngles() const; - boost::optional getComfortViewFactorAngle(unsigned groupIndex) const; + std::vector angleFactors() const; + unsigned int numberofAngleFactors() const; + boost::optional angleFactorIndex(const Surface& surface) const; + boost::optional getAngleFactor(unsigned groupIndex) const; bool addAngleFactor(const AngleFactor& angleFactor); bool addAngleFactor(const Surface& surface, double angleFactor); - bool removeComfortViewFactorAngle(unsigned groupIndex); - void removeAllComfortViewFactorAngles(); + bool addAngleFactors(const std::vector& angleFactors); + bool removeAngleFactor(unsigned groupIndex); + void removeAllAngleFactors(); + + private: + REGISTER_LOGGER("openstudio.model.ComfortViewFactorAngles"); + + boost::optional getSurface(unsigned groupIndex) const; + boost::optional getAngleFactorValue(unsigned groupIndex) const; }; } } // namespace model } // namespace openstudio -#endif // MODEL_COMFORTVIEWFACTORANGLES_IMPL_HPP \ No newline at end of file +#endif // MODEL_COMFORTVIEWFACTORANGLES_IMPL_HPP diff --git a/src/model/ModelGeometry.i b/src/model/ModelGeometry.i index 844fc60fd8..7806168e05 100644 --- a/src/model/ModelGeometry.i +++ b/src/model/ModelGeometry.i @@ -263,7 +263,7 @@ MODELOBJECT_TEMPLATES(FoundationKiva); MODELOBJECT_TEMPLATES(SurfacePropertyExposedFoundationPerimeter); MODELOBJECT_TEMPLATES(ViewFactor); // Helper class defined in ZonePropertyUserViewFactorsBySurfaceName MODELOBJECT_TEMPLATES(ZonePropertyUserViewFactorsBySurfaceName); -MODELOBJECT_TEMPLATES(AngleFactor); +MODELOBJECT_TEMPLATES(AngleFactor); // Helper class defined in ComfortViewFactorAngles MODELOBJECT_TEMPLATES(ComfortViewFactorAngles); MODELOBJECT_TEMPLATES(MRTWeightingFactor); // Helper class defined in ZoneMRTCalculation MODELOBJECT_TEMPLATES(ZoneMRTCalculation); @@ -324,6 +324,7 @@ SWIG_MODELOBJECT(FoundationKiva, 1); SWIG_MODELOBJECT(SurfacePropertyExposedFoundationPerimeter, 1); SWIG_MODELOBJECT(ZonePropertyUserViewFactorsBySurfaceName, 1); SWIG_MODELOBJECT(ZoneMRTCalculation, 1); +SWIG_MODELOBJECT(ComfortViewFactorAngles, 1); SWIG_MODELOBJECT(ExteriorLoadInstance, 0); SWIG_MODELOBJECT(ExteriorLights, 1); SWIG_MODELOBJECT(ExteriorFuelEquipment, 1); diff --git a/src/model/test/ComfortViewFactorAngles_GTest.cpp b/src/model/test/ComfortViewFactorAngles_GTest.cpp index 0053e65bb2..919a7f6375 100644 --- a/src/model/test/ComfortViewFactorAngles_GTest.cpp +++ b/src/model/test/ComfortViewFactorAngles_GTest.cpp @@ -5,6 +5,8 @@ #include +#include + #include "ModelFixture.hpp" #include "../ComfortViewFactorAngles.hpp" @@ -12,6 +14,8 @@ #include "../PeopleDefinition.hpp" #include "../Surface.hpp" +#include "../../utilities/geometry/Point3d.hpp" + using namespace openstudio; using namespace openstudio::model; @@ -21,34 +25,53 @@ TEST_F(ModelFixture, ComfortViewFactorAngles) { Surface surface(points, model); ComfortViewFactorAngles comfortViewFactorAngles(model); + // Individual angle factors must remain within the IDD's inclusive range. EXPECT_THROW(AngleFactor(surface, -0.01), openstudio::Exception); EXPECT_THROW(AngleFactor(surface, 1.01), openstudio::Exception); + // A valid group is stored, indexed by its Surface, and returned as an AngleFactor. EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(surface, 0.25)); - EXPECT_EQ(1u, comfortViewFactorAngles.numberofComfortViewFactorAngles()); + EXPECT_EQ(1u, comfortViewFactorAngles.numberofAngleFactors()); + EXPECT_EQ(0u, comfortViewFactorAngles.angleFactorIndex(surface).get()); + EXPECT_FALSE(comfortViewFactorAngles.getAngleFactor(1)); + + const auto angleFactors = comfortViewFactorAngles.angleFactors(); + ASSERT_EQ(1u, angleFactors.size()); + EXPECT_EQ(surface.handle(), angleFactors.front().surface().handle()); + EXPECT_DOUBLE_EQ(0.25, angleFactors.front().angleFactor()); + EXPECT_EQ(AngleFactor(surface, 0.25), angleFactors.front()); + EXPECT_NE(AngleFactor(surface, 0.5), angleFactors.front()); + std::stringstream stream; + stream << angleFactors.front(); + EXPECT_EQ("(surface='Surface 1', angle factor=0.25)", stream.str()); - auto comfortViewFactorAngle = comfortViewFactorAngles.getComfortViewFactorAngle(0); - ASSERT_TRUE(comfortViewFactorAngle); - EXPECT_EQ(surface.handle(), comfortViewFactorAngle->surface().handle()); - EXPECT_DOUBLE_EQ(0.25, comfortViewFactorAngle->angleFactor()); - EXPECT_EQ(AngleFactor(surface, 0.25), *comfortViewFactorAngle); - EXPECT_NE(AngleFactor(surface, 0.5), *comfortViewFactorAngle); - EXPECT_FALSE(comfortViewFactorAngles.getComfortViewFactorAngle(1)); + // Reusing a Surface updates its existing group instead of creating a duplicate. + EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(surface, 0.5)); + EXPECT_EQ(1u, comfortViewFactorAngles.numberofAngleFactors()); + EXPECT_DOUBLE_EQ(0.5, comfortViewFactorAngles.getAngleFactor(0)->angleFactor()); + // A Surface in another Model cannot be referenced. Model otherModel; Surface otherSurface(points, otherModel); + EXPECT_FALSE(comfortViewFactorAngles.angleFactorIndex(otherSurface)); EXPECT_FALSE(comfortViewFactorAngles.addAngleFactor(otherSurface, 0.75)); - EXPECT_EQ(1u, comfortViewFactorAngles.numberofComfortViewFactorAngles()); + EXPECT_EQ(1u, comfortViewFactorAngles.numberofAngleFactors()); PeopleDefinition peopleDefinition(model); EXPECT_FALSE(peopleDefinition.setSurfaceNameAngleFactorListName(otherSurface)); EXPECT_EQ("EnclosureAveraged", peopleDefinition.meanRadiantTemperatureCalculationType()); EXPECT_FALSE(peopleDefinition.surfaceNameAngleFactorListName()); - comfortViewFactorAngles.removeComfortViewFactorAngle(0); - EXPECT_EQ(0u, comfortViewFactorAngles.numberofComfortViewFactorAngles()); + // Indexed removal deletes one group. + comfortViewFactorAngles.removeAngleFactor(0); + EXPECT_EQ(0u, comfortViewFactorAngles.numberofAngleFactors()); - EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(surface, 1.0)); - comfortViewFactorAngles.removeAllComfortViewFactorAngles(); - EXPECT_TRUE(comfortViewFactorAngles.comfortViewFactorAngles().empty()); + // Bulk addition supports distinct Surface groups, and clearing removes them all. + Surface otherSurfaceInModel(points, model); + EXPECT_TRUE(comfortViewFactorAngles.addAngleFactors({AngleFactor(surface, 0.25), AngleFactor(otherSurfaceInModel, 0.75)})); + EXPECT_EQ(2u, comfortViewFactorAngles.numberofAngleFactors()); + EXPECT_DOUBLE_EQ(0.25, comfortViewFactorAngles.getAngleFactor(0)->angleFactor()); + EXPECT_DOUBLE_EQ(0.75, comfortViewFactorAngles.getAngleFactor(1)->angleFactor()); + comfortViewFactorAngles.removeAllAngleFactors(); + EXPECT_TRUE(comfortViewFactorAngles.angleFactors().empty()); } diff --git a/src/model/test/People_GTest.cpp b/src/model/test/People_GTest.cpp index 69da6ef5fe..7f446044f0 100644 --- a/src/model/test/People_GTest.cpp +++ b/src/model/test/People_GTest.cpp @@ -20,6 +20,8 @@ #include "../ScheduleTypeRegistry.hpp" #include "../ScheduleConstant.hpp" +#include "../../utilities/geometry/Point3d.hpp" + #include using namespace openstudio; @@ -55,11 +57,11 @@ TEST_F(ModelFixture, People_DefaultConstructor) { ComfortViewFactorAngles comfortViewFactorAngles(model); EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(surface, 1.0)); - EXPECT_EQ(1u, comfortViewFactorAngles.numberofComfortViewFactorAngles()); - auto comfortViewFactorAngle = comfortViewFactorAngles.getComfortViewFactorAngle(0); - ASSERT_TRUE(comfortViewFactorAngle); - EXPECT_EQ(surface.handle(), comfortViewFactorAngle->surface().handle()); - EXPECT_DOUBLE_EQ(1.0, comfortViewFactorAngle->angleFactor()); + EXPECT_EQ(1u, comfortViewFactorAngles.numberofAngleFactors()); + const auto angleFactors = comfortViewFactorAngles.angleFactors(); + ASSERT_EQ(1u, angleFactors.size()); + EXPECT_EQ(surface.handle(), angleFactors.front().surface().handle()); + EXPECT_DOUBLE_EQ(1.0, angleFactors.front().angleFactor()); EXPECT_TRUE(definition.setSurfaceNameAngleFactorListName(comfortViewFactorAngles)); EXPECT_EQ("AngleFactor", definition.meanRadiantTemperatureCalculationType()); From cef6abba85a2f7fdc5798dce5af72ef2a477d472 Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Tue, 8 Sep 2026 14:04:31 -0700 Subject: [PATCH 11/20] Refine ComfortViewFactorAngles translation --- ...orwardTranslateComfortViewFactorAngles.cpp | 45 ++++++++++------ .../ForwardTranslatePeople.cpp | 4 ++ ...everseTranslateComfortViewFactorAngles.cpp | 8 ++- .../Test/ComfortViewFactorAngles_GTest.cpp | 52 ++++++++++++++++--- 4 files changed, 83 insertions(+), 26 deletions(-) diff --git a/src/energyplus/ForwardTranslator/ForwardTranslateComfortViewFactorAngles.cpp b/src/energyplus/ForwardTranslator/ForwardTranslateComfortViewFactorAngles.cpp index c6c66c1edd..45d740e6fa 100644 --- a/src/energyplus/ForwardTranslator/ForwardTranslateComfortViewFactorAngles.cpp +++ b/src/energyplus/ForwardTranslator/ForwardTranslateComfortViewFactorAngles.cpp @@ -5,10 +5,16 @@ #include "../ForwardTranslator.hpp" +#include "../../model/Model.hpp" +#include "../../model/Surface.hpp" +#include "../../model/Surface_Impl.hpp" #include "../../model/ComfortViewFactorAngles.hpp" +#include "../../model/ComfortViewFactorAngles_Impl.hpp" +#include "../../utilities/idf/IdfExtensibleGroup.hpp" #include "../../utilities/math/FloatCompare.hpp" +#include #include using namespace openstudio::model; @@ -17,32 +23,37 @@ namespace openstudio { namespace energyplus { boost::optional ForwardTranslator::translateComfortViewFactorAngles(ComfortViewFactorAngles& modelObject) { - const auto comfortViewFactorAngles = modelObject.comfortViewFactorAngles(); - if (comfortViewFactorAngles.empty()) { - LOG(Error, modelObject.briefDescription() << " does not have any angle factors and will not be translated."); + std::vector angleFactors = modelObject.angleFactors(); + if (angleFactors.empty()) { + LOG(Warn, modelObject.briefDescription() << " does not have any angle factors and will not be translated."); return boost::none; } + std::vector> translatedAngleFactors; double sum = 0.0; - for (const auto& comfortViewFactorAngle : comfortViewFactorAngles) { - sum += comfortViewFactorAngle.angleFactor(); - } constexpr double tolerance = 0.000001; - if (!equal(sum, 1.0, tolerance)) { - LOG(Error, modelObject.briefDescription() << " has angle factors that sum to " << sum << " instead of 1.0 and will not be translated."); - return boost::none; - } - std::vector> translatedAngleFactors; - for (const auto& comfortViewFactorAngle : comfortViewFactorAngles) { - if (auto surface = translateAndMapModelObject(comfortViewFactorAngle.surface())) { - translatedAngleFactors.emplace_back(surface->nameString(), comfortViewFactorAngle.angleFactor()); + for (const AngleFactor& angleFactor : angleFactors) { + Surface surface = angleFactor.surface(); + if (auto idfSurface = translateAndMapModelObject(surface)) { + const double value = angleFactor.angleFactor(); + sum += value; + translatedAngleFactors.emplace_back(idfSurface->nameString(), value); } else { - LOG(Error, "Could not translate a Surface for " << modelObject.briefDescription() << "."); - return boost::none; + LOG(Error, "Could not translate an AngleFactor group for " << modelObject.briefDescription() << ". Continuing with the rest."); } } + if (translatedAngleFactors.empty()) { + LOG(Error, modelObject.briefDescription() << " does not have any valid angle factors and will not be translated."); + return boost::none; + } + + if (!equal(sum, 1.0, tolerance)) { + LOG(Error, modelObject.briefDescription() << " has valid angle factors that sum to " << sum << " instead of 1.0 and will not be translated."); + return boost::none; + } + IdfObject idfObject = createRegisterAndNameIdfObject(IddObjectType::ComfortViewFactorAngles, modelObject); for (const auto& [surfaceName, angleFactor] : translatedAngleFactors) { auto group = idfObject.pushExtensibleGroup(); @@ -53,4 +64,4 @@ namespace energyplus { } } // namespace energyplus -} // namespace openstudio \ No newline at end of file +} // namespace openstudio diff --git a/src/energyplus/ForwardTranslator/ForwardTranslatePeople.cpp b/src/energyplus/ForwardTranslator/ForwardTranslatePeople.cpp index ffeffb3749..fcf1d236ff 100644 --- a/src/energyplus/ForwardTranslator/ForwardTranslatePeople.cpp +++ b/src/energyplus/ForwardTranslator/ForwardTranslatePeople.cpp @@ -19,6 +19,10 @@ #include "../../model/ThermalZone_Impl.hpp" #include "../../model/Schedule.hpp" #include "../../model/Schedule_Impl.hpp" +#include "../../model/Surface.hpp" +#include "../../model/Surface_Impl.hpp" +#include "../../model/ComfortViewFactorAngles.hpp" +#include "../../model/ComfortViewFactorAngles_Impl.hpp" #include "../../model/LifeCycleCost.hpp" #include "../../utilities/idf/IdfExtensibleGroup.hpp" diff --git a/src/energyplus/ReverseTranslator/ReverseTranslateComfortViewFactorAngles.cpp b/src/energyplus/ReverseTranslator/ReverseTranslateComfortViewFactorAngles.cpp index 7c24fd8f83..23ef511b39 100644 --- a/src/energyplus/ReverseTranslator/ReverseTranslateComfortViewFactorAngles.cpp +++ b/src/energyplus/ReverseTranslator/ReverseTranslateComfortViewFactorAngles.cpp @@ -6,11 +6,17 @@ #include "../ReverseTranslator.hpp" #include "../../model/ComfortViewFactorAngles.hpp" +#include "../../model/ComfortViewFactorAngles_Impl.hpp" #include "../../model/Surface.hpp" +#include "../../model/Surface_Impl.hpp" #include "../../utilities/idf/WorkspaceExtensibleGroup.hpp" #include +#include "../../utilities/idd/IddEnums.hpp" +#include + +#include "../../utilities/core/Assert.hpp" using namespace openstudio::model; @@ -46,4 +52,4 @@ namespace energyplus { } } // namespace energyplus -} // namespace openstudio \ No newline at end of file +} // namespace openstudio diff --git a/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp b/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp index 43a25c6ac0..7942b50964 100644 --- a/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp +++ b/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp @@ -50,6 +50,15 @@ TEST_F(EnergyPlusFixture, ForwardTranslator_ComfortViewFactorAngles) { EXPECT_DOUBLE_EQ(1.0, group.getDouble(1).get()); } +TEST_F(EnergyPlusFixture, ForwardTranslator_ComfortViewFactorAngles_Empty) { + Model model; + ComfortViewFactorAngles comfortViewFactorAngles(model); + + ForwardTranslator forwardTranslator; + Workspace workspace = forwardTranslator.translateModelObject(comfortViewFactorAngles); + EXPECT_TRUE(workspace.getObjectsByType(IddObjectType::ComfortViewFactorAngles).empty()); +} + TEST_F(EnergyPlusFixture, ForwardTranslator_ComfortViewFactorAngles_InvalidSum) { Model model; ThermalZone zone(model); @@ -81,6 +90,33 @@ TEST_F(EnergyPlusFixture, ForwardTranslator_ComfortViewFactorAngles_Untranslated EXPECT_TRUE(workspace.getObjectsByType(IddObjectType::ComfortViewFactorAngles).empty()); } +TEST_F(EnergyPlusFixture, ForwardTranslator_ComfortViewFactorAngles_SkipsUntranslatedSurface) { + Model model; + ThermalZone zone(model); + Space space(model); + EXPECT_TRUE(space.setThermalZone(zone)); + + Point3dVector points{{0, 0, 0}, {1, 0, 0}, {1, 1, 0}}; + Surface translatedSurface(points, model); + translatedSurface.setName("Translated Surface"); + EXPECT_TRUE(translatedSurface.setSpace(space)); + Surface untranslatedSurface(points, model); + + ComfortViewFactorAngles comfortViewFactorAngles(model); + EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(translatedSurface, 1.0)); + EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(untranslatedSurface, 0.0)); + + ForwardTranslator forwardTranslator; + Workspace workspace = forwardTranslator.translateModelObject(comfortViewFactorAngles); + + const auto angleFactorObjects = workspace.getObjectsByType(IddObjectType::ComfortViewFactorAngles); + ASSERT_EQ(1u, angleFactorObjects.size()); + ASSERT_EQ(1u, angleFactorObjects.front().numExtensibleGroups()); + const auto group = angleFactorObjects.front().extensibleGroups().front(); + EXPECT_EQ("Translated Surface", group.getString(0).get()); + EXPECT_DOUBLE_EQ(1.0, group.getDouble(1).get()); +} + TEST_F(EnergyPlusFixture, ReverseTranslator_ComfortViewFactorAngles) { Workspace workspace(StrictnessLevel::Minimal, IddFileType::EnergyPlus); @@ -114,13 +150,13 @@ TEST_F(EnergyPlusFixture, ReverseTranslator_ComfortViewFactorAngles) { ReverseTranslator reverseTranslator; Model model = reverseTranslator.translateWorkspace(workspace); - const auto angleFactors = model.getConcreteModelObjects(); - ASSERT_EQ(1u, angleFactors.size()); - const auto& comfortViewFactorAngles = angleFactors.front(); + const auto comfortViewFactorAngleObjects = model.getConcreteModelObjects(); + ASSERT_EQ(1u, comfortViewFactorAngleObjects.size()); + const auto& comfortViewFactorAngles = comfortViewFactorAngleObjects.front(); EXPECT_EQ("Angle Factors", comfortViewFactorAngles.nameString()); - ASSERT_EQ(1u, comfortViewFactorAngles.numberofComfortViewFactorAngles()); - const auto comfortViewFactorAngle = comfortViewFactorAngles.getComfortViewFactorAngle(0); - ASSERT_TRUE(comfortViewFactorAngle); - EXPECT_EQ("Surface 1", comfortViewFactorAngle->surface().nameString()); - EXPECT_DOUBLE_EQ(1.0, comfortViewFactorAngle->angleFactor()); + ASSERT_EQ(1u, comfortViewFactorAngles.numberofAngleFactors()); + const auto angleFactors = comfortViewFactorAngles.angleFactors(); + ASSERT_EQ(1u, angleFactors.size()); + EXPECT_EQ("Surface 1", angleFactors.front().surface().nameString()); + EXPECT_DOUBLE_EQ(1.0, angleFactors.front().angleFactor()); } \ No newline at end of file From 9a9d173001f22c0b349a71ab3b4668ad77dbe6fe Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Tue, 8 Sep 2026 14:24:34 -0700 Subject: [PATCH 12/20] Fix ComfortViewFactorAngles model integration --- .../Test/ComfortViewFactorAngles_GTest.cpp | 3 +++ src/model/ComfortViewFactorAngles.cpp | 21 +++++++++++++++++++ src/model/PeopleDefinition.cpp | 12 +++++++++++ .../test/ComfortViewFactorAngles_GTest.cpp | 13 ++++++++++-- 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp b/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp index 7942b50964..29480ec420 100644 --- a/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp +++ b/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp @@ -15,6 +15,9 @@ #include "../../model/Surface.hpp" #include "../../model/ThermalZone.hpp" +#include "../../utilities/idf/IdfExtensibleGroup.hpp" +#include "../../utilities/geometry/Point3d.hpp" + #include #include diff --git a/src/model/ComfortViewFactorAngles.cpp b/src/model/ComfortViewFactorAngles.cpp index f47abd9bd8..544f12bce0 100644 --- a/src/model/ComfortViewFactorAngles.cpp +++ b/src/model/ComfortViewFactorAngles.cpp @@ -138,6 +138,27 @@ namespace model { return false; } const auto existingIndex = angleFactorIndex(angleFactor.surface()); + + double sum = angleFactor.angleFactor(); + for (unsigned i = 0; i < numberofAngleFactors(); ++i) { + if (existingIndex && (i == *existingIndex)) { + continue; + } + auto existingValue = getAngleFactorValue(i); + if (!existingValue || (*existingValue < 0.0) || (*existingValue > 1.0)) { + LOG(Error, "Cannot add an AngleFactor to " << briefDescription() << " because an existing Angle Factor is invalid."); + return false; + } + sum += *existingValue; + } + + constexpr double tolerance = 0.000001; + if (sum > 1.0 + tolerance) { + LOG(Error, "Cannot add an AngleFactor to " << briefDescription() << " because the Angle Factors would sum to " << sum + << ", which is greater than 1."); + return false; + } + auto group = (existingIndex ? getExtensibleGroup(*existingIndex).cast() : pushExtensibleGroup({}, false).cast()); bool surfaceSet = group.setPointer(OS_ComfortViewFactorAnglesExtensibleFields::SurfaceName, angleFactor.surface().handle(), false); diff --git a/src/model/PeopleDefinition.cpp b/src/model/PeopleDefinition.cpp index 370c76964b..812e56f537 100644 --- a/src/model/PeopleDefinition.cpp +++ b/src/model/PeopleDefinition.cpp @@ -504,6 +504,10 @@ namespace model { return getImpl()->isMeanRadiantTemperatureCalculationTypeDefaulted(); } + boost::optional PeopleDefinition::surfaceNameAngleFactorListName() const { + return getImpl()->surfaceNameAngleFactorListName(); + } + boost::optional PeopleDefinition::getThermalComfortModelType(int i) const { return getImpl()->getThermalComfortModelType(i); } @@ -564,6 +568,14 @@ namespace model { getImpl()->resetMeanRadiantTemperatureCalculationType(); } + bool PeopleDefinition::setSurfaceNameAngleFactorListName(const ModelObject& modelObject) { + return getImpl()->setSurfaceNameAngleFactorListName(modelObject); + } + + void PeopleDefinition::resetSurfaceNameAngleFactorListName() { + getImpl()->resetSurfaceNameAngleFactorListName(); + } + bool PeopleDefinition::pushThermalComfortModelType(const std::string& thermalComfortModelType) { return getImpl()->pushThermalComfortModelType(thermalComfortModelType); } diff --git a/src/model/test/ComfortViewFactorAngles_GTest.cpp b/src/model/test/ComfortViewFactorAngles_GTest.cpp index 919a7f6375..f76e9d9144 100644 --- a/src/model/test/ComfortViewFactorAngles_GTest.cpp +++ b/src/model/test/ComfortViewFactorAngles_GTest.cpp @@ -50,12 +50,20 @@ TEST_F(ModelFixture, ComfortViewFactorAngles) { EXPECT_EQ(1u, comfortViewFactorAngles.numberofAngleFactors()); EXPECT_DOUBLE_EQ(0.5, comfortViewFactorAngles.getAngleFactor(0)->angleFactor()); + // New groups may bring the total to one, but cannot make it exceed one. + Surface otherSurfaceInModel(points, model); + EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(otherSurfaceInModel, 0.5)); + EXPECT_EQ(2u, comfortViewFactorAngles.numberofAngleFactors()); + Surface thirdSurfaceInModel(points, model); + EXPECT_FALSE(comfortViewFactorAngles.addAngleFactor(thirdSurfaceInModel, 0.01)); + EXPECT_EQ(2u, comfortViewFactorAngles.numberofAngleFactors()); + // A Surface in another Model cannot be referenced. Model otherModel; Surface otherSurface(points, otherModel); EXPECT_FALSE(comfortViewFactorAngles.angleFactorIndex(otherSurface)); EXPECT_FALSE(comfortViewFactorAngles.addAngleFactor(otherSurface, 0.75)); - EXPECT_EQ(1u, comfortViewFactorAngles.numberofAngleFactors()); + EXPECT_EQ(2u, comfortViewFactorAngles.numberofAngleFactors()); PeopleDefinition peopleDefinition(model); EXPECT_FALSE(peopleDefinition.setSurfaceNameAngleFactorListName(otherSurface)); @@ -64,10 +72,11 @@ TEST_F(ModelFixture, ComfortViewFactorAngles) { // Indexed removal deletes one group. comfortViewFactorAngles.removeAngleFactor(0); + EXPECT_EQ(1u, comfortViewFactorAngles.numberofAngleFactors()); + comfortViewFactorAngles.removeAngleFactor(0); EXPECT_EQ(0u, comfortViewFactorAngles.numberofAngleFactors()); // Bulk addition supports distinct Surface groups, and clearing removes them all. - Surface otherSurfaceInModel(points, model); EXPECT_TRUE(comfortViewFactorAngles.addAngleFactors({AngleFactor(surface, 0.25), AngleFactor(otherSurfaceInModel, 0.75)})); EXPECT_EQ(2u, comfortViewFactorAngles.numberofAngleFactors()); EXPECT_DOUBLE_EQ(0.25, comfortViewFactorAngles.getAngleFactor(0)->angleFactor()); From 25b1e1aac117b34d57e8fd1ff01a74c231d3a0af Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Tue, 8 Sep 2026 14:29:43 -0700 Subject: [PATCH 13/20] Add direct dependencies to ComfortViewFactorAngles translator tests --- src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp | 4 +++- src/energyplus/Test/People_GTest.cpp | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp b/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp index 29480ec420..3f8798e4a4 100644 --- a/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp +++ b/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp @@ -10,12 +10,14 @@ #include "../ReverseTranslator.hpp" #include "../../model/ComfortViewFactorAngles.hpp" +#include "../../model/ComfortViewFactorAngles_Impl.hpp" #include "../../model/Model.hpp" #include "../../model/Space.hpp" #include "../../model/Surface.hpp" #include "../../model/ThermalZone.hpp" #include "../../utilities/idf/IdfExtensibleGroup.hpp" +#include "../../utilities/idf/WorkspaceExtensibleGroup.hpp" #include "../../utilities/geometry/Point3d.hpp" #include @@ -146,7 +148,7 @@ TEST_F(EnergyPlusFixture, ReverseTranslator_ComfortViewFactorAngles) { auto angleFactorObject = workspace.addObject(IdfObject(IddObjectType::ComfortViewFactorAngles)).get(); angleFactorObject.setName("Angle Factors"); - auto group = angleFactorObject.pushExtensibleGroup(); + auto group = angleFactorObject.pushExtensibleGroup().cast(); EXPECT_TRUE(group.setPointer(0, surface.handle())); EXPECT_TRUE(group.setDouble(1, 1.0)); diff --git a/src/energyplus/Test/People_GTest.cpp b/src/energyplus/Test/People_GTest.cpp index d776e65c51..0e5203b2d5 100644 --- a/src/energyplus/Test/People_GTest.cpp +++ b/src/energyplus/Test/People_GTest.cpp @@ -20,6 +20,9 @@ #include "../../model/ScheduleConstant.hpp" #include "../../model/Surface.hpp" +#include "../../utilities/geometry/Point3d.hpp" +#include "../../utilities/idf/IdfExtensibleGroup.hpp" + #include #include #include From 85e090abdbdc1d69f19283d0e0687d7684be4aec Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Tue, 8 Sep 2026 14:34:39 -0700 Subject: [PATCH 14/20] Correct ComfortViewFactorAngles translation test paths and People field indices --- .../Test/ComfortViewFactorAngles_GTest.cpp | 33 ++++++++++++++++--- .../test/VersionTranslator_GTest.cpp | 12 +++---- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp b/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp index 3f8798e4a4..df9d8544ea 100644 --- a/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp +++ b/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp @@ -12,6 +12,8 @@ #include "../../model/ComfortViewFactorAngles.hpp" #include "../../model/ComfortViewFactorAngles_Impl.hpp" #include "../../model/Model.hpp" +#include "../../model/People.hpp" +#include "../../model/PeopleDefinition.hpp" #include "../../model/Space.hpp" #include "../../model/Surface.hpp" #include "../../model/ThermalZone.hpp" @@ -42,8 +44,13 @@ TEST_F(EnergyPlusFixture, ForwardTranslator_ComfortViewFactorAngles) { comfortViewFactorAngles.setName("Angle Factors"); EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(surface, 1.0)); + PeopleDefinition definition(model); + EXPECT_TRUE(definition.setSurfaceNameAngleFactorListName(comfortViewFactorAngles)); + People people(definition); + EXPECT_TRUE(people.setSpace(space)); + ForwardTranslator forwardTranslator; - Workspace workspace = forwardTranslator.translateModelObject(comfortViewFactorAngles); + Workspace workspace = forwardTranslator.translateModel(model); const auto angleFactorObjects = workspace.getObjectsByType(IddObjectType::ComfortViewFactorAngles); ASSERT_EQ(1u, angleFactorObjects.size()); @@ -77,21 +84,34 @@ TEST_F(EnergyPlusFixture, ForwardTranslator_ComfortViewFactorAngles_InvalidSum) ComfortViewFactorAngles comfortViewFactorAngles(model); EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(surface, 0.5)); + PeopleDefinition definition(model); + EXPECT_TRUE(definition.setSurfaceNameAngleFactorListName(comfortViewFactorAngles)); + People people(definition); + EXPECT_TRUE(people.setSpace(space)); + ForwardTranslator forwardTranslator; - Workspace workspace = forwardTranslator.translateModelObject(comfortViewFactorAngles); + Workspace workspace = forwardTranslator.translateModel(model); EXPECT_TRUE(workspace.getObjectsByType(IddObjectType::ComfortViewFactorAngles).empty()); } TEST_F(EnergyPlusFixture, ForwardTranslator_ComfortViewFactorAngles_UntranslatedSurface) { Model model; + ThermalZone zone(model); + Space space(model); + EXPECT_TRUE(space.setThermalZone(zone)); Point3dVector points{{0, 0, 0}, {1, 0, 0}, {1, 1, 0}}; Surface surface(points, model); ComfortViewFactorAngles comfortViewFactorAngles(model); EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(surface, 1.0)); + PeopleDefinition definition(model); + EXPECT_TRUE(definition.setSurfaceNameAngleFactorListName(comfortViewFactorAngles)); + People people(definition); + EXPECT_TRUE(people.setSpace(space)); + ForwardTranslator forwardTranslator; - Workspace workspace = forwardTranslator.translateModelObject(comfortViewFactorAngles); + Workspace workspace = forwardTranslator.translateModel(model); EXPECT_TRUE(workspace.getObjectsByType(IddObjectType::ComfortViewFactorAngles).empty()); } @@ -111,8 +131,13 @@ TEST_F(EnergyPlusFixture, ForwardTranslator_ComfortViewFactorAngles_SkipsUntrans EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(translatedSurface, 1.0)); EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(untranslatedSurface, 0.0)); + PeopleDefinition definition(model); + EXPECT_TRUE(definition.setSurfaceNameAngleFactorListName(comfortViewFactorAngles)); + People people(definition); + EXPECT_TRUE(people.setSpace(space)); + ForwardTranslator forwardTranslator; - Workspace workspace = forwardTranslator.translateModelObject(comfortViewFactorAngles); + Workspace workspace = forwardTranslator.translateModel(model); const auto angleFactorObjects = workspace.getObjectsByType(IddObjectType::ComfortViewFactorAngles); ASSERT_EQ(1u, angleFactorObjects.size()); diff --git a/src/osversion/test/VersionTranslator_GTest.cpp b/src/osversion/test/VersionTranslator_GTest.cpp index 385b3b5133..370fd8a44e 100644 --- a/src/osversion/test/VersionTranslator_GTest.cpp +++ b/src/osversion/test/VersionTranslator_GTest.cpp @@ -4967,12 +4967,12 @@ TEST_F(OSVersionFixture, update_3_10_0_to_3_11_0_People) { ASSERT_EQ(1u, ps.size()); const auto& p = ps.front(); - EXPECT_TRUE(p.isEmpty(7)); // Work Efficiency Schedule Name - EXPECT_EQ("ClothingInsulationSchedule", p.getString(8).get()); // Clothing Insulation Calculation Method - EXPECT_TRUE(p.isEmpty(9)); // Clothing Insulation Calculation Method Schedule Name - EXPECT_EQ("Clothing Insulation Schedule", p.getTarget(10)->nameString()); // Clothing Insulation Schedule Name - EXPECT_TRUE(p.isEmpty(11)); // Air Velocity Schedule Name - EXPECT_EQ(1.0, p.getDouble(12).get()); // Multiplier + EXPECT_TRUE(p.isEmpty(6)); // Work Efficiency Schedule Name + EXPECT_EQ("ClothingInsulationSchedule", p.getString(7).get()); // Clothing Insulation Calculation Method + EXPECT_TRUE(p.isEmpty(8)); // Clothing Insulation Calculation Method Schedule Name + EXPECT_EQ("Clothing Insulation Schedule", p.getTarget(9)->nameString()); // Clothing Insulation Schedule Name + EXPECT_TRUE(p.isEmpty(10)); // Air Velocity Schedule Name + EXPECT_EQ(1.0, p.getDouble(11).get()); // Multiplier } TEST_F(OSVersionFixture, update_3_10_0_to_3_11_0_EvaporativeFluidCooler) { From b0f4b5cea94c0a798f455f71e4db69c4d8123d2a Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Wed, 9 Sep 2026 08:26:57 -0700 Subject: [PATCH 15/20] Update ComfortViewFactorAngles EnergyPlus IDD metadata Require a name, declare the minimum field count, and document the enclosure requirement for referenced surfaces. --- resources/energyplus/ProposedEnergy+.idd | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/resources/energyplus/ProposedEnergy+.idd b/resources/energyplus/ProposedEnergy+.idd index a5a0e7727d..1f280d3c46 100644 --- a/resources/energyplus/ProposedEnergy+.idd +++ b/resources/energyplus/ProposedEnergy+.idd @@ -12158,16 +12158,16 @@ People:Definition, \note optional (seventh thermal comfort model and report type) ComfortViewFactorAngles, + \min-fields 3 \memo Used to specify radiant view factors for thermal comfort calculations. \memo Note that the following angle factor fractions must sum up to 1.0 - \memo The number of surfaces can be expanded beyond 100, if necessary, by adding more - \memo groups to the end of the list. - \extensible:2 + \extensible:2 - repeat last two fields, remembering to remove ; from "inner" fields. + \memo All surfaces must be in the same enclosure. A1 , \field Name + \required-field \type alpha \reference AllHeatTranAngFacNames - \object-list ZoneNames - A3 , \field Surface 1 Name + A2 , \field Surface 1 Name \type object-list \object-list AllHeatTranSurfNames \begin-extensible From 2a92c968431ffe85ad978c73575faaf57b4d30fc Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Wed, 9 Sep 2026 08:27:06 -0700 Subject: [PATCH 16/20] Require ComfortViewFactorAngles surfaces in one thermal zone Validate surface space and thermal-zone assignments before adding angle factors, reject cross-zone lists, and align extensible-group update handling with ZoneMRTCalculation. --- src/model/ComfortViewFactorAngles.cpp | 89 ++++++++++++++----- .../test/ComfortViewFactorAngles_GTest.cpp | 17 ++++ src/model/test/People_GTest.cpp | 5 ++ 3 files changed, 90 insertions(+), 21 deletions(-) diff --git a/src/model/ComfortViewFactorAngles.cpp b/src/model/ComfortViewFactorAngles.cpp index 544f12bce0..a647774c2a 100644 --- a/src/model/ComfortViewFactorAngles.cpp +++ b/src/model/ComfortViewFactorAngles.cpp @@ -8,6 +8,8 @@ #include "Surface.hpp" #include "Surface_Impl.hpp" +#include "Space.hpp" +#include "ThermalZone.hpp" #include "Model.hpp" #include "Model_Impl.hpp" @@ -45,7 +47,6 @@ namespace model { } namespace detail { - ComfortViewFactorAngles_Impl::ComfortViewFactorAngles_Impl(const IdfObject& idfObject, Model_Impl* model, bool keepHandle) : ModelObject_Impl(idfObject, model, keepHandle) { OS_ASSERT(idfObject.iddObject().type() == ComfortViewFactorAngles::iddObjectType()); @@ -53,9 +54,7 @@ namespace model { ComfortViewFactorAngles_Impl::ComfortViewFactorAngles_Impl(const openstudio::detail::WorkspaceObject_Impl& other, Model_Impl* model, bool keepHandle) - : ModelObject_Impl(other, model, keepHandle) { - OS_ASSERT(other.iddObject().type() == ComfortViewFactorAngles::iddObjectType()); - } + : ModelObject_Impl(other, model, keepHandle) {} ComfortViewFactorAngles_Impl::ComfortViewFactorAngles_Impl(const ComfortViewFactorAngles_Impl& other, Model_Impl* model, bool keepHandle) : ModelObject_Impl(other, model, keepHandle) {} @@ -133,11 +132,46 @@ namespace model { } bool ComfortViewFactorAngles_Impl::addAngleFactor(const AngleFactor& angleFactor) { - if (angleFactor.surface().model() != model()) { + bool result = false; + + Surface surface = angleFactor.surface(); + if (surface.model() != model()) { LOG(Error, "Cannot add a Surface from another Model to " << briefDescription() << "."); - return false; + return result; + } + + boost::optional space = surface.space(); + if (!space) { + LOG(Error, "Cannot add " << surface.briefDescription() << " to " << briefDescription() << " because it is not assigned to a Space."); + return result; + } + + boost::optional thermalZone = space->thermalZone(); + if (!thermalZone) { + LOG(Error, "Cannot add " << surface.briefDescription() << " to " << briefDescription() + << " because it is not assigned to any ThermalZone."); + return result; + } + + for (const auto& existingAngleFactor : angleFactors()) { + const auto existingSpace = existingAngleFactor.surface().space(); + const auto existingThermalZone = existingSpace ? existingSpace->thermalZone() : boost::none; + if (!existingThermalZone || (existingThermalZone->handle() != thermalZone->handle())) { + LOG(Error, "Cannot add " << surface.briefDescription() << " to " << briefDescription() << " because it is assigned to ThermalZone '" + << thermalZone->nameString() + << "' instead of '" + << (existingThermalZone ? existingThermalZone->nameString() : "an unassigned ThermalZone") << "'."); + return result; + } + } + + boost::optional existingIndex = angleFactorIndex(surface); + if (existingIndex) { + boost::optional existingAngleFactor = getAngleFactor(existingIndex.get()); + OS_ASSERT(existingAngleFactor); + LOG(Warn, "For " << briefDescription() << ", AngleFactor already exists, will be modified in place from " << existingAngleFactor.get() + << " to " << angleFactor << "."); } - const auto existingIndex = angleFactorIndex(angleFactor.surface()); double sum = angleFactor.angleFactor(); for (unsigned i = 0; i < numberofAngleFactors(); ++i) { @@ -147,7 +181,7 @@ namespace model { auto existingValue = getAngleFactorValue(i); if (!existingValue || (*existingValue < 0.0) || (*existingValue > 1.0)) { LOG(Error, "Cannot add an AngleFactor to " << briefDescription() << " because an existing Angle Factor is invalid."); - return false; + return result; } sum += *existingValue; } @@ -156,20 +190,29 @@ namespace model { if (sum > 1.0 + tolerance) { LOG(Error, "Cannot add an AngleFactor to " << briefDescription() << " because the Angle Factors would sum to " << sum << ", which is greater than 1."); - return false; + return result; + } + + std::vector temp; + ModelExtensibleGroup group = (existingIndex ? getExtensibleGroup(existingIndex.get()).cast() + : pushExtensibleGroup(temp, false).cast()); + + bool surfaceSet = group.setPointer(OS_ComfortViewFactorAnglesExtensibleFields::SurfaceName, surface.handle(), false); + if (!surfaceSet) { + LOG(Error, "Unable to add AngleFactor which has an incompatible Surface object to " << briefDescription()); + OS_ASSERT(false); } - auto group = (existingIndex ? getExtensibleGroup(*existingIndex).cast() - : pushExtensibleGroup({}, false).cast()); - bool surfaceSet = group.setPointer(OS_ComfortViewFactorAnglesExtensibleFields::SurfaceName, angleFactor.surface().handle(), false); bool angleFactorSet = group.setDouble(OS_ComfortViewFactorAnglesExtensibleFields::AngleFactor, angleFactor.angleFactor()); if (surfaceSet && angleFactorSet) { - return true; - } - if (!existingIndex) { - getObject().eraseExtensibleGroup(group.groupIndex()); + result = true; + } else { + if (!existingIndex) { + getObject().eraseExtensibleGroup(group.groupIndex()); + } } - return false; + + return result; } bool ComfortViewFactorAngles_Impl::addAngleFactor(const Surface& surface, double angleFactor) { @@ -181,17 +224,21 @@ namespace model { for (const auto& angleFactor : angleFactors) { if (!addAngleFactor(angleFactor)) { result = false; + LOG(Error, "Could not add AngleFactor " << angleFactor << " to " << briefDescription() << ". Continuing with others."); } } return result; } bool ComfortViewFactorAngles_Impl::removeAngleFactor(unsigned groupIndex) { - if (groupIndex >= numberofAngleFactors()) { - return false; + bool result = false; + + if (groupIndex < numberofAngleFactors()) { + getObject().eraseExtensibleGroup(groupIndex); + result = true; } - getObject().eraseExtensibleGroup(groupIndex); - return true; + + return result; } void ComfortViewFactorAngles_Impl::removeAllAngleFactors() { diff --git a/src/model/test/ComfortViewFactorAngles_GTest.cpp b/src/model/test/ComfortViewFactorAngles_GTest.cpp index f76e9d9144..ea03f6bdd0 100644 --- a/src/model/test/ComfortViewFactorAngles_GTest.cpp +++ b/src/model/test/ComfortViewFactorAngles_GTest.cpp @@ -12,7 +12,9 @@ #include "../ComfortViewFactorAngles.hpp" #include "../Model.hpp" #include "../PeopleDefinition.hpp" +#include "../Space.hpp" #include "../Surface.hpp" +#include "../ThermalZone.hpp" #include "../../utilities/geometry/Point3d.hpp" @@ -21,8 +23,12 @@ using namespace openstudio::model; TEST_F(ModelFixture, ComfortViewFactorAngles) { Model model; + ThermalZone thermalZone(model); + Space space(model); + ASSERT_TRUE(space.setThermalZone(thermalZone)); Point3dVector points{{0, 0, 0}, {1, 0, 0}, {1, 1, 0}}; Surface surface(points, model); + ASSERT_TRUE(surface.setSpace(space)); ComfortViewFactorAngles comfortViewFactorAngles(model); // Individual angle factors must remain within the IDD's inclusive range. @@ -52,12 +58,23 @@ TEST_F(ModelFixture, ComfortViewFactorAngles) { // New groups may bring the total to one, but cannot make it exceed one. Surface otherSurfaceInModel(points, model); + ASSERT_TRUE(otherSurfaceInModel.setSpace(space)); EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(otherSurfaceInModel, 0.5)); EXPECT_EQ(2u, comfortViewFactorAngles.numberofAngleFactors()); Surface thirdSurfaceInModel(points, model); + ASSERT_TRUE(thirdSurfaceInModel.setSpace(space)); EXPECT_FALSE(comfortViewFactorAngles.addAngleFactor(thirdSurfaceInModel, 0.01)); EXPECT_EQ(2u, comfortViewFactorAngles.numberofAngleFactors()); + // Every referenced Surface must belong to the same ThermalZone. + ThermalZone otherThermalZone(model); + Space otherSpace(model); + ASSERT_TRUE(otherSpace.setThermalZone(otherThermalZone)); + Surface surfaceInOtherThermalZone(points, model); + ASSERT_TRUE(surfaceInOtherThermalZone.setSpace(otherSpace)); + EXPECT_FALSE(comfortViewFactorAngles.addAngleFactor(surfaceInOtherThermalZone, 0.0)); + EXPECT_EQ(2u, comfortViewFactorAngles.numberofAngleFactors()); + // A Surface in another Model cannot be referenced. Model otherModel; Surface otherSurface(points, otherModel); diff --git a/src/model/test/People_GTest.cpp b/src/model/test/People_GTest.cpp index 7f446044f0..bade8cfd13 100644 --- a/src/model/test/People_GTest.cpp +++ b/src/model/test/People_GTest.cpp @@ -14,6 +14,7 @@ #include "../PeopleDefinition_Impl.hpp" #include "../ComfortViewFactorAngles.hpp" #include "../Surface.hpp" +#include "../ThermalZone.hpp" #include "../ScheduleRuleset.hpp" #include "../ScheduleDay.hpp" @@ -54,6 +55,10 @@ TEST_F(ModelFixture, People_DefaultConstructor) { Point3dVector points{{0, 0, 0}, {1, 0, 0}, {1, 1, 0}}; Surface surface(points, model); + ThermalZone thermalZone(model); + Space space(model); + ASSERT_TRUE(space.setThermalZone(thermalZone)); + ASSERT_TRUE(surface.setSpace(space)); ComfortViewFactorAngles comfortViewFactorAngles(model); EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(surface, 1.0)); From 25785c8b8ca175fd3ef6aa07d7999484b39d4f77 Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Wed, 9 Sep 2026 08:27:13 -0700 Subject: [PATCH 17/20] Reject cross-zone ComfortViewFactorAngles during translation Revalidate every referenced surface's thermal zone before generating the EnergyPlus object and cover stale surface assignments in the forward-translator tests. --- ...orwardTranslateComfortViewFactorAngles.cpp | 11 ++++++++++ .../Test/ComfortViewFactorAngles_GTest.cpp | 21 ++++++++++--------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/energyplus/ForwardTranslator/ForwardTranslateComfortViewFactorAngles.cpp b/src/energyplus/ForwardTranslator/ForwardTranslateComfortViewFactorAngles.cpp index 45d740e6fa..c5589f2472 100644 --- a/src/energyplus/ForwardTranslator/ForwardTranslateComfortViewFactorAngles.cpp +++ b/src/energyplus/ForwardTranslator/ForwardTranslateComfortViewFactorAngles.cpp @@ -8,6 +8,8 @@ #include "../../model/Model.hpp" #include "../../model/Surface.hpp" #include "../../model/Surface_Impl.hpp" +#include "../../model/Space.hpp" +#include "../../model/ThermalZone.hpp" #include "../../model/ComfortViewFactorAngles.hpp" #include "../../model/ComfortViewFactorAngles_Impl.hpp" @@ -32,9 +34,18 @@ namespace energyplus { std::vector> translatedAngleFactors; double sum = 0.0; constexpr double tolerance = 0.000001; + boost::optional thermalZone; for (const AngleFactor& angleFactor : angleFactors) { Surface surface = angleFactor.surface(); + const auto space = surface.space(); + const auto surfaceThermalZone = space ? space->thermalZone() : boost::none; + if (!surfaceThermalZone || (thermalZone && (surfaceThermalZone->handle() != thermalZone->handle()))) { + LOG(Error, modelObject.briefDescription() + << " has surfaces that are not all assigned to the same ThermalZone and will not be translated."); + return boost::none; + } + thermalZone = surfaceThermalZone; if (auto idfSurface = translateAndMapModelObject(surface)) { const double value = angleFactor.angleFactor(); sum += value; diff --git a/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp b/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp index df9d8544ea..4ca5c18a85 100644 --- a/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp +++ b/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp @@ -103,7 +103,7 @@ TEST_F(EnergyPlusFixture, ForwardTranslator_ComfortViewFactorAngles_Untranslated Surface surface(points, model); ComfortViewFactorAngles comfortViewFactorAngles(model); - EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(surface, 1.0)); + EXPECT_FALSE(comfortViewFactorAngles.addAngleFactor(surface, 1.0)); PeopleDefinition definition(model); EXPECT_TRUE(definition.setSurfaceNameAngleFactorListName(comfortViewFactorAngles)); @@ -115,7 +115,7 @@ TEST_F(EnergyPlusFixture, ForwardTranslator_ComfortViewFactorAngles_Untranslated EXPECT_TRUE(workspace.getObjectsByType(IddObjectType::ComfortViewFactorAngles).empty()); } -TEST_F(EnergyPlusFixture, ForwardTranslator_ComfortViewFactorAngles_SkipsUntranslatedSurface) { +TEST_F(EnergyPlusFixture, ForwardTranslator_ComfortViewFactorAngles_RejectsSurfacesInDifferentThermalZones) { Model model; ThermalZone zone(model); Space space(model); @@ -125,11 +125,16 @@ TEST_F(EnergyPlusFixture, ForwardTranslator_ComfortViewFactorAngles_SkipsUntrans Surface translatedSurface(points, model); translatedSurface.setName("Translated Surface"); EXPECT_TRUE(translatedSurface.setSpace(space)); - Surface untranslatedSurface(points, model); + Surface surfaceToMove(points, model); + EXPECT_TRUE(surfaceToMove.setSpace(space)); + ThermalZone otherThermalZone(model); + Space otherSpace(model); + EXPECT_TRUE(otherSpace.setThermalZone(otherThermalZone)); ComfortViewFactorAngles comfortViewFactorAngles(model); - EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(translatedSurface, 1.0)); - EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(untranslatedSurface, 0.0)); + EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(translatedSurface, 0.5)); + EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(surfaceToMove, 0.5)); + EXPECT_TRUE(surfaceToMove.setSpace(otherSpace)); PeopleDefinition definition(model); EXPECT_TRUE(definition.setSurfaceNameAngleFactorListName(comfortViewFactorAngles)); @@ -140,11 +145,7 @@ TEST_F(EnergyPlusFixture, ForwardTranslator_ComfortViewFactorAngles_SkipsUntrans Workspace workspace = forwardTranslator.translateModel(model); const auto angleFactorObjects = workspace.getObjectsByType(IddObjectType::ComfortViewFactorAngles); - ASSERT_EQ(1u, angleFactorObjects.size()); - ASSERT_EQ(1u, angleFactorObjects.front().numExtensibleGroups()); - const auto group = angleFactorObjects.front().extensibleGroups().front(); - EXPECT_EQ("Translated Surface", group.getString(0).get()); - EXPECT_DOUBLE_EQ(1.0, group.getDouble(1).get()); + EXPECT_TRUE(angleFactorObjects.empty()); } TEST_F(EnergyPlusFixture, ReverseTranslator_ComfortViewFactorAngles) { From b857c7b65cf1cbbc5e95e2ae75281ba80ba5f622 Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Wed, 9 Sep 2026 09:04:52 -0700 Subject: [PATCH 18/20] Harden ComfortViewFactorAngles and MRT target validation Reject non-finite angle factors and clear the MRT target when resetting the calculation type, with model regression coverage. --- src/model/ComfortViewFactorAngles.cpp | 2 +- src/model/PeopleDefinition.cpp | 1 + src/model/test/ComfortViewFactorAngles_GTest.cpp | 2 ++ src/model/test/People_GTest.cpp | 4 ++++ 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/model/ComfortViewFactorAngles.cpp b/src/model/ComfortViewFactorAngles.cpp index a647774c2a..e52da91ab8 100644 --- a/src/model/ComfortViewFactorAngles.cpp +++ b/src/model/ComfortViewFactorAngles.cpp @@ -24,7 +24,7 @@ namespace openstudio { namespace model { AngleFactor::AngleFactor(const Surface& surface, double angleFactor) : m_surface(surface), m_angleFactor(angleFactor) { - if ((m_angleFactor < 0.0) || (m_angleFactor > 1.0)) { + if (!((m_angleFactor >= 0.0) && (m_angleFactor <= 1.0))) { LOG_AND_THROW("Angle Factor must be between 0 and 1."); } } diff --git a/src/model/PeopleDefinition.cpp b/src/model/PeopleDefinition.cpp index 812e56f537..6e9b5d3f54 100644 --- a/src/model/PeopleDefinition.cpp +++ b/src/model/PeopleDefinition.cpp @@ -269,6 +269,7 @@ namespace model { void PeopleDefinition_Impl::resetMeanRadiantTemperatureCalculationType() { bool result = setString(OS_People_DefinitionFields::MeanRadiantTemperatureCalculationType, ""); OS_ASSERT(result); + resetSurfaceNameAngleFactorListName(); } bool PeopleDefinition_Impl::setSurfaceNameAngleFactorListName(const ModelObject& modelObject) { diff --git a/src/model/test/ComfortViewFactorAngles_GTest.cpp b/src/model/test/ComfortViewFactorAngles_GTest.cpp index ea03f6bdd0..dabd18884e 100644 --- a/src/model/test/ComfortViewFactorAngles_GTest.cpp +++ b/src/model/test/ComfortViewFactorAngles_GTest.cpp @@ -5,6 +5,7 @@ #include +#include #include #include "ModelFixture.hpp" @@ -34,6 +35,7 @@ TEST_F(ModelFixture, ComfortViewFactorAngles) { // Individual angle factors must remain within the IDD's inclusive range. EXPECT_THROW(AngleFactor(surface, -0.01), openstudio::Exception); EXPECT_THROW(AngleFactor(surface, 1.01), openstudio::Exception); + EXPECT_THROW(AngleFactor(surface, std::numeric_limits::quiet_NaN()), openstudio::Exception); // A valid group is stored, indexed by its Surface, and returned as an AngleFactor. EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(surface, 0.25)); diff --git a/src/model/test/People_GTest.cpp b/src/model/test/People_GTest.cpp index bade8cfd13..95f1858d9e 100644 --- a/src/model/test/People_GTest.cpp +++ b/src/model/test/People_GTest.cpp @@ -76,6 +76,10 @@ TEST_F(ModelFixture, People_DefaultConstructor) { EXPECT_EQ("SurfaceWeighted", definition.meanRadiantTemperatureCalculationType()); EXPECT_TRUE(definition.surfaceNameAngleFactorListName()); + definition.resetMeanRadiantTemperatureCalculationType(); + EXPECT_EQ("EnclosureAveraged", definition.meanRadiantTemperatureCalculationType()); + EXPECT_FALSE(definition.surfaceNameAngleFactorListName()); + // Backward compat EXPECT_TRUE(definition.setMeanRadiantTemperatureCalculationType("ZoneAveraged")); EXPECT_EQ("EnclosureAveraged", definition.meanRadiantTemperatureCalculationType()); From 24d1ad7568b667d8c804e6abca27695c101b9a0a Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Wed, 9 Sep 2026 09:04:58 -0700 Subject: [PATCH 19/20] Migrate legacy People MRT targets to definitions Move unambiguous 3.11 People MRT targets to PeopleDefinition during version translation, report shared-definition conflicts, and cover the migrated surface target. --- src/osversion/VersionTranslator.cpp | 32 +++++++++++++ src/osversion/test/3_12_0/test_vt_People.osm | 45 ++++++++++++------- src/osversion/test/3_12_0/test_vt_People.rb | 10 +++++ .../test/VersionTranslator_GTest.cpp | 5 ++- 4 files changed, 75 insertions(+), 17 deletions(-) diff --git a/src/osversion/VersionTranslator.cpp b/src/osversion/VersionTranslator.cpp index 5fb4f063e1..50babc026e 100644 --- a/src/osversion/VersionTranslator.cpp +++ b/src/osversion/VersionTranslator.cpp @@ -10234,6 +10234,27 @@ namespace osversion { IdfFile targetIdf(idd_3_12_0.iddFile()); ss << targetIdf.versionObject().get(); + // The MRT target moves from each People instance to its shared PeopleDefinition. Preserve it when all instances agree, + // and record conflicting definitions because one target cannot represent multiple legacy values. + std::map peopleDefinitionMRTTargets; + std::set conflictingPeopleDefinitions; + for (const IdfObject& object : idf_3_11_0.objects()) { + if (object.iddObject().name() != "OS:People") { + continue; + } + + const auto peopleDefinitionHandle = object.getString(2); + if (!peopleDefinitionHandle) { + continue; + } + + const std::string mrtTarget = object.getString(6).get_value_or(""); + const auto [it, inserted] = peopleDefinitionMRTTargets.emplace(*peopleDefinitionHandle, mrtTarget); + if (!inserted && (it->second != mrtTarget)) { + conflictingPeopleDefinitions.insert(*peopleDefinitionHandle); + } + } + for (const IdfObject& object : idf_3_11_0.objects()) { auto iddname = object.iddObject().name(); @@ -10328,6 +10349,17 @@ namespace osversion { } } + const auto peopleDefinitionHandle = object.getString(0); + if (peopleDefinitionHandle && (conflictingPeopleDefinitions.find(*peopleDefinitionHandle) == conflictingPeopleDefinitions.end())) { + if (const auto it = peopleDefinitionMRTTargets.find(*peopleDefinitionHandle); + (it != peopleDefinitionMRTTargets.end()) && !it->second.empty()) { + newObject.setString(11, it->second); + } + } else if (peopleDefinitionHandle) { + LOG(Error, "Cannot migrate Surface Name/Angle Factor List Name for " << object.briefDescription() + << " because its People instances have conflicting targets."); + } + ss << newObject; m_refactored.emplace_back(std::move(object), std::move(newObject)); diff --git a/src/osversion/test/3_12_0/test_vt_People.osm b/src/osversion/test/3_12_0/test_vt_People.osm index 5797f4dda9..d842c8553d 100644 --- a/src/osversion/test/3_12_0/test_vt_People.osm +++ b/src/osversion/test/3_12_0/test_vt_People.osm @@ -1,35 +1,48 @@ OS:Version, - {b3d95565-1790-4500-aab1-06a7e08376b0}, !- Handle + {7fcc2e3b-305f-4aba-93c9-827304d6928c}, !- Handle 3.11.0; !- Version Identifier OS:People:Definition, - {4af0ce1e-eaa2-4451-b35d-777777777777}, !- Handle + {d28a1203-e804-40b0-875d-bfca06094583}, !- Handle People Definition 1, !- Name - People, !- Number of People Calculation Method - 1, !- Number of People - , !- People per Space Floor Area - , !- Space Floor Area per Person + People, !- Number of People Calculation Method + 0, !- Number of People {people} + , !- People per Space Floor Area {person/m2} + , !- Space Floor Area per Person {m2/person} 0.3, !- Fraction Radiant - autocalculate, !- Sensible Heat Fraction - 3.82e-08, !- Carbon Dioxide Generation Rate - No, !- Enable ASHRAE 55 Comfort Warnings + , !- Sensible Heat Fraction + , !- Carbon Dioxide Generation Rate {m3/s-W} + , !- Enable ASHRAE 55 Comfort Warnings SurfaceWeighted, !- Mean Radiant Temperature Calculation Type Fanger; !- Thermal Comfort Model 1 Type OS:People, - {c69a6d60-b110-4322-8888-999999999999}, !- Handle + {e54d1270-e739-46ec-96c4-7d4bf02373d0}, !- Handle People 1, !- Name - {4af0ce1e-eaa2-4451-b35d-777777777777}, !- People Definition Name + {d28a1203-e804-40b0-875d-bfca06094583}, !- People Definition Name , !- Space or SpaceType Name , !- Number of People Schedule Name , !- Activity Level Schedule Name - , !- Surface Name/Angle Factor List Name + {224e6e57-50bc-4fc2-8443-e38791843688}, !- Surface Name/Angle Factor List Name , !- Work Efficiency Schedule Name ClothingInsulationSchedule, !- Clothing Insulation Calculation Method , !- Clothing Insulation Calculation Method Schedule Name , !- Clothing Insulation Schedule Name , !- Air Velocity Schedule Name - 1, !- Multiplier - , !- Ankle Level Air Velocity Schedule Name - 15.56, !- Cold Stress Temperature Threshold - 30; !- Heat Stress Temperature Threshold + 1; !- Multiplier + +OS:Surface, + {224e6e57-50bc-4fc2-8443-e38791843688}, !- Handle + Radiant Surface, !- Name + RoofCeiling, !- Surface Type + , !- Construction Name + , !- Space Name + Outdoors, !- Outside Boundary Condition + , !- Outside Boundary Condition Object + SunExposed, !- Sun Exposure + WindExposed, !- Wind Exposure + , !- View Factor to Ground + , !- Number of Vertices + 0, 0, 0, !- X,Y,Z Vertex 1 {m} + 1, 0, 0, !- X,Y,Z Vertex 2 {m} + 1, 1, 0; !- X,Y,Z Vertex 3 {m} diff --git a/src/osversion/test/3_12_0/test_vt_People.rb b/src/osversion/test/3_12_0/test_vt_People.rb index c0301810f0..941821e09c 100644 --- a/src/osversion/test/3_12_0/test_vt_People.rb +++ b/src/osversion/test/3_12_0/test_vt_People.rb @@ -6,7 +6,17 @@ people_def = PeopleDefinition.new(m) people_def.setMeanRadiantTemperatureCalculationType('SurfaceWeighted') +# Field 11 is the first thermal comfort model type in the v3.11 PeopleDefinition IDD. +people_def.setString(11, 'Fanger') people = People.new(people_def) people.setClothingInsulationCalculationMethod('ClothingInsulationSchedule') +points = OpenStudio::Point3dVector.new +points << OpenStudio::Point3d.new(0, 0, 0) +points << OpenStudio::Point3d.new(1, 0, 0) +points << OpenStudio::Point3d.new(1, 1, 0) +surface = Surface.new(points, m) +surface.setName('Radiant Surface') +# The legacy v3.11 API does not expose this setter; field 6 is Surface Name/Angle Factor List Name. +people.setString(6, surface.handle.to_s) m.save('test_vt_People.osm', true) diff --git a/src/osversion/test/VersionTranslator_GTest.cpp b/src/osversion/test/VersionTranslator_GTest.cpp index 370fd8a44e..1dcc8e9e8c 100644 --- a/src/osversion/test/VersionTranslator_GTest.cpp +++ b/src/osversion/test/VersionTranslator_GTest.cpp @@ -5113,7 +5113,10 @@ TEST_F(OSVersionFixture, update_3_11_0_to_3_12_0_People) { ASSERT_EQ(1u, definitions.size()); const auto& definition = definitions.front(); EXPECT_EQ("SurfaceWeighted", definition.getString(10).get()); // Mean Radiant Temperature Calculation Type - EXPECT_TRUE(definition.isEmpty(11)); // Surface Name/Angle Factor List Name + const auto mrtTarget = definition.getTarget(11); // Surface Name/Angle Factor List Name + ASSERT_TRUE(mrtTarget); + EXPECT_EQ("OS:Surface", mrtTarget->iddObject().name()); + EXPECT_EQ("Radiant Surface", mrtTarget->nameString()); EXPECT_EQ("Fanger", definition.getString(12).get()); // Thermal Comfort Model 1 Type std::vector people = model->getObjectsByType("OS:People"); From f940f2ec028a4e82af2860d4883c892db85b07d1 Mon Sep 17 00:00:00 2001 From: Joe Robertson Date: Wed, 9 Sep 2026 09:30:14 -0700 Subject: [PATCH 20/20] Support all heat-transfer surfaces in ComfortViewFactorAngles Accept Surface, SubSurface, and InternalMass targets from AllHeatTranSurfNames, preserve same-zone validation, and cover model and EnergyPlus translation behavior. --- resources/model/OpenStudio.idd | 2 +- ...orwardTranslateComfortViewFactorAngles.cpp | 12 ++++- ...everseTranslateComfortViewFactorAngles.cpp | 4 +- .../Test/ComfortViewFactorAngles_GTest.cpp | 32 +++++++++++ src/model/ComfortViewFactorAngles.cpp | 53 ++++++++++++++----- src/model/ComfortViewFactorAngles.hpp | 11 ++-- src/model/ComfortViewFactorAngles_Impl.hpp | 8 +-- .../test/ComfortViewFactorAngles_GTest.cpp | 28 ++++++++++ 8 files changed, 122 insertions(+), 28 deletions(-) diff --git a/resources/model/OpenStudio.idd b/resources/model/OpenStudio.idd index d063f3bc2b..90c7d41551 100644 --- a/resources/model/OpenStudio.idd +++ b/resources/model/OpenStudio.idd @@ -3987,7 +3987,7 @@ OS:ComfortViewFactorAngles, \reference AllHeatTranAngFacNames A3, \field Surface 1 Name \type object-list - \object-list SurfaceNames + \object-list AllHeatTranSurfNames \begin-extensible \required-field N1; \field Angle Factor 1 diff --git a/src/energyplus/ForwardTranslator/ForwardTranslateComfortViewFactorAngles.cpp b/src/energyplus/ForwardTranslator/ForwardTranslateComfortViewFactorAngles.cpp index c5589f2472..a1b70a401d 100644 --- a/src/energyplus/ForwardTranslator/ForwardTranslateComfortViewFactorAngles.cpp +++ b/src/energyplus/ForwardTranslator/ForwardTranslateComfortViewFactorAngles.cpp @@ -8,6 +8,9 @@ #include "../../model/Model.hpp" #include "../../model/Surface.hpp" #include "../../model/Surface_Impl.hpp" +#include "../../model/InternalMass.hpp" +#include "../../model/InternalMass_Impl.hpp" +#include "../../model/PlanarSurface.hpp" #include "../../model/Space.hpp" #include "../../model/ThermalZone.hpp" #include "../../model/ComfortViewFactorAngles.hpp" @@ -37,8 +40,13 @@ namespace energyplus { boost::optional thermalZone; for (const AngleFactor& angleFactor : angleFactors) { - Surface surface = angleFactor.surface(); - const auto space = surface.space(); + ModelObject surface = angleFactor.surface(); + boost::optional space; + if (auto planarSurface = surface.optionalCast()) { + space = planarSurface->space(); + } else if (auto internalMass = surface.optionalCast()) { + space = internalMass->space(); + } const auto surfaceThermalZone = space ? space->thermalZone() : boost::none; if (!surfaceThermalZone || (thermalZone && (surfaceThermalZone->handle() != thermalZone->handle()))) { LOG(Error, modelObject.briefDescription() diff --git a/src/energyplus/ReverseTranslator/ReverseTranslateComfortViewFactorAngles.cpp b/src/energyplus/ReverseTranslator/ReverseTranslateComfortViewFactorAngles.cpp index 23ef511b39..10547fb24d 100644 --- a/src/energyplus/ReverseTranslator/ReverseTranslateComfortViewFactorAngles.cpp +++ b/src/energyplus/ReverseTranslator/ReverseTranslateComfortViewFactorAngles.cpp @@ -42,9 +42,7 @@ namespace energyplus { continue; } if (auto modelObject = translateAndMapWorkspaceObject(*surfaceTarget)) { - if (auto surface = modelObject->optionalCast()) { - comfortViewFactorAngles.addAngleFactor(*surface, *angleFactor); - } + comfortViewFactorAngles.addAngleFactor(*modelObject, *angleFactor); } } diff --git a/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp b/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp index 4ca5c18a85..38f2627e25 100644 --- a/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp +++ b/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp @@ -16,6 +16,7 @@ #include "../../model/PeopleDefinition.hpp" #include "../../model/Space.hpp" #include "../../model/Surface.hpp" +#include "../../model/SubSurface.hpp" #include "../../model/ThermalZone.hpp" #include "../../utilities/idf/IdfExtensibleGroup.hpp" @@ -62,6 +63,37 @@ TEST_F(EnergyPlusFixture, ForwardTranslator_ComfortViewFactorAngles) { EXPECT_DOUBLE_EQ(1.0, group.getDouble(1).get()); } +TEST_F(EnergyPlusFixture, ForwardTranslator_ComfortViewFactorAngles_SubSurface) { + Model model; + ThermalZone zone(model); + Space space(model); + ASSERT_TRUE(space.setThermalZone(zone)); + + Point3dVector points{{0, 0, 0}, {1, 0, 0}, {1, 1, 0}}; + Surface surface(points, model); + ASSERT_TRUE(surface.setSpace(space)); + SubSurface subSurface(points, model); + subSurface.setName("Radiant SubSurface"); + ASSERT_TRUE(subSurface.setSurface(surface)); + + ComfortViewFactorAngles comfortViewFactorAngles(model); + ASSERT_TRUE(comfortViewFactorAngles.addAngleFactor(subSurface, 1.0)); + + PeopleDefinition definition(model); + ASSERT_TRUE(definition.setSurfaceNameAngleFactorListName(comfortViewFactorAngles)); + People people(definition); + ASSERT_TRUE(people.setSpace(space)); + + ForwardTranslator forwardTranslator; + Workspace workspace = forwardTranslator.translateModel(model); + + const auto angleFactorObjects = workspace.getObjectsByType(IddObjectType::ComfortViewFactorAngles); + ASSERT_EQ(1u, angleFactorObjects.size()); + const auto group = angleFactorObjects.front().extensibleGroups().front(); + EXPECT_EQ("Radiant SubSurface", group.getString(0).get()); + EXPECT_DOUBLE_EQ(1.0, group.getDouble(1).get()); +} + TEST_F(EnergyPlusFixture, ForwardTranslator_ComfortViewFactorAngles_Empty) { Model model; ComfortViewFactorAngles comfortViewFactorAngles(model); diff --git a/src/model/ComfortViewFactorAngles.cpp b/src/model/ComfortViewFactorAngles.cpp index e52da91ab8..1d79577c7b 100644 --- a/src/model/ComfortViewFactorAngles.cpp +++ b/src/model/ComfortViewFactorAngles.cpp @@ -8,6 +8,11 @@ #include "Surface.hpp" #include "Surface_Impl.hpp" +#include "SubSurface.hpp" +#include "SubSurface_Impl.hpp" +#include "InternalMass.hpp" +#include "InternalMass_Impl.hpp" +#include "PlanarSurface.hpp" #include "Space.hpp" #include "ThermalZone.hpp" #include "Model.hpp" @@ -23,13 +28,31 @@ namespace openstudio { namespace model { - AngleFactor::AngleFactor(const Surface& surface, double angleFactor) : m_surface(surface), m_angleFactor(angleFactor) { + namespace { + // Resolves the Space that owns a valid EnergyPlus heat-transfer surface target. + boost::optional spaceForSurface(const ModelObject& surface) { + if (auto planarSurface = surface.optionalCast()) { + return planarSurface->space(); + } + if (auto internalMass = surface.optionalCast()) { + return internalMass->space(); + } + return boost::none; + } + + // Matches the model objects accepted by EnergyPlus AllHeatTranSurfNames. + bool isHeatTransferSurface(const ModelObject& surface) { + return surface.optionalCast() || surface.optionalCast() || surface.optionalCast(); + } + } // namespace + + AngleFactor::AngleFactor(const ModelObject& surface, double angleFactor) : m_surface(surface), m_angleFactor(angleFactor) { if (!((m_angleFactor >= 0.0) && (m_angleFactor <= 1.0))) { LOG_AND_THROW("Angle Factor must be between 0 and 1."); } } - Surface AngleFactor::surface() const { + ModelObject AngleFactor::surface() const { return m_surface; } @@ -82,7 +105,7 @@ namespace model { return numExtensibleGroups(); } - boost::optional ComfortViewFactorAngles_Impl::angleFactorIndex(const Surface& surface) const { + boost::optional ComfortViewFactorAngles_Impl::angleFactorIndex(const ModelObject& surface) const { for (unsigned i = 0; i < numberofAngleFactors(); ++i) { if (auto existingSurface = getSurface(i); existingSurface && (existingSurface->handle() == surface.handle())) { return i; @@ -91,8 +114,8 @@ namespace model { return boost::none; } - boost::optional ComfortViewFactorAngles_Impl::getSurface(unsigned groupIndex) const { - boost::optional result; + boost::optional ComfortViewFactorAngles_Impl::getSurface(unsigned groupIndex) const { + boost::optional result; if (groupIndex >= numberofAngleFactors()) { LOG(Error, "Asked to get Surface with index " << groupIndex << ", but " << briefDescription() << " has just " << numberofAngleFactors() @@ -100,7 +123,7 @@ namespace model { return result; } auto group = getExtensibleGroup(groupIndex).cast(); - result = group.getModelObjectTarget(OS_ComfortViewFactorAnglesExtensibleFields::SurfaceName); + result = group.getModelObjectTarget(OS_ComfortViewFactorAnglesExtensibleFields::SurfaceName); if (!result) { LOG(Error, "Could not retrieve Surface Name for extensible group " << group.groupIndex() << "."); @@ -134,13 +157,19 @@ namespace model { bool ComfortViewFactorAngles_Impl::addAngleFactor(const AngleFactor& angleFactor) { bool result = false; - Surface surface = angleFactor.surface(); + ModelObject surface = angleFactor.surface(); if (surface.model() != model()) { LOG(Error, "Cannot add a Surface from another Model to " << briefDescription() << "."); return result; } - boost::optional space = surface.space(); + if (!isHeatTransferSurface(surface)) { + LOG(Error, "Cannot add " << surface.briefDescription() << " to " << briefDescription() + << " because it is not a Surface, SubSurface, or InternalMass object."); + return result; + } + + boost::optional space = spaceForSurface(surface); if (!space) { LOG(Error, "Cannot add " << surface.briefDescription() << " to " << briefDescription() << " because it is not assigned to a Space."); return result; @@ -154,7 +183,7 @@ namespace model { } for (const auto& existingAngleFactor : angleFactors()) { - const auto existingSpace = existingAngleFactor.surface().space(); + const auto existingSpace = spaceForSurface(existingAngleFactor.surface()); const auto existingThermalZone = existingSpace ? existingSpace->thermalZone() : boost::none; if (!existingThermalZone || (existingThermalZone->handle() != thermalZone->handle())) { LOG(Error, "Cannot add " << surface.briefDescription() << " to " << briefDescription() << " because it is assigned to ThermalZone '" @@ -215,7 +244,7 @@ namespace model { return result; } - bool ComfortViewFactorAngles_Impl::addAngleFactor(const Surface& surface, double angleFactor) { + bool ComfortViewFactorAngles_Impl::addAngleFactor(const ModelObject& surface, double angleFactor) { return addAngleFactor(AngleFactor(surface, angleFactor)); } @@ -263,7 +292,7 @@ namespace model { return getImpl()->numberofAngleFactors(); } - boost::optional ComfortViewFactorAngles::angleFactorIndex(const Surface& surface) const { + boost::optional ComfortViewFactorAngles::angleFactorIndex(const ModelObject& surface) const { return getImpl()->angleFactorIndex(surface); } @@ -275,7 +304,7 @@ namespace model { return getImpl()->addAngleFactor(angleFactor); } - bool ComfortViewFactorAngles::addAngleFactor(const Surface& surface, double angleFactor) { + bool ComfortViewFactorAngles::addAngleFactor(const ModelObject& surface, double angleFactor) { return getImpl()->addAngleFactor(surface, angleFactor); } diff --git a/src/model/ComfortViewFactorAngles.hpp b/src/model/ComfortViewFactorAngles.hpp index 789fa8085e..1d2b83018c 100644 --- a/src/model/ComfortViewFactorAngles.hpp +++ b/src/model/ComfortViewFactorAngles.hpp @@ -8,7 +8,6 @@ #include "ModelAPI.hpp" #include "ModelObject.hpp" -#include "Surface.hpp" namespace openstudio { namespace model { @@ -20,16 +19,16 @@ namespace model { class MODEL_API AngleFactor { public: - AngleFactor(const Surface& surface, double angleFactor); + AngleFactor(const ModelObject& surface, double angleFactor); - Surface surface() const; + ModelObject surface() const; double angleFactor() const; bool operator==(const AngleFactor& other) const = default; bool operator!=(const AngleFactor& other) const; private: - Surface m_surface; + ModelObject m_surface; double m_angleFactor; REGISTER_LOGGER("openstudio.model.AngleFactor"); }; @@ -48,11 +47,11 @@ namespace model { std::vector angleFactors() const; unsigned int numberofAngleFactors() const; - boost::optional angleFactorIndex(const Surface& surface) const; + boost::optional angleFactorIndex(const ModelObject& surface) const; boost::optional getAngleFactor(unsigned groupIndex) const; bool addAngleFactor(const AngleFactor& angleFactor); - bool addAngleFactor(const Surface& surface, double angleFactor); + bool addAngleFactor(const ModelObject& surface, double angleFactor); bool addAngleFactors(const std::vector& angleFactors); void removeAngleFactor(int groupIndex); void removeAllAngleFactors(); diff --git a/src/model/ComfortViewFactorAngles_Impl.hpp b/src/model/ComfortViewFactorAngles_Impl.hpp index 1dce8ffb6f..8c301ab42e 100644 --- a/src/model/ComfortViewFactorAngles_Impl.hpp +++ b/src/model/ComfortViewFactorAngles_Impl.hpp @@ -13,7 +13,7 @@ namespace openstudio { namespace model { class AngleFactor; - class Surface; + class ModelObject; namespace detail { class MODEL_API ComfortViewFactorAngles_Impl : public ModelObject_Impl @@ -30,11 +30,11 @@ namespace model { std::vector angleFactors() const; unsigned int numberofAngleFactors() const; - boost::optional angleFactorIndex(const Surface& surface) const; + boost::optional angleFactorIndex(const ModelObject& surface) const; boost::optional getAngleFactor(unsigned groupIndex) const; bool addAngleFactor(const AngleFactor& angleFactor); - bool addAngleFactor(const Surface& surface, double angleFactor); + bool addAngleFactor(const ModelObject& surface, double angleFactor); bool addAngleFactors(const std::vector& angleFactors); bool removeAngleFactor(unsigned groupIndex); void removeAllAngleFactors(); @@ -42,7 +42,7 @@ namespace model { private: REGISTER_LOGGER("openstudio.model.ComfortViewFactorAngles"); - boost::optional getSurface(unsigned groupIndex) const; + boost::optional getSurface(unsigned groupIndex) const; boost::optional getAngleFactorValue(unsigned groupIndex) const; }; } diff --git a/src/model/test/ComfortViewFactorAngles_GTest.cpp b/src/model/test/ComfortViewFactorAngles_GTest.cpp index dabd18884e..7be2c4596c 100644 --- a/src/model/test/ComfortViewFactorAngles_GTest.cpp +++ b/src/model/test/ComfortViewFactorAngles_GTest.cpp @@ -11,10 +11,13 @@ #include "ModelFixture.hpp" #include "../ComfortViewFactorAngles.hpp" +#include "../InternalMass.hpp" +#include "../InternalMassDefinition.hpp" #include "../Model.hpp" #include "../PeopleDefinition.hpp" #include "../Space.hpp" #include "../Surface.hpp" +#include "../SubSurface.hpp" #include "../ThermalZone.hpp" #include "../../utilities/geometry/Point3d.hpp" @@ -103,3 +106,28 @@ TEST_F(ModelFixture, ComfortViewFactorAngles) { comfortViewFactorAngles.removeAllAngleFactors(); EXPECT_TRUE(comfortViewFactorAngles.angleFactors().empty()); } + +TEST_F(ModelFixture, ComfortViewFactorAngles_HeatTransferSurfaceTargets) { + Model model; + ThermalZone thermalZone(model); + Space space(model); + ASSERT_TRUE(space.setThermalZone(thermalZone)); + Point3dVector points{{0, 0, 0}, {1, 0, 0}, {1, 1, 0}}; + + Surface surface(points, model); + ASSERT_TRUE(surface.setSpace(space)); + SubSurface subSurface(points, model); + ASSERT_TRUE(subSurface.setSurface(surface)); + InternalMassDefinition internalMassDefinition(model); + InternalMass internalMass(internalMassDefinition); + ASSERT_TRUE(internalMass.setSpace(space)); + + ComfortViewFactorAngles comfortViewFactorAngles(model); + EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(subSurface, 0.5)); + EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(internalMass, 0.5)); + + const auto angleFactors = comfortViewFactorAngles.angleFactors(); + ASSERT_EQ(2u, angleFactors.size()); + EXPECT_EQ(subSurface.handle(), angleFactors[0].surface().handle()); + EXPECT_EQ(internalMass.handle(), angleFactors[1].surface().handle()); +}