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 diff --git a/resources/model/OpenStudio.idd b/resources/model/OpenStudio.idd index 2e4c37e634..4c9feba74a 100644 --- a/resources/model/OpenStudio.idd +++ b/resources/model/OpenStudio.idd @@ -3972,6 +3972,27 @@ OS:People:Definition, \key CoolingEffectASH55 \key AnkleDraftASH55 +OS:ComfortViewFactorAngles, + \min-fields 4 + \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 AllHeatTranSurfNames + \begin-extensible + \required-field + N1; \field Angle Factor 1 + \type real + \minimum 0.0 + \maximum 1.0 + \required-field + OS:Lights:Definition, \min-fields 1 A1, \field Handle @@ -7080,7 +7101,6 @@ OS:People, \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 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 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..a1b70a401d --- /dev/null +++ b/src/energyplus/ForwardTranslator/ForwardTranslateComfortViewFactorAngles.cpp @@ -0,0 +1,86 @@ +/*********************************************************************************************************************** +* OpenStudio(R), Copyright (c) Alliance for Energy Innovation, LLC. +* See also https://openstudio.net/license +***********************************************************************************************************************/ + +#include "../ForwardTranslator.hpp" + +#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" +#include "../../model/ComfortViewFactorAngles_Impl.hpp" + +#include "../../utilities/idf/IdfExtensibleGroup.hpp" +#include "../../utilities/math/FloatCompare.hpp" + +#include +#include + +using namespace openstudio::model; + +namespace openstudio { +namespace energyplus { + + boost::optional ForwardTranslator::translateComfortViewFactorAngles(ComfortViewFactorAngles& modelObject) { + 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; + constexpr double tolerance = 0.000001; + boost::optional thermalZone; + + for (const AngleFactor& angleFactor : angleFactors) { + 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() + << " 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; + translatedAngleFactors.emplace_back(idfSurface->nameString(), value); + } else { + 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(); + group.setString(ComfortViewFactorAnglesExtensibleFields::SurfaceName, surfaceName); + group.setDouble(ComfortViewFactorAnglesExtensibleFields::AngleFactor, angleFactor); + } + return idfObject; + } + +} // namespace energyplus +} // namespace openstudio diff --git a/src/energyplus/ForwardTranslator/ForwardTranslatePeople.cpp b/src/energyplus/ForwardTranslator/ForwardTranslatePeople.cpp index e002e5b8a2..01880adb4d 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" @@ -18,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" @@ -102,11 +107,50 @@ namespace energyplus { } } - if (!definition.isMeanRadiantTemperatureCalculationTypeDefaulted()) { - idfObject.setString(PeopleFields::MeanRadiantTemperatureCalculationType, definition.meanRadiantTemperatureCalculationType()); + std::string mrtType = definition.meanRadiantTemperatureCalculationType(); + if (auto target = modelObject.surfaceNameAngleFactorListName()) { + if (target->optionalCast()) { + if (!istringEqual(mrtType, "SurfaceWeighted")) { + LOG(Warn, modelObject.briefDescription() << " references " << target->briefDescription() + << ", but its PeopleDefinition has Mean Radiant Temperature Calculation Type '" << mrtType + << "'. Using 'SurfaceWeighted'."); + } + if (auto idfTarget = translateAndMapModelObject(*target)) { + mrtType = "SurfaceWeighted"; + idfObject.setString(PeopleFields::SurfaceName_AngleFactorListName, idfTarget->nameString()); + } else { + LOG(Warn, "Could not translate " << target->briefDescription() << " referenced by " << modelObject.briefDescription() + << "; using 'EnclosureAveraged' for Mean Radiant Temperature Calculation Type."); + mrtType = "EnclosureAveraged"; + } + } else if (target->optionalCast()) { + if (!istringEqual(mrtType, "AngleFactor")) { + LOG(Warn, modelObject.briefDescription() << " references " << target->briefDescription() + << ", but its PeopleDefinition has Mean Radiant Temperature Calculation Type '" << mrtType + << "'. Using 'AngleFactor'."); + } + if (auto idfTarget = translateAndMapModelObject(*target)) { + mrtType = "AngleFactor"; + idfObject.setString(PeopleFields::SurfaceName_AngleFactorListName, idfTarget->nameString()); + } else { + LOG(Warn, "Could not translate " << target->briefDescription() << " referenced by " << modelObject.briefDescription() + << "; using 'EnclosureAveraged' for Mean Radiant Temperature Calculation Type."); + mrtType = "EnclosureAveraged"; + } + } else { + LOG(Warn, modelObject.briefDescription() << " references unsupported Surface Name/Angle Factor List Name object " + << target->briefDescription() << "; using 'EnclosureAveraged' for Mean Radiant Temperature Calculation Type."); + mrtType = "EnclosureAveraged"; + } + } else if (!istringEqual(mrtType, "EnclosureAveraged")) { + LOG(Warn, modelObject.briefDescription() << " has Mean Radiant Temperature Calculation Type '" << mrtType + << "' but no Surface Name/Angle Factor List Name; using '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())) { 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..10547fb24d --- /dev/null +++ b/src/energyplus/ReverseTranslator/ReverseTranslateComfortViewFactorAngles.cpp @@ -0,0 +1,53 @@ +/*********************************************************************************************************************** +* OpenStudio(R), Copyright (c) Alliance for Energy Innovation, LLC. +* See also https://openstudio.net/license +***********************************************************************************************************************/ + +#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; + +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)) { + comfortViewFactorAngles.addAngleFactor(*modelObject, *angleFactor); + } + } + + return comfortViewFactorAngles; + } + +} // namespace energyplus +} // namespace openstudio diff --git a/src/energyplus/ReverseTranslator/ReverseTranslatePeople.cpp b/src/energyplus/ReverseTranslator/ReverseTranslatePeople.cpp index 3b48d693f5..663046d94e 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" @@ -102,11 +103,6 @@ namespace energyplus { definition.setMeanRadiantTemperatureCalculationType(*s); } - OptionalWorkspaceObject target = workspaceObject.getTarget(openstudio::PeopleFields::SurfaceName_AngleFactorListName); - if (target) { - LOG(Error, "SurfaceName_AngleFactorListName not currently imported"); - } - // As of 22.2.0, this is no longer possible to make this an extensible field // because E+ added 3 regular fields at the end (eg: Ankle Level Velocity Schedule Name) for (unsigned i = PeopleFields::ThermalComfortModel1Type, k = 0; i <= PeopleFields::ThermalComfortModel7Type; ++i) { @@ -119,6 +115,13 @@ namespace energyplus { // create the instance People people(definition); + OptionalWorkspaceObject target = workspaceObject.getTarget(openstudio::PeopleFields::SurfaceName_AngleFactorListName); + if (target) { + if (auto modelObject = translateAndMapWorkspaceObject(*target)) { + people.setSurfaceNameAngleFactorListName(*modelObject); + } + } + s = workspaceObject.name(); if (s) { people.setName(*s); diff --git a/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp b/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp new file mode 100644 index 0000000000..6faf8692b9 --- /dev/null +++ b/src/energyplus/Test/ComfortViewFactorAngles_GTest.cpp @@ -0,0 +1,209 @@ +/*********************************************************************************************************************** +* 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/ComfortViewFactorAngles_Impl.hpp" +#include "../../model/InternalMass.hpp" +#include "../../model/InternalMassDefinition.hpp" +#include "../../model/Model.hpp" +#include "../../model/People.hpp" +#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" +#include "../../utilities/idf/WorkspaceExtensibleGroup.hpp" +#include "../../utilities/geometry/Point3d.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)); + SubSurface subSurface(points, model); + subSurface.setName("Radiant SubSurface"); + EXPECT_TRUE(subSurface.setSurface(surface)); + InternalMassDefinition internalMassDefinition(model); + InternalMass internalMass(internalMassDefinition); + internalMass.setName("Radiant InternalMass"); + EXPECT_TRUE(internalMass.setSpace(space)); + + ComfortViewFactorAngles comfortViewFactorAngles(model); + comfortViewFactorAngles.setName("Angle Factors"); + EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(surface, 0.25)); + EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(subSurface, 0.25)); + EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(internalMass, 0.5)); + + PeopleDefinition definition(model); + People people(definition); + EXPECT_TRUE(people.setSurfaceNameAngleFactorListName(comfortViewFactorAngles)); + EXPECT_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& angleFactorObject = angleFactorObjects.front(); + EXPECT_EQ("Angle Factors", angleFactorObject.nameString()); + ASSERT_EQ(3u, angleFactorObject.numExtensibleGroups()); + const auto groups = angleFactorObject.extensibleGroups(); + EXPECT_EQ("Radiant Surface", groups[0].getString(0).get()); + EXPECT_DOUBLE_EQ(0.25, groups[0].getDouble(1).get()); + EXPECT_EQ("Radiant SubSurface", groups[1].getString(0).get()); + EXPECT_DOUBLE_EQ(0.25, groups[1].getDouble(1).get()); + EXPECT_EQ("Radiant InternalMass", groups[2].getString(0).get()); + EXPECT_DOUBLE_EQ(0.5, groups[2].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); + 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.addAngleFactor(surface, 0.5)); + + PeopleDefinition definition(model); + People people(definition); + EXPECT_TRUE(people.setSurfaceNameAngleFactorListName(comfortViewFactorAngles)); + EXPECT_TRUE(people.setSpace(space)); + + ForwardTranslator forwardTranslator; + 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_FALSE(comfortViewFactorAngles.addAngleFactor(surface, 1.0)); + + PeopleDefinition definition(model); + People people(definition); + EXPECT_TRUE(people.setSurfaceNameAngleFactorListName(comfortViewFactorAngles)); + EXPECT_TRUE(people.setSpace(space)); + + ForwardTranslator forwardTranslator; + Workspace workspace = forwardTranslator.translateModel(model); + EXPECT_TRUE(workspace.getObjectsByType(IddObjectType::ComfortViewFactorAngles).empty()); +} + +TEST_F(EnergyPlusFixture, ForwardTranslator_ComfortViewFactorAngles_RejectsSurfacesInDifferentThermalZones) { + 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 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, 0.5)); + EXPECT_TRUE(comfortViewFactorAngles.addAngleFactor(surfaceToMove, 0.5)); + EXPECT_TRUE(surfaceToMove.setSpace(otherSpace)); + + PeopleDefinition definition(model); + People people(definition); + EXPECT_TRUE(people.setSurfaceNameAngleFactorListName(comfortViewFactorAngles)); + EXPECT_TRUE(people.setSpace(space)); + + ForwardTranslator forwardTranslator; + Workspace workspace = forwardTranslator.translateModel(model); + + const auto angleFactorObjects = workspace.getObjectsByType(IddObjectType::ComfortViewFactorAngles); + EXPECT_TRUE(angleFactorObjects.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().cast(); + EXPECT_TRUE(group.setPointer(0, surface.handle())); + EXPECT_TRUE(group.setDouble(1, 1.0)); + + ReverseTranslator reverseTranslator; + Model model = reverseTranslator.translateWorkspace(workspace); + + 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.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 diff --git a/src/energyplus/Test/People_GTest.cpp b/src/energyplus/Test/People_GTest.cpp index 3d68348e51..0eb08df455 100644 --- a/src/energyplus/Test/People_GTest.cpp +++ b/src/energyplus/Test/People_GTest.cpp @@ -16,9 +16,15 @@ #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 "../../utilities/geometry/Point3d.hpp" +#include "../../utilities/idf/IdfExtensibleGroup.hpp" #include +#include #include #include @@ -118,7 +124,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 +141,126 @@ 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.addAngleFactor(surface, 1.0)); + + PeopleDefinition definition(model); + People people(definition); + EXPECT_TRUE(people.setSurfaceNameAngleFactorListName(comfortViewFactorAngles)); + 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); + People people(definition); + EXPECT_TRUE(people.setSurfaceNameAngleFactorListName(surface)); + 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, ForwardTranslator_People_MRTTypeFollowsSurfaceTarget) { + 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)); + + PeopleDefinition definition(model); + People people(definition); + EXPECT_TRUE(people.setSurfaceNameAngleFactorListName(surface)); + EXPECT_TRUE(definition.setMeanRadiantTemperatureCalculationType("AngleFactor")); + EXPECT_TRUE(people.setSpace(space)); + + ForwardTranslator forwardTranslator; + Workspace workspace = forwardTranslator.translateModel(model); + + const auto peopleObjects = workspace.getObjectsByType(IddObjectType::People); + ASSERT_EQ(1u, peopleObjects.size()); + EXPECT_EQ("SurfaceWeighted", peopleObjects.front().getString(PeopleFields::MeanRadiantTemperatureCalculationType).get()); + EXPECT_EQ(surface.nameString(), peopleObjects.front().getString(PeopleFields::SurfaceName_AngleFactorListName).get()); +} + +TEST_F(EnergyPlusFixture, ForwardTranslator_People_MRTTypeWithoutTargetDefaultsToEnclosureAveraged) { + Model model; + PeopleDefinition definition(model); + People people(definition); + EXPECT_TRUE(definition.setMeanRadiantTemperatureCalculationType("SurfaceWeighted")); + + ForwardTranslator forwardTranslator; + Workspace workspace = forwardTranslator.translateModel(model); + + const auto peopleObjects = workspace.getObjectsByType(IddObjectType::People); + ASSERT_EQ(1u, peopleObjects.size()); + EXPECT_EQ("EnclosureAveraged", peopleObjects.front().getString(PeopleFields::MeanRadiantTemperatureCalculationType).get()); + EXPECT_TRUE(peopleObjects.front().isEmpty(PeopleFields::SurfaceName_AngleFactorListName)); +} + +TEST_F(EnergyPlusFixture, ForwardTranslator_People_UntranslatedMRTTargetDefaultsToEnclosureAveraged) { + Model model; + ComfortViewFactorAngles comfortViewFactorAngles(model); + PeopleDefinition definition(model); + People people(definition); + EXPECT_TRUE(people.setSurfaceNameAngleFactorListName(comfortViewFactorAngles)); + + ForwardTranslator forwardTranslator; + Workspace workspace = forwardTranslator.translateModel(model); + + const auto peopleObjects = workspace.getObjectsByType(IddObjectType::People); + ASSERT_EQ(1u, peopleObjects.size()); + EXPECT_EQ("EnclosureAveraged", peopleObjects.front().getString(PeopleFields::MeanRadiantTemperatureCalculationType).get()); + EXPECT_TRUE(peopleObjects.front().isEmpty(PeopleFields::SurfaceName_AngleFactorListName)); +} + TEST_F(EnergyPlusFixture, ReverseTranslator_People) { ReverseTranslator rt; @@ -148,6 +274,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 +299,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 +348,10 @@ TEST_F(EnergyPlusFixture, ReverseTranslator_People) { auto pd = p.definition().cast(); EXPECT_EQ("Area/Person", pd.numberofPeopleCalculationMethod()); + EXPECT_EQ("AngleFactor", pd.meanRadiantTemperatureCalculationType()); + auto mrtTarget = p.surfaceNameAngleFactorListName(); + ASSERT_TRUE(mrtTarget); + EXPECT_EQ("Angle Factors", mrtTarget->nameString()); EXPECT_FALSE(pd.numberofPeople()); EXPECT_FALSE(pd.peopleperSpaceFloorArea()); ASSERT_TRUE(pd.spaceFloorAreaperPerson()); @@ -227,7 +361,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()); 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..36e17d9920 --- /dev/null +++ b/src/model/ComfortViewFactorAngles.cpp @@ -0,0 +1,298 @@ +/*********************************************************************************************************************** +* OpenStudio(R), Copyright (c) Alliance for Energy Innovation, LLC. +* See also https://openstudio.net/license +***********************************************************************************************************************/ + +#include "ComfortViewFactorAngles.hpp" +#include "ComfortViewFactorAngles_Impl.hpp" + +#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" +#include "Model_Impl.hpp" + +#include "ModelExtensibleGroup.hpp" + +#include +#include + +#include "../utilities/core/Assert.hpp" + +namespace openstudio { +namespace model { + + 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; + } + + } // namespace + + AngleFactor::AngleFactor(const ModelObject& surface, double angleFactor) : m_surface(surface), m_angleFactor(angleFactor) { + const IddObjectType surfaceIddType = surface.iddObjectType(); + if ((surfaceIddType != IddObjectType::OS_Surface) && (surfaceIddType != IddObjectType::OS_SubSurface) + && (surfaceIddType != IddObjectType::OS_InternalMass)) { + LOG_AND_THROW("surface can be only of type Surface, SubSurface or InternalMass, not " << surfaceIddType.value()); + } + if (!((m_angleFactor >= 0.0) && (m_angleFactor <= 1.0))) { + LOG_AND_THROW("Angle Factor must be between 0 and 1."); + } + } + + ModelObject AngleFactor::surface() const { + return m_surface; + } + + 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) + : 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) {} + + 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::angleFactors() const { + std::vector result; + for (unsigned i = 0; i < numberofAngleFactors(); ++i) { + if (auto angleFactor = getAngleFactor(i)) { + result.push_back(*angleFactor); + } + } + return result; + } + + unsigned int ComfortViewFactorAngles_Impl::numberofAngleFactors() const { + return numExtensibleGroups(); + } + + boost::optional ComfortViewFactorAngles_Impl::angleFactorIndex(const ModelObject& surface) const { + for (unsigned i = 0; i < numberofAngleFactors(); ++i) { + auto group = getExtensibleGroup(i).cast(); + auto existingSurface = group.getModelObjectTarget(OS_ComfortViewFactorAnglesExtensibleFields::SurfaceName); + if (existingSurface && (existingSurface->handle() == surface.handle())) { + return i; + } + } + return boost::none; + } + + boost::optional ComfortViewFactorAngles_Impl::getAngleFactor(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); + if (!surface) { + LOG(Error, "Could not retrieve Surface Name for extensible group " << group.groupIndex() << "."); + } + if (!angleFactor) { + LOG(Error, "Could not retrieve Angle Factor for extensible group " << group.groupIndex() << "."); + } + if (!surface || !angleFactor) { + return boost::none; + } + return AngleFactor(*surface, *angleFactor); + } + + bool ComfortViewFactorAngles_Impl::addAngleFactor(const AngleFactor& angleFactor) { + bool result = false; + + ModelObject surface = angleFactor.surface(); + if (surface.model() != model()) { + LOG(Error, "Cannot add a Surface from another Model to " << briefDescription() << "."); + 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; + } + + 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 = 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 '" + << 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 << "."); + } + + double sum = angleFactor.angleFactor(); + for (unsigned i = 0; i < numberofAngleFactors(); ++i) { + if (existingIndex && (i == *existingIndex)) { + continue; + } + auto group = getExtensibleGroup(i).cast(); + auto existingValue = group.getDouble(OS_ComfortViewFactorAnglesExtensibleFields::AngleFactor); + sum += existingValue.get(); + } + + 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 result; + } + + std::vector temp; + ModelExtensibleGroup group = (existingIndex ? getExtensibleGroup(existingIndex.get()).cast() + : pushExtensibleGroup(temp, false).cast()); + + OS_ASSERT(group.setPointer(OS_ComfortViewFactorAnglesExtensibleFields::SurfaceName, surface.handle(), false)); + + bool angleFactorSet = group.setDouble(OS_ComfortViewFactorAnglesExtensibleFields::AngleFactor, angleFactor.angleFactor()); + if (angleFactorSet) { + result = true; + } else { + if (!existingIndex) { + getObject().eraseExtensibleGroup(group.groupIndex()); + } + } + + return result; + } + + bool ComfortViewFactorAngles_Impl::addAngleFactor(const ModelObject& surface, double angleFactor) { + return addAngleFactor(AngleFactor(surface, angleFactor)); + } + + bool ComfortViewFactorAngles_Impl::addAngleFactors(const std::vector& angleFactors) { + bool result = true; + 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) { + bool result = false; + + if (groupIndex < numberofAngleFactors()) { + getObject().eraseExtensibleGroup(groupIndex); + result = true; + } + + return result; + } + + void ComfortViewFactorAngles_Impl::removeAllAngleFactors() { + 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::angleFactors() const { + return getImpl()->angleFactors(); + } + + unsigned int ComfortViewFactorAngles::numberofAngleFactors() const { + return getImpl()->numberofAngleFactors(); + } + + boost::optional ComfortViewFactorAngles::angleFactorIndex(const ModelObject& surface) const { + return getImpl()->angleFactorIndex(surface); + } + + boost::optional ComfortViewFactorAngles::getAngleFactor(unsigned groupIndex) const { + return getImpl()->getAngleFactor(groupIndex); + } + + bool ComfortViewFactorAngles::addAngleFactor(const AngleFactor& angleFactor) { + return getImpl()->addAngleFactor(angleFactor); + } + + bool ComfortViewFactorAngles::addAngleFactor(const ModelObject& surface, double angleFactor) { + return getImpl()->addAngleFactor(surface, angleFactor); + } + + bool ComfortViewFactorAngles::addAngleFactors(const std::vector& angleFactors) { + return getImpl()->addAngleFactors(angleFactors); + } + + void ComfortViewFactorAngles::removeAngleFactor(int groupIndex) { + getImpl()->removeAngleFactor(groupIndex); + } + + void ComfortViewFactorAngles::removeAllAngleFactors() { + getImpl()->removeAllAngleFactors(); + } + + ComfortViewFactorAngles::ComfortViewFactorAngles(std::shared_ptr impl) : ModelObject(std::move(impl)) {} + +} // namespace model +} // namespace openstudio diff --git a/src/model/ComfortViewFactorAngles.hpp b/src/model/ComfortViewFactorAngles.hpp new file mode 100644 index 0000000000..1d2b83018c --- /dev/null +++ b/src/model/ComfortViewFactorAngles.hpp @@ -0,0 +1,79 @@ +/*********************************************************************************************************************** +* 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" + +namespace openstudio { +namespace model { + + namespace detail { + class ComfortViewFactorAngles_Impl; + } + + class MODEL_API AngleFactor + { + public: + AngleFactor(const ModelObject& surface, double angleFactor); + + ModelObject surface() const; + double angleFactor() const; + + bool operator==(const AngleFactor& other) const = default; + bool operator!=(const AngleFactor& other) const; + + private: + ModelObject 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: + explicit ComfortViewFactorAngles(const Model& model); + + virtual ~ComfortViewFactorAngles() override = default; + + static IddObjectType iddObjectType(); + + std::vector angleFactors() const; + unsigned int numberofAngleFactors() const; + boost::optional angleFactorIndex(const ModelObject& surface) const; + boost::optional getAngleFactor(unsigned groupIndex) const; + + bool addAngleFactor(const AngleFactor& angleFactor); + bool addAngleFactor(const ModelObject& surface, double angleFactor); + bool addAngleFactors(const std::vector& angleFactors); + void removeAngleFactor(int groupIndex); + void removeAllAngleFactors(); + + 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 diff --git a/src/model/ComfortViewFactorAngles_Impl.hpp b/src/model/ComfortViewFactorAngles_Impl.hpp new file mode 100644 index 0000000000..d17d9ff93f --- /dev/null +++ b/src/model/ComfortViewFactorAngles_Impl.hpp @@ -0,0 +1,51 @@ +/*********************************************************************************************************************** +* 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 AngleFactor; + class ModelObject; + + 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 angleFactors() const; + unsigned int numberofAngleFactors() const; + boost::optional angleFactorIndex(const ModelObject& surface) const; + boost::optional getAngleFactor(unsigned groupIndex) const; + + bool addAngleFactor(const AngleFactor& angleFactor); + bool addAngleFactor(const ModelObject& surface, double angleFactor); + bool addAngleFactors(const std::vector& angleFactors); + bool removeAngleFactor(unsigned groupIndex); + void removeAllAngleFactors(); + + private: + REGISTER_LOGGER("openstudio.model.ComfortViewFactorAngles"); + + }; + } + +} // namespace model +} // namespace openstudio + +#endif // MODEL_COMFORTVIEWFACTORANGLES_IMPL_HPP diff --git a/src/model/ConcreteModelObjects.hpp b/src/model/ConcreteModelObjects.hpp index 2835b2addc..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" @@ -710,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/InternalMass.cpp b/src/model/InternalMass.cpp index 9cffbf726b..b956bea110 100644 --- a/src/model/InternalMass.cpp +++ b/src/model/InternalMass.cpp @@ -5,7 +5,8 @@ #include "InternalMass.hpp" #include "InternalMass_Impl.hpp" - +#include "ComfortViewFactorAngles.hpp" +#include "ComfortViewFactorAngles_Impl.hpp" #include "InternalMassDefinition.hpp" #include "InternalMassDefinition_Impl.hpp" #include "Space.hpp" @@ -49,6 +50,17 @@ namespace model { return InternalMass::iddObjectType(); } + std::vector InternalMass_Impl::remove() { + InternalMass internalMass = getObject().cast(); + for (ComfortViewFactorAngles& comfortViewFactorAngles : internalMass.getModelObjectSources()) { + while (boost::optional index = comfortViewFactorAngles.angleFactorIndex(internalMass)) { + comfortViewFactorAngles.removeAngleFactor(index.get()); + } + } + + return SpaceLoadInstance_Impl::remove(); + } + bool InternalMass_Impl::hardSize() { boost::optional space = this->space(); if (!space) { diff --git a/src/model/InternalMass_Impl.hpp b/src/model/InternalMass_Impl.hpp index d2086245cc..6706b99597 100644 --- a/src/model/InternalMass_Impl.hpp +++ b/src/model/InternalMass_Impl.hpp @@ -52,6 +52,8 @@ namespace model { virtual bool isAbsolute() const override; + virtual std::vector remove() override; + //@} /** @name Getters */ //@{ diff --git a/src/model/Model.cpp b/src/model/Model.cpp index 9e4ff763d4..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); @@ -4633,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 0068937e08..7806168e05 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,6 +263,8 @@ MODELOBJECT_TEMPLATES(FoundationKiva); MODELOBJECT_TEMPLATES(SurfacePropertyExposedFoundationPerimeter); MODELOBJECT_TEMPLATES(ViewFactor); // Helper class defined in ZonePropertyUserViewFactorsBySurfaceName MODELOBJECT_TEMPLATES(ZonePropertyUserViewFactorsBySurfaceName); +MODELOBJECT_TEMPLATES(AngleFactor); // Helper class defined in ComfortViewFactorAngles +MODELOBJECT_TEMPLATES(ComfortViewFactorAngles); MODELOBJECT_TEMPLATES(MRTWeightingFactor); // Helper class defined in ZoneMRTCalculation MODELOBJECT_TEMPLATES(ZoneMRTCalculation); MODELOBJECT_TEMPLATES(ExteriorLoadInstance); @@ -313,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/People.cpp b/src/model/People.cpp index a41db3efd1..729266c4e2 100644 --- a/src/model/People.cpp +++ b/src/model/People.cpp @@ -9,6 +9,10 @@ #include "Model.hpp" #include "PeopleDefinition.hpp" #include "PeopleDefinition_Impl.hpp" +#include "ComfortViewFactorAngles.hpp" +#include "ComfortViewFactorAngles_Impl.hpp" +#include "Surface.hpp" +#include "Surface_Impl.hpp" #include "Schedule.hpp" #include "Schedule_Impl.hpp" #include "ScheduleTypeLimits.hpp" @@ -392,6 +396,37 @@ namespace model { OS_ASSERT(result); } + boost::optional People_Impl::surfaceNameAngleFactorListName() const { + return getObject().getModelObjectTarget(OS_PeopleFields::SurfaceName_AngleFactorListName); + } + + bool People_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 (!peopleDefinition().setMeanRadiantTemperatureCalculationType(mrtType)) { + return false; + } + return setPointer(OS_PeopleFields::SurfaceName_AngleFactorListName, modelObject.handle()); + } + + void People_Impl::resetSurfaceNameAngleFactorListName() { + bool result = setString(OS_PeopleFields::SurfaceName_AngleFactorListName, ""); + OS_ASSERT(result); + peopleDefinition().resetMeanRadiantTemperatureCalculationType(); + } + boost::optional People_Impl::numberOfPeople() const { OptionalDouble temp = peopleDefinition().numberofPeople(); if (temp) { @@ -777,6 +812,18 @@ namespace model { getImpl()->resetAirVelocitySchedule(); } + boost::optional People::surfaceNameAngleFactorListName() const { + return getImpl()->surfaceNameAngleFactorListName(); + } + + bool People::setSurfaceNameAngleFactorListName(const ModelObject& modelObject) { + return getImpl()->setSurfaceNameAngleFactorListName(modelObject); + } + + void People::resetSurfaceNameAngleFactorListName() { + getImpl()->resetSurfaceNameAngleFactorListName(); + } + boost::optional People::numberOfPeople() const { return getImpl()->numberOfPeople(); } diff --git a/src/model/People.hpp b/src/model/People.hpp index 150389f299..caf47adcd7 100644 --- a/src/model/People.hpp +++ b/src/model/People.hpp @@ -83,6 +83,8 @@ namespace model { /** Returns the ankle-level air velocity schedule. */ boost::optional ankleLevelAirVelocitySchedule() const; + boost::optional surfaceNameAngleFactorListName() const; + double coldStressTemperatureThreshold() const; bool isColdStressTemperatureThresholdDefaulted() const; @@ -152,6 +154,9 @@ namespace model { bool setAnkleLevelAirVelocitySchedule(Schedule& schedule); void resetAnkleLevelAirVelocitySchedule(); + bool setSurfaceNameAngleFactorListName(const ModelObject& modelObject); + void resetSurfaceNameAngleFactorListName(); + bool setColdStressTemperatureThreshold(double coldStressTemperatureThreshold); void resetColdStressTemperatureThreshold(); diff --git a/src/model/People_Impl.hpp b/src/model/People_Impl.hpp index 1c2bbec154..79e05f2680 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; @@ -97,6 +95,8 @@ namespace model { boost::optional ankleLevelAirVelocitySchedule() const; + boost::optional surfaceNameAngleFactorListName() const; + double coldStressTemperatureThreshold() const; bool isColdStressTemperatureThresholdDefaulted() const; @@ -161,6 +161,9 @@ namespace model { bool setAnkleLevelAirVelocitySchedule(Schedule& schedule); void resetAnkleLevelAirVelocitySchedule(); + bool setSurfaceNameAngleFactorListName(const ModelObject& modelObject); + void resetSurfaceNameAngleFactorListName(); + bool setColdStressTemperatureThreshold(double coldStressTemperatureThreshold); void resetColdStressTemperatureThreshold(); diff --git a/src/model/SubSurface.cpp b/src/model/SubSurface.cpp index 46c934d239..ad9c2cc4cc 100644 --- a/src/model/SubSurface.cpp +++ b/src/model/SubSurface.cpp @@ -5,7 +5,8 @@ #include "SubSurface.hpp" #include "SubSurface_Impl.hpp" - +#include "ComfortViewFactorAngles.hpp" +#include "ComfortViewFactorAngles_Impl.hpp" #include "Model.hpp" #include "Model_Impl.hpp" #include "Surface.hpp" @@ -128,6 +129,13 @@ namespace model { this->resetAdjacentSubSurface(); } + SubSurface subSurface = getObject(); + for (ComfortViewFactorAngles& comfortViewFactorAngles : subSurface.getModelObjectSources()) { + while (boost::optional index = comfortViewFactorAngles.angleFactorIndex(subSurface)) { + comfortViewFactorAngles.removeAngleFactor(index.get()); + } + } + // Remove it from the extensible groups in ShadingControl(s) for (auto& sc : shadingControls()) { sc.removeSubSurface(this->getObject()); diff --git a/src/model/Surface.cpp b/src/model/Surface.cpp index e9fb00d13a..89b059bd71 100644 --- a/src/model/Surface.cpp +++ b/src/model/Surface.cpp @@ -5,7 +5,8 @@ #include "Surface.hpp" #include "Surface_Impl.hpp" - +#include "ComfortViewFactorAngles.hpp" +#include "ComfortViewFactorAngles_Impl.hpp" #include "Model.hpp" #include "Model_Impl.hpp" #include "Space.hpp" @@ -125,6 +126,13 @@ namespace model { this->resetAdjacentSurface(); } + Surface surface = getObject(); + for (ComfortViewFactorAngles& comfortViewFactorAngles : surface.getModelObjectSources()) { + while (boost::optional index = comfortViewFactorAngles.angleFactorIndex(surface)) { + comfortViewFactorAngles.removeAngleFactor(index.get()); + } + } + return ParentObject_Impl::remove(); } diff --git a/src/model/ZonePropertyUserViewFactorsBySurfaceName.cpp b/src/model/ZonePropertyUserViewFactorsBySurfaceName.cpp index 9de9941e8c..bd42a19e6a 100644 --- a/src/model/ZonePropertyUserViewFactorsBySurfaceName.cpp +++ b/src/model/ZonePropertyUserViewFactorsBySurfaceName.cpp @@ -55,13 +55,13 @@ namespace model { } // Check the IDD types to ensure they are ok - IddObjectType fromIddType = fromSurface.iddObjectType(); + const IddObjectType fromIddType = fromSurface.iddObjectType(); if ((fromIddType != IddObjectType::OS_Surface) && (fromIddType != IddObjectType::OS_SubSurface) && (fromIddType != IddObjectType::OS_InternalMass)) { LOG_AND_THROW("fromSurface can be only of type Surface, SubSurface or InternalMass, not " << fromIddType.value()); } - IddObjectType toIddType = toSurface.iddObjectType(); + const IddObjectType toIddType = toSurface.iddObjectType(); if ((toIddType != IddObjectType::OS_Surface) && (toIddType != IddObjectType::OS_SubSurface) && (toIddType != IddObjectType::OS_InternalMass)) { LOG_AND_THROW("toSurface can be only of type Surface, SubSurface or InternalMass, not " << toIddType.value()); } diff --git a/src/model/test/ComfortViewFactorAngles_GTest.cpp b/src/model/test/ComfortViewFactorAngles_GTest.cpp new file mode 100644 index 0000000000..0ac3404297 --- /dev/null +++ b/src/model/test/ComfortViewFactorAngles_GTest.cpp @@ -0,0 +1,179 @@ +/*********************************************************************************************************************** +* OpenStudio(R), Copyright (c) Alliance for Energy Innovation, LLC. +* See also https://openstudio.net/license +***********************************************************************************************************************/ + +#include + +#include +#include + +#include "ModelFixture.hpp" + +#include "../ComfortViewFactorAngles.hpp" +#include "../InternalMass.hpp" +#include "../InternalMassDefinition.hpp" +#include "../Model.hpp" +#include "../People.hpp" +#include "../PeopleDefinition.hpp" +#include "../Space.hpp" +#include "../Surface.hpp" +#include "../SubSurface.hpp" +#include "../ThermalZone.hpp" + +#include "../../utilities/geometry/Point3d.hpp" + +using namespace openstudio; +using namespace openstudio::model; + +TEST_F(ModelFixture, ComfortViewFactorAngles) { + // Build a valid surface target in a thermal zone for the normal insertion path. + 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); + + // AngleFactor validates both the target type and the inclusive numeric range at construction. + EXPECT_THROW(AngleFactor(space, 0.5), openstudio::Exception); + Surface unassignedSurface(points, model); + EXPECT_FALSE(comfortViewFactorAngles.addAngleFactor(unassignedSurface, 0.5)); + + // 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)); + 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()); + + // 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()); + + // 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); + EXPECT_FALSE(comfortViewFactorAngles.angleFactorIndex(otherSurface)); + EXPECT_FALSE(comfortViewFactorAngles.addAngleFactor(otherSurface, 0.75)); + EXPECT_EQ(2u, comfortViewFactorAngles.numberofAngleFactors()); + + PeopleDefinition peopleDefinition(model); + People people(peopleDefinition); + EXPECT_FALSE(people.setSurfaceNameAngleFactorListName(otherSurface)); + EXPECT_EQ("EnclosureAveraged", peopleDefinition.meanRadiantTemperatureCalculationType()); + EXPECT_FALSE(people.surfaceNameAngleFactorListName()); + + // 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. + 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()); + + // Bulk insertion reports failure but keeps groups that were added before an invalid item. + Surface unassignedBulkSurface(points, model); + EXPECT_FALSE(comfortViewFactorAngles.addAngleFactors({AngleFactor(surface, 0.5), AngleFactor(unassignedBulkSurface, 0.5)})); + EXPECT_EQ(1u, comfortViewFactorAngles.numberofAngleFactors()); +} + +TEST_F(ModelFixture, ComfortViewFactorAngles_HeatTransferSurfaceTargets) { + // All EnergyPlus heat-transfer surface types are valid angle-factor targets. + 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()); +} + +TEST_F(ModelFixture, ComfortViewFactorAngles_RemovesDeletedTargets) { + // Removing any referenced heat-transfer object removes the corresponding group. + 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)); + Surface subSurfaceParent(points, model); + ASSERT_TRUE(subSurfaceParent.setSpace(space)); + SubSurface subSurface(points, model); + ASSERT_TRUE(subSurface.setSurface(subSurfaceParent)); + InternalMassDefinition internalMassDefinition(model); + InternalMass internalMass(internalMassDefinition); + ASSERT_TRUE(internalMass.setSpace(space)); + + ComfortViewFactorAngles comfortViewFactorAngles(model); + ASSERT_TRUE(comfortViewFactorAngles.addAngleFactor(surface, 0.25)); + ASSERT_TRUE(comfortViewFactorAngles.addAngleFactor(subSurface, 0.25)); + ASSERT_TRUE(comfortViewFactorAngles.addAngleFactor(internalMass, 0.25)); + ASSERT_TRUE(comfortViewFactorAngles.addAngleFactor(subSurfaceParent, 0.25)); + ASSERT_EQ(4u, comfortViewFactorAngles.numberofAngleFactors()); + + surface.remove(); + EXPECT_EQ(3u, comfortViewFactorAngles.numberofAngleFactors()); + subSurface.remove(); + EXPECT_EQ(2u, comfortViewFactorAngles.numberofAngleFactors()); + internalMass.remove(); + EXPECT_EQ(1u, comfortViewFactorAngles.numberofAngleFactors()); +} diff --git a/src/model/test/People_GTest.cpp b/src/model/test/People_GTest.cpp index 9aded7fdb9..0352dd54cb 100644 --- a/src/model/test/People_GTest.cpp +++ b/src/model/test/People_GTest.cpp @@ -12,12 +12,17 @@ #include "../People_Impl.hpp" #include "../PeopleDefinition.hpp" #include "../PeopleDefinition_Impl.hpp" +#include "../ComfortViewFactorAngles.hpp" +#include "../Surface.hpp" +#include "../ThermalZone.hpp" #include "../ScheduleRuleset.hpp" #include "../ScheduleDay.hpp" #include "../ScheduleTypeRegistry.hpp" #include "../ScheduleConstant.hpp" +#include "../../utilities/geometry/Point3d.hpp" + #include using namespace openstudio; @@ -48,12 +53,37 @@ 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); + 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)); + 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(person.setSurfaceNameAngleFactorListName(comfortViewFactorAngles)); + EXPECT_EQ("AngleFactor", definition.meanRadiantTemperatureCalculationType()); + EXPECT_TRUE(person.surfaceNameAngleFactorListName()); + + EXPECT_TRUE(person.setSurfaceNameAngleFactorListName(surface)); EXPECT_EQ("SurfaceWeighted", definition.meanRadiantTemperatureCalculationType()); + EXPECT_TRUE(person.surfaceNameAngleFactorListName()); + + person.resetSurfaceNameAngleFactorListName(); + EXPECT_EQ("EnclosureAveraged", definition.meanRadiantTemperatureCalculationType()); + EXPECT_FALSE(person.surfaceNameAngleFactorListName()); // Backward compat EXPECT_TRUE(definition.setMeanRadiantTemperatureCalculationType("ZoneAveraged")); EXPECT_EQ("EnclosureAveraged", definition.meanRadiantTemperatureCalculationType()); + EXPECT_FALSE(person.surfaceNameAngleFactorListName()); EXPECT_FALSE(definition.isMeanRadiantTemperatureCalculationTypeDefaulted()); EXPECT_TRUE(definition.setMeanRadiantTemperatureCalculationType("SurfaceWeighted"));