diff --git a/CMakeLists.txt b/CMakeLists.txt index 82c801ca2f9..ca7bcbfcb24 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -199,13 +199,13 @@ set(ENERGYPLUS_VERSION_MINOR 1) set(ENERGYPLUS_VERSION_PATCH 0) set(ENERGYPLUS_VERSION "${ENERGYPLUS_VERSION_MAJOR}.${ENERGYPLUS_VERSION_MINOR}.${ENERGYPLUS_VERSION_PATCH}") # Build SHA is not required to have a value, but if it does OpenStudio will require this build. -set(ENERGYPLUS_BUILD_SHA "68a4a7c774") +set(ENERGYPLUS_BUILD_SHA "1c11a3d85f") # ENERGYPLUS_RELEASE_NAME is used to locate the E+ download # from the github releases -set(ENERGYPLUS_RELEASE_NAME "v25.1.0") +set(ENERGYPLUS_RELEASE_NAME "v25.1.0-WithDSOASpaceListFixes") -set(ENERGYPLUS_REPO "NREL") +set(ENERGYPLUS_REPO "jmarrec") # Radiance set(RADIANCE_VERSION "5.0.a.12") @@ -645,27 +645,27 @@ endif() if(UNIX) if(APPLE) if (ARCH MATCHES "arm64") - set(ENERGYPLUS_EXPECTED_HASH e5a071a01c130aff04515641b502b1ed) + set(ENERGYPLUS_EXPECTED_HASH 7301caad82ed104f23704e6123345989) set(ENERGYPLUS_PLATFORM "Darwin-macOS13-arm64") else() - set(ENERGYPLUS_EXPECTED_HASH 21c6e2c536737ef837c2f58c241133cc) + set(ENERGYPLUS_EXPECTED_HASH 8746aaad0e901f640b644ffc96d4cde9) set(ENERGYPLUS_PLATFORM "Darwin-macOS12.1-x86_64") endif() elseif(LSB_RELEASE_ID_SHORT MATCHES "CentOS") - set(ENERGYPLUS_EXPECTED_HASH 53aef5bb832ad7f1564ea568a60b4d12) + set(ENERGYPLUS_EXPECTED_HASH TODO_TBD_TODO) set(ENERGYPLUS_PLATFORM "Linux-CentOS7.9.2009-x86_64") else() if(LSB_RELEASE_VERSION_SHORT MATCHES "24.04") if (ARCH MATCHES "arm64") - set(ENERGYPLUS_EXPECTED_HASH 4862acc3510e3bc4c9a176160f4a4c16) + set(ENERGYPLUS_EXPECTED_HASH 143d5972ba83ee676a8a789104242e19) else() - set(ENERGYPLUS_EXPECTED_HASH 27939b216b986527c62270055bd96c7d) + set(ENERGYPLUS_EXPECTED_HASH e0733483ab27dd6887ac596c047ff4ac) endif() elseif(LSB_RELEASE_VERSION_SHORT MATCHES "22.04") if (ARCH MATCHES "arm64") - set(ENERGYPLUS_EXPECTED_HASH ffef7fb46ab03eb514c105052d7a9537) + set(ENERGYPLUS_EXPECTED_HASH 66e36cf50076c87feeb12fee6bede7f5) else() - set(ENERGYPLUS_EXPECTED_HASH a96af44c88792877cdf32e0cb90bcc42) + set(ENERGYPLUS_EXPECTED_HASH 29365cf56e4d65bac3c64a4568acecc8) endif() else() # e.g., 18.04, 20.04 message(FATAL_ERROR "EnergyPlus no longer provides packages for Ubuntu < 22.04") @@ -702,11 +702,11 @@ elseif(WIN32) if(CMAKE_SIZEOF_VOID_P EQUAL 8) # 64 bit set(ENERGYPLUS_PATH "EnergyPlus-${ENERGYPLUS_VERSION}-${ENERGYPLUS_BUILD_SHA}-Windows-x86_64") set(ENERGYPLUS_ARCH 64) - set(ENERGYPLUS_EXPECTED_HASH fbf1ec77822c33dd3c991fa1fb183ee8) + set(ENERGYPLUS_EXPECTED_HASH d4870d7e79c233c4a938df056f64ccce) else() set(ENERGYPLUS_PATH "EnergyPlus-${ENERGYPLUS_VERSION}-${ENERGYPLUS_BUILD_SHA}-Windows-i386") set(ENERGYPLUS_ARCH 32) - set(ENERGYPLUS_EXPECTED_HASH e1057c847387a1e76645fc02e2625d6d) + set(ENERGYPLUS_EXPECTED_HASH TODO_TBD_TODO) endif() if(EXISTS "${PROJECT_BINARY_DIR}/${ENERGYPLUS_PATH}.zip") file(MD5 "${PROJECT_BINARY_DIR}/${ENERGYPLUS_PATH}.zip" ENERGYPLUS_HASH) diff --git a/src/energyplus/ForwardTranslator.cpp b/src/energyplus/ForwardTranslator.cpp index 4f4db90c98f..4ef7c863642 100644 --- a/src/energyplus/ForwardTranslator.cpp +++ b/src/energyplus/ForwardTranslator.cpp @@ -1502,9 +1502,8 @@ namespace energyplus { return retVal; } case openstudio::IddObjectType::OS_DesignSpecification_OutdoorAir: { - auto designSpecificationOutdoorAir = modelObject.cast(); - retVal = translateDesignSpecificationOutdoorAir(designSpecificationOutdoorAir); - break; + LOG_AND_THROW("Shouldn't get there"); + return retVal; } case openstudio::IddObjectType::OS_DesignSpecification_ZoneAirDistribution: { // DLM: appears to be translated in SizingZone @@ -4034,6 +4033,8 @@ namespace energyplus { m_map.clear(); + m_zoneDSOAsMap.clear(); + m_anyNumberScheduleTypeLimits.reset(); m_interiorPartitionSurfaceConstruction.reset(); diff --git a/src/energyplus/ForwardTranslator.hpp b/src/energyplus/ForwardTranslator.hpp index 7b83e9b8d7e..2f6f6ec83a9 100644 --- a/src/energyplus/ForwardTranslator.hpp +++ b/src/energyplus/ForwardTranslator.hpp @@ -508,7 +508,11 @@ namespace energyplus { namespace detail { struct ForwardTranslatorInitializer; - }; + + // TODO: I have to put this back because of AirTerminalDualDuctVAV which should be using Control for Outdoor Air + // I'm setting it up as a free function in detail:: though, so you know you shouldn't call it! + boost::optional translateDesignSpecificationOutdoorAir(model::DesignSpecificationOutdoorAir& modelObject); + }; // namespace detail #define ENERGYPLUS_VERSION "25.1" @@ -938,7 +942,14 @@ namespace energyplus { boost::optional translateDesignDay(model::DesignDay& modelObject); - boost::optional translateDesignSpecificationOutdoorAir(model::DesignSpecificationOutdoorAir& modelObject); + // Construct (or fetch if already created) a DesignSpecificationOutdoorAir (DSOA) or DSOA:SpaceList + // * if there are no spaces with a DSOA assigned: return empty + // * otherwise: + // * otherwise: + // * if we translated to E+ with spaces: + // * If a single one: DSOA, otherwise create a DSOA:SpaceList + // * if we do not use E+ spaces: create DSOA + boost::optional getOrCreateThermalZoneDSOA(const model::ThermalZone& z); boost::optional translateDistrictCooling(model::DistrictCooling& modelObject); @@ -1507,6 +1518,9 @@ namespace energyplus { boost::optional translateThermalZone(model::ThermalZone& modelObject); void translateThermalZoneSpacesWhenCombinedSpaces(model::ThermalZone& modelObject, IdfObject& idfObject); void translateThermalZoneSpacesToEnergyPlusSpaces(model::ThermalZone& modelObject, IdfObject& idfObject); + // Helper for the DesignSpecification:ZoneAirDistribution, returns empty when the DSZAD is not needed, + // which is when fields on Sizing:Zone related to it are all defaulted (Implemeted in ForwardTranslateSizingZone) + boost::optional zoneDSZADName(const model::ThermalZone& zone); boost::optional translateThermostatSetpointDualSetpoint(model::ThermostatSetpointDualSetpoint& tsds); @@ -1698,6 +1712,9 @@ namespace energyplus { ModelObjectMap m_map; + using ZoneToMaybeDSOA = std::map>; + ZoneToMaybeDSOA m_zoneDSOAsMap; + std::vector m_idfObjects; boost::optional m_anyNumberScheduleTypeLimits; diff --git a/src/energyplus/ForwardTranslator/ForwardTranslateAirTerminalDualDuctVAV.cpp b/src/energyplus/ForwardTranslator/ForwardTranslateAirTerminalDualDuctVAV.cpp index 0c043473102..199a987feec 100644 --- a/src/energyplus/ForwardTranslator/ForwardTranslateAirTerminalDualDuctVAV.cpp +++ b/src/energyplus/ForwardTranslator/ForwardTranslateAirTerminalDualDuctVAV.cpp @@ -14,9 +14,12 @@ #include "../../model/Node_Impl.hpp" #include "../../model/DesignSpecificationOutdoorAir.hpp" #include "../../model/DesignSpecificationOutdoorAir_Impl.hpp" + +#include "../../utilities/idd/IddEnums.hpp" +#include "../../utilities/plot/ProgressBar.hpp" + #include #include -#include "../../utilities/idd/IddEnums.hpp" #include #include @@ -71,8 +74,29 @@ namespace energyplus { idfObject.setDouble(AirTerminal_DualDuct_VAVFields::ZoneMinimumAirFlowFraction, value); } + // TODO: replace with "Control for Outdoor Air!" if (auto designOA = modelObject.designSpecificationOutdoorAirObject()) { - if (auto idf = translateAndMapModelObject(designOA.get())) { + + auto translateAndMapDSOA = [this](DesignSpecificationOutdoorAir& dsoa) -> boost::optional { + auto objInMapIt = m_map.find(dsoa.handle()); + if (objInMapIt != m_map.end()) { + return objInMapIt->second; + } + + auto idf_dsoa_ = detail::translateDesignSpecificationOutdoorAir(dsoa); + + if (idf_dsoa_) { + m_idfObjects.push_back(*idf_dsoa_); + m_map.emplace(dsoa.handle(), *idf_dsoa_); + } + + if (m_progressBar) { + m_progressBar->setValue((int)m_map.size()); + } + return idf_dsoa_; + }; + + if (auto idf = translateAndMapDSOA(designOA.get())) { idfObject.setString(AirTerminal_DualDuct_VAVFields::DesignSpecificationOutdoorAirObjectName, idf->name().get()); } } diff --git a/src/energyplus/ForwardTranslator/ForwardTranslateAirTerminalDualDuctVAVOutdoorAir.cpp b/src/energyplus/ForwardTranslator/ForwardTranslateAirTerminalDualDuctVAVOutdoorAir.cpp index c5704225e0e..cd065c3dfb0 100644 --- a/src/energyplus/ForwardTranslator/ForwardTranslateAirTerminalDualDuctVAVOutdoorAir.cpp +++ b/src/energyplus/ForwardTranslator/ForwardTranslateAirTerminalDualDuctVAVOutdoorAir.cpp @@ -76,29 +76,26 @@ namespace energyplus { // ControlForOutdoorAir: if yes, get the zone's space's DSOA { - bool dsoa_found = false; if (modelObject.controlForOutdoorAir()) { + bool dsoa_found = false; if (auto airLoopHVAC = modelObject.airLoopHVAC()) { auto zones = airLoopHVAC->demandComponents(modelObject, airLoopHVAC->demandOutletNode(), model::ThermalZone::iddObjectType()); if (!zones.empty()) { auto zone = zones.front(); - auto spaces = zone.cast().spaces(); - if (!spaces.empty()) { - if (auto designSpecificationOutdoorAir = spaces.front().designSpecificationOutdoorAir()) { - idfObject.setString(AirTerminal_DualDuct_VAV_OutdoorAirFields::DesignSpecificationOutdoorAirObjectName, - designSpecificationOutdoorAir->name().get()); - dsoa_found = true; - } + if (auto dsoaOrList_ = getOrCreateThermalZoneDSOA(zone.cast())) { + idfObject.setString(AirTerminal_DualDuct_VAV_OutdoorAirFields::DesignSpecificationOutdoorAirObjectName, dsoaOrList_->nameString()); + dsoa_found = true; } } } - } - if (!dsoa_found) { - LOG(Error, "Cannot set the Required 'Design Specification Outdoor Air' (DSOA) object for " - << modelObject.briefDescription() - << ". You should set controlForOutdoorAir to 'true' and ensure the zone's Space/SpaceType has a DSOA attached."); + if (!dsoa_found) { + LOG(Error, "Cannot set the Required 'Design Specification Outdoor Air' (DSOA) object for " + << modelObject.briefDescription() + << ". You should set controlForOutdoorAir to 'true' and ensure the zone's Space/SpaceType has a DSOA attached."); + } } } + // Populate fields for AirDistributionUnit if (boost::optional outletNode = modelObject.outletModelObject()) { _airDistributionUnit.setString(ZoneHVAC_AirDistributionUnitFields::AirDistributionUnitOutletNodeName, outletNode->name().get()); diff --git a/src/energyplus/ForwardTranslator/ForwardTranslateAirTerminalSingleDuctInletSideMixer.cpp b/src/energyplus/ForwardTranslator/ForwardTranslateAirTerminalSingleDuctInletSideMixer.cpp index 977f9c2135a..2a3aedd43cb 100644 --- a/src/energyplus/ForwardTranslator/ForwardTranslateAirTerminalSingleDuctInletSideMixer.cpp +++ b/src/energyplus/ForwardTranslator/ForwardTranslateAirTerminalSingleDuctInletSideMixer.cpp @@ -98,13 +98,9 @@ namespace energyplus { auto zones = airLoopHVAC->demandComponents(modelObject, airLoopHVAC->demandOutletNode(), model::ThermalZone::iddObjectType()); if (!zones.empty()) { auto zone = zones.front(); - auto spaces = zone.cast().spaces(); - if (!spaces.empty()) { - if (auto designSpecificationOutdoorAir = spaces.front().designSpecificationOutdoorAir()) { - dsoa_found = true; - idfObject.setString(AirTerminal_SingleDuct_MixerFields::DesignSpecificationOutdoorAirObjectName, - designSpecificationOutdoorAir->name().get()); - } + if (auto dsoaOrList_ = getOrCreateThermalZoneDSOA(zone.cast())) { + dsoa_found = true; + idfObject.setString(AirTerminal_SingleDuct_MixerFields::DesignSpecificationOutdoorAirObjectName, dsoaOrList_->nameString()); } } } diff --git a/src/energyplus/ForwardTranslator/ForwardTranslateAirTerminalSingleDuctVAVNoReheat.cpp b/src/energyplus/ForwardTranslator/ForwardTranslateAirTerminalSingleDuctVAVNoReheat.cpp index 9e53b8dc211..1bd7029d7fa 100644 --- a/src/energyplus/ForwardTranslator/ForwardTranslateAirTerminalSingleDuctVAVNoReheat.cpp +++ b/src/energyplus/ForwardTranslator/ForwardTranslateAirTerminalSingleDuctVAVNoReheat.cpp @@ -163,12 +163,8 @@ namespace energyplus { auto zones = airLoopHVAC->demandComponents(modelObject, airLoopHVAC->demandOutletNode(), model::ThermalZone::iddObjectType()); if (!zones.empty()) { auto zone = zones.front(); - auto spaces = zone.cast().spaces(); - if (!spaces.empty()) { - if (auto designSpecificationOutdoorAir = spaces.front().designSpecificationOutdoorAir()) { - idfObject.setString(AirTerminal_SingleDuct_VAV_NoReheatFields::DesignSpecificationOutdoorAirObjectName, - designSpecificationOutdoorAir->name().get()); - } + if (auto dsoaOrList_ = getOrCreateThermalZoneDSOA(zone.cast())) { + idfObject.setString(AirTerminal_SingleDuct_VAV_NoReheatFields::DesignSpecificationOutdoorAirObjectName, dsoaOrList_->nameString()); } } } diff --git a/src/energyplus/ForwardTranslator/ForwardTranslateAirTerminalSingleDuctVAVReheat.cpp b/src/energyplus/ForwardTranslator/ForwardTranslateAirTerminalSingleDuctVAVReheat.cpp index af325a45bee..83fcafd2b28 100644 --- a/src/energyplus/ForwardTranslator/ForwardTranslateAirTerminalSingleDuctVAVReheat.cpp +++ b/src/energyplus/ForwardTranslator/ForwardTranslateAirTerminalSingleDuctVAVReheat.cpp @@ -207,12 +207,8 @@ namespace energyplus { auto zones = airLoopHVAC->demandComponents(modelObject, airLoopHVAC->demandOutletNode(), model::ThermalZone::iddObjectType()); if (!zones.empty()) { auto zone = zones.front(); - auto spaces = zone.cast().spaces(); - if (!spaces.empty()) { - if (auto designSpecificationOutdoorAir = spaces.front().designSpecificationOutdoorAir()) { - idfObject.setString(AirTerminal_SingleDuct_VAV_ReheatFields::DesignSpecificationOutdoorAirObjectName, - designSpecificationOutdoorAir->name().get()); - } + if (auto dsoaOrList_ = getOrCreateThermalZoneDSOA(zone.cast())) { + idfObject.setString(AirTerminal_SingleDuct_VAV_ReheatFields::DesignSpecificationOutdoorAirObjectName, dsoaOrList_->nameString()); } } } diff --git a/src/energyplus/ForwardTranslator/ForwardTranslateControllerMechanicalVentilation.cpp b/src/energyplus/ForwardTranslator/ForwardTranslateControllerMechanicalVentilation.cpp index da68a5cac76..230f7ef8093 100644 --- a/src/energyplus/ForwardTranslator/ForwardTranslateControllerMechanicalVentilation.cpp +++ b/src/energyplus/ForwardTranslator/ForwardTranslateControllerMechanicalVentilation.cpp @@ -98,7 +98,21 @@ namespace energyplus { } } - // Extensible Groups for DSOAs are pushed in translateSizingZone to retain order of the file + // Extensible Groups for DSOAs are no longer pushed in translateSizingZone to retain order of the file, because: + // 1) Thermal Zones are translated before AirLoopHVACs, so we guarantee proper order (unless we mess something up, like adding a new always + // translated object early on that will call it) + // 2) Doing it in translateSizingZone means that it will NOT be written if the Sizing:Zone isn't, for eg when you have no design days + // but that is a valid use case + auto oa_controller = modelObject.controllerOutdoorAir(); + if (auto oa_sys_ = oa_controller.airLoopHVACOutdoorAirSystem()) { + if (auto a_ = oa_sys_->airLoopHVAC()) { + for (const auto& z : a_->thermalZones()) { + if (auto dsoaOrList_ = getOrCreateThermalZoneDSOA(z)) { + IdfExtensibleGroup eg = idfObject.pushExtensibleGroup({z.nameString(), dsoaOrList_->nameString(), zoneDSZADName(z).value_or("")}); + } + } + } + } m_idfObjects.push_back(idfObject); return boost::optional(idfObject); diff --git a/src/energyplus/ForwardTranslator/ForwardTranslateDesignSpecificationOutdoorAir.cpp b/src/energyplus/ForwardTranslator/ForwardTranslateDesignSpecificationOutdoorAir.cpp index 2b1cabb697a..154708b32ec 100644 --- a/src/energyplus/ForwardTranslator/ForwardTranslateDesignSpecificationOutdoorAir.cpp +++ b/src/energyplus/ForwardTranslator/ForwardTranslateDesignSpecificationOutdoorAir.cpp @@ -4,14 +4,21 @@ ***********************************************************************************************************************/ #include "../ForwardTranslator.hpp" + #include "../../model/Model.hpp" + #include "../../model/DesignSpecificationOutdoorAir.hpp" #include "../../model/DesignSpecificationOutdoorAir_Impl.hpp" #include "../../model/Schedule.hpp" -#include "../../model/Schedule_Impl.hpp" +#include "../../model/Space.hpp" +#include "../../model/ThermalZone.hpp" + #include "../../utilities/core/Logger.hpp" -#include "../../utilities/core/Assert.hpp" +#include "../../utilities/idf/IdfExtensibleGroup.hpp" +#include "../../utilities/plot/ProgressBar.hpp" + #include +#include #include "../../utilities/idd/IddEnums.hpp" #include #include @@ -22,43 +29,110 @@ namespace openstudio { namespace energyplus { - boost::optional ForwardTranslator::translateDesignSpecificationOutdoorAir(DesignSpecificationOutdoorAir& modelObject) { - boost::optional s; - boost::optional value; + namespace detail { + boost::optional translateDesignSpecificationOutdoorAir(DesignSpecificationOutdoorAir& modelObject) { + boost::optional s; + boost::optional value; - IdfObject idfObject(IddObjectType::DesignSpecification_OutdoorAir); + IdfObject idfObject(IddObjectType::DesignSpecification_OutdoorAir); - idfObject.setString(DesignSpecification_OutdoorAirFields::Name, modelObject.name().get()); + idfObject.setString(DesignSpecification_OutdoorAirFields::Name, modelObject.name().get()); - std::string outdoorAirMethod = modelObject.outdoorAirMethod(); - idfObject.setString(DesignSpecification_OutdoorAirFields::OutdoorAirMethod, outdoorAirMethod); + std::string outdoorAirMethod = modelObject.outdoorAirMethod(); + idfObject.setString(DesignSpecification_OutdoorAirFields::OutdoorAirMethod, outdoorAirMethod); - double flowPerPerson = modelObject.outdoorAirFlowperPerson(); - double flowPerArea = modelObject.outdoorAirFlowperFloorArea(); - double flowPerZone = modelObject.outdoorAirFlowRate(); - double ach = modelObject.outdoorAirFlowAirChangesperHour(); + double flowPerPerson = modelObject.outdoorAirFlowperPerson(); + double flowPerArea = modelObject.outdoorAirFlowperFloorArea(); + double flowPerZone = modelObject.outdoorAirFlowRate(); + double ach = modelObject.outdoorAirFlowAirChangesperHour(); - if (istringEqual(outdoorAirMethod, "Sum") || istringEqual(outdoorAirMethod, "Maximum")) { + if (istringEqual(outdoorAirMethod, "Sum") || istringEqual(outdoorAirMethod, "Maximum")) { - idfObject.setDouble(DesignSpecification_OutdoorAirFields::OutdoorAirFlowperPerson, flowPerPerson); - idfObject.setDouble(DesignSpecification_OutdoorAirFields::OutdoorAirFlowperZoneFloorArea, flowPerArea); - idfObject.setDouble(DesignSpecification_OutdoorAirFields::OutdoorAirFlowperZone, flowPerZone); - idfObject.setDouble(DesignSpecification_OutdoorAirFields::OutdoorAirFlowAirChangesperHour, ach); + idfObject.setDouble(DesignSpecification_OutdoorAirFields::OutdoorAirFlowperPerson, flowPerPerson); + idfObject.setDouble(DesignSpecification_OutdoorAirFields::OutdoorAirFlowperZoneFloorArea, flowPerArea); + idfObject.setDouble(DesignSpecification_OutdoorAirFields::OutdoorAirFlowperZone, flowPerZone); + idfObject.setDouble(DesignSpecification_OutdoorAirFields::OutdoorAirFlowAirChangesperHour, ach); - } else { - LOG(Error, "Unknown OutdoorAirMethod '" << outdoorAirMethod << "' specified for OS:DesignSpecification:OutdoorAir named '" + } else { + LOG_FREE(Error, "openstudio.energyplus.ForwardTranslator", + "Unknown OutdoorAirMethod '" << outdoorAirMethod << "' specified for OS:DesignSpecification:OutdoorAir named '" << modelObject.name().get() << "'"); + return boost::none; + } + + boost::optional schedule = modelObject.outdoorAirFlowRateFractionSchedule(); + if (schedule) { + idfObject.setString(DesignSpecification_OutdoorAirFields::OutdoorAirScheduleName, schedule->name().get()); + } + + return idfObject; + } + } // namespace detail + + boost::optional ForwardTranslator::getOrCreateThermalZoneDSOA(const model::ThermalZone& z) { + + auto objInMapIt = m_zoneDSOAsMap.find(z.handle()); + if (objInMapIt != m_zoneDSOAsMap.end()) { + return objInMapIt->second; + } + + auto translateAndMapDSOA = [this](DesignSpecificationOutdoorAir& dsoa) -> boost::optional { + auto objInMapIt = m_map.find(dsoa.handle()); + if (objInMapIt != m_map.end()) { + return objInMapIt->second; + } + + auto idf_dsoa_ = detail::translateDesignSpecificationOutdoorAir(dsoa); + + if (idf_dsoa_) { + m_idfObjects.push_back(*idf_dsoa_); + m_map.emplace(dsoa.handle(), *idf_dsoa_); + + if (m_progressBar) { + m_progressBar->setValue((int)m_map.size()); + } + } + return idf_dsoa_; + }; + + auto spaces = z.spacesWithDesignSpecificationOutdoorAir(); + if (spaces.empty()) { + m_zoneDSOAsMap.emplace(z.handle(), boost::none); return boost::none; } - boost::optional schedule = modelObject.outdoorAirFlowRateFractionSchedule(); - if (schedule) { - idfObject.setString(DesignSpecification_OutdoorAirFields::OutdoorAirScheduleName, schedule->name().get()); + OptionalIdfObject result; + + if (m_forwardTranslatorOptions.excludeSpaceTranslation() || spaces.size() == 1) { + // Spaces, and therefore DSOAs have been combined already + auto dsoa = spaces.front().designSpecificationOutdoorAir().get(); + result = translateAndMapDSOA(dsoa); + } else { + + // DSOA:SpaceList + IdfObject dsoa_sp(IddObjectType::DesignSpecification_OutdoorAir_SpaceList); + dsoa_sp.setName(z.nameString() + " DSOA Space List"); + + // sort by space name so we ensure consistency/reproducibility + std::sort(spaces.begin(), spaces.end(), WorkspaceObjectNameLess()); + for (const auto& s : spaces) { + auto dsoa = *(s.designSpecificationOutdoorAir()); + if (auto dsoa_ = translateAndMapDSOA(dsoa)) { + dsoa_sp.pushExtensibleGroup({s.nameString(), dsoa_->nameString()}); + } + } + + m_idfObjects.push_back(dsoa_sp); + result = dsoa_sp; } - m_idfObjects.push_back(idfObject); + if (result) { + m_zoneDSOAsMap.emplace(z.handle(), result); + } else { + m_zoneDSOAsMap.emplace(z.handle(), boost::none); + } - return idfObject; + return result; } } // namespace energyplus diff --git a/src/energyplus/ForwardTranslator/ForwardTranslateSizingZone.cpp b/src/energyplus/ForwardTranslator/ForwardTranslateSizingZone.cpp index 190f41ebcfe..8f57da5389f 100644 --- a/src/energyplus/ForwardTranslator/ForwardTranslateSizingZone.cpp +++ b/src/energyplus/ForwardTranslator/ForwardTranslateSizingZone.cpp @@ -15,8 +15,6 @@ #include "../../model/ControllerMechanicalVentilation_Impl.hpp" #include "../../model/ControllerOutdoorAir.hpp" #include "../../model/ControllerOutdoorAir_Impl.hpp" -#include "../../model/DesignSpecificationOutdoorAir.hpp" -#include "../../model/DesignSpecificationOutdoorAir_Impl.hpp" #include "../../model/ThermalZone.hpp" #include "../../model/ThermalZone_Impl.hpp" #include "../../model/Schedule.hpp" @@ -39,6 +37,22 @@ namespace openstudio { namespace energyplus { + boost::optional ForwardTranslator::zoneDSZADName(const ThermalZone& zone) { + // If any of the DSZAD fields is non-default, then it's worth it to translate it. Otherwise it has no effect. + auto sizingZone = zone.sizingZone(); + + const bool isDSZADTranslated = + !(sizingZone.isDesignZoneAirDistributionEffectivenessinCoolingModeDefaulted() + && sizingZone.isDesignZoneAirDistributionEffectivenessinHeatingModeDefaulted() + && sizingZone.isDesignZoneSecondaryRecirculationFractionDefaulted() && sizingZone.isDesignMinimumZoneVentilationEfficiencyDefaulted()); + + if (!isDSZADTranslated) { + return boost::none; + } + + return zone.nameString() + " Design Spec Zone Air Dist"; + } + boost::optional ForwardTranslator::translateSizingZone(SizingZone& modelObject) { boost::optional s; boost::optional value; @@ -198,21 +212,11 @@ namespace energyplus { // * Design Zone Air Distribution Effectiveness in Heating Mode // * Design Zone Secondary Recirculation Fraction // * Design Minimum Zone Ventilation Efficiency - // If any of the DSZAD fields is non-default, then it's worth it to translate it. Otherwise it has no effect. - bool isDSZADTranslated = - !(modelObject.isDesignZoneAirDistributionEffectivenessinCoolingModeDefaulted() - && modelObject.isDesignZoneAirDistributionEffectivenessinHeatingModeDefaulted() - && modelObject.isDesignZoneSecondaryRecirculationFractionDefaulted() && modelObject.isDesignMinimumZoneVentilationEfficiencyDefaulted()); - - // Have to declare it here for scoping, so that we can access it when trying to set it for the Controller:MechanicalVentilation - std::string dSZADName; - - if (isDSZADTranslated) { + if (auto dSZADName_ = zoneDSZADName(thermalZone)) { IdfObject dSZAD(IddObjectType::DesignSpecification_ZoneAirDistribution); - dSZADName = name + " Design Spec Zone Air Dist"; - dSZAD.setName(dSZADName); + dSZAD.setName(*dSZADName_); // register the DSZAD m_idfObjects.push_back(dSZAD); @@ -241,40 +245,6 @@ namespace energyplus { idfObject.setString(Sizing_ZoneFields::DesignSpecificationZoneAirDistributionObjectName, dSZAD.name().get()); } - // Add ThermalZone and associated design objects to ControllerMechanicalVentilation. - // This would be done in forwardTranslateControllerMechanicalVentilation except doing it here maintains proper order of the idf file. - - // Now that Multiple AirLoopHVACs serving the same zone are possible, need to loop on all - // NOTE: translateControllerMechanicalVentilation ensures ControllerMechanicalVentilation doesn't end up with no extensible groups! - for (const auto& airLoopHVAC : thermalZone.airLoopHVACs()) { - if (boost::optional oaSystem = airLoopHVAC.airLoopHVACOutdoorAirSystem()) { - model::ControllerOutdoorAir controllerOutdoorAir = oaSystem->getControllerOutdoorAir(); - model::ControllerMechanicalVentilation controllerMechanicalVentilation = controllerOutdoorAir.controllerMechanicalVentilation(); - if (boost::optional _controllerMechanicalVentilation = translateAndMapModelObject(controllerMechanicalVentilation)) { - IdfExtensibleGroup eg = _controllerMechanicalVentilation->pushExtensibleGroup(); - - // Thermal Zone Name - eg.setString(Controller_MechanicalVentilationExtensibleFields::ZoneorZoneListName, name); - - // DesignSpecificationOutdoorAir - std::vector spaces = thermalZone.spaces(); - - if (!spaces.empty()) { - if (boost::optional designOASpec = spaces.front().designSpecificationOutdoorAir()) { - if (boost::optional _designOASpec = translateAndMapModelObject(designOASpec.get())) { - eg.setString(Controller_MechanicalVentilationExtensibleFields::DesignSpecificationOutdoorAirObjectName, _designOASpec->name().get()); - } - } - } - - // DesignSpecificationZoneAirDistributionObjectName - if (isDSZADTranslated) { - eg.setString(Controller_MechanicalVentilationExtensibleFields::DesignSpecificationZoneAirDistributionObjectName, dSZADName); - } - } - } - } - if (modelObject.accountforDedicatedOutdoorAirSystem()) { idfObject.setString(Sizing_ZoneFields::AccountforDedicatedOutdoorAirSystem, "Yes"); diff --git a/src/energyplus/ForwardTranslator/ForwardTranslateThermalZone.cpp b/src/energyplus/ForwardTranslator/ForwardTranslateThermalZone.cpp index 219fb2ad995..33b2a0e8e3a 100644 --- a/src/energyplus/ForwardTranslator/ForwardTranslateThermalZone.cpp +++ b/src/energyplus/ForwardTranslator/ForwardTranslateThermalZone.cpp @@ -924,110 +924,17 @@ namespace energyplus { OS_ASSERT(sizingZoneIdf); } - boost::optional dsoaList; - bool needToRegisterDSOAList = false; - bool atLeastOneDSOAWasWritten = true; - - if (!m_forwardTranslatorOptions.excludeSpaceTranslation() && sizingZoneIdf) { - // DO not register it yet! E+ will crash if the DSOA Space List ends up empty - dsoaList = IdfObject(openstudio::IddObjectType::DesignSpecification_OutdoorAir_SpaceList); - needToRegisterDSOAList = true; - atLeastOneDSOAWasWritten = false; - dsoaList->setName(tzName + " DSOA Space List"); - } - - // map the design specification outdoor air - boost::optional designSpecificationOutdoorAir; - // For the ZoneVentilation workaround bool createZvs = false; - double zvRateForPeople = 0.0; - double zvRateForArea = 0.0; - double zvRate = 0.0; - double zvRateForVolume = 0.0; - double totVolume = 0.0; - - for (const Space& space : spaces) { - designSpecificationOutdoorAir = space.designSpecificationOutdoorAir(); - if (designSpecificationOutdoorAir) { - - // TODO: We definitely need to do something here... - // TODO: this isn't good. We also need to check the SpaceType-level DSOA... - boost::optional thisDSOA = translateAndMapModelObject(*designSpecificationOutdoorAir); - if (sizingZoneIdf) { - if (m_forwardTranslatorOptions.excludeSpaceTranslation()) { - // point the sizing object to the outdoor air spec - sizingZoneIdf->setString(Sizing_ZoneFields::DesignSpecificationOutdoorAirObjectName, designSpecificationOutdoorAir->nameString()); - } else { - if (needToRegisterDSOAList) { - m_idfObjects.emplace_back(dsoaList.get()); - sizingZoneIdf->setString(Sizing_ZoneFields::DesignSpecificationOutdoorAirObjectName, dsoaList->nameString()); - needToRegisterDSOAList = false; - } - - // push an extensible group on the DSOA:SpaceList - dsoaList->pushExtensibleGroup(std::vector{space.nameString(), thisDSOA->nameString()}); - atLeastOneDSOAWasWritten = true; - } - } - // create zone ventilation if needed - // TODO: we could remove all this code if we used ZoneHVAC:IdealLoadsAirSystem instead of HVACTemplate:Zone:IdealLoadsAirSystem - // We have space level stuff, that we need to write at Zone-level. So we compute the rate for each component (per Person, Floor Area, - // absolute and ACH) by looping on spaces. Then we'll write that by dividing by the total zone number of people, floor area, 1, and volume - // This amount to computing a weighted average - if (zoneEquipment.empty()) { - createZvs = true; - - totVolume += space.volume(); - - double rateForPeople = space.numberOfPeople() * designSpecificationOutdoorAir->outdoorAirFlowperPerson(); - double rateForArea = space.floorArea() * designSpecificationOutdoorAir->outdoorAirFlowperFloorArea(); - double rate = designSpecificationOutdoorAir->outdoorAirFlowRate(); - // ACH * volume = m3/hour, divide by 3600 s/hr to get m3/s - double rateForVolume = space.volume() * designSpecificationOutdoorAir->outdoorAirFlowAirChangesperHour() / 3600.0; - - std::string outdoorAirMethod = designSpecificationOutdoorAir->outdoorAirMethod(); - if (istringEqual(outdoorAirMethod, "Maximum")) { - - double biggestRate = std::max({rateForPeople, rateForArea, rate, rateForVolume}); - - if (rateForPeople == biggestRate) { - //rateForPeople = 0.0; - rateForArea = 0.0; - rate = 0.0; - rateForVolume = 0.0; - } else if (rateForArea == biggestRate) { - rateForPeople = 0.0; - //rateForArea = 0.0; - rate = 0.0; - rateForVolume = 0.0; - } else if (rate == biggestRate) { - rateForPeople = 0.0; - rateForArea = 0.0; - //rate = 0.0; - rateForVolume = 0.0; - } else { - //rateForVolume == biggestRate - rateForPeople = 0.0; - rateForArea = 0.0; - rate = 0.0; - //rateForVolume = 0.0; - } - - } else { - // sum - } - - zvRateForPeople += rateForPeople; - zvRateForArea += rateForArea; - zvRate += rate; - zvRateForVolume += rateForVolume; - } // if zoneEquipment.empty() - } // if dsoa - } // loop on spaces - - if (!atLeastOneDSOAWasWritten && sizingZoneIdf) { + if (auto dsoaOrList_ = getOrCreateThermalZoneDSOA(modelObject.cast())) { + if (sizingZoneIdf) { + sizingZoneIdf->setString(Sizing_ZoneFields::DesignSpecificationOutdoorAirObjectName, dsoaOrList_->nameString()); + } + if (zoneEquipment.empty()) { // We know this is useIdealAirLoads + createZvs = true; + } + } else if (sizingZoneIdf) { // Controller:MechnicalVentilation: Design Specification Outdoor Air Object Name // > If this field is blank, the corresponding DesignSpecification:OutdoorAir object for the zone will come from // > the DesignSpecification:OutdoorAir object referenced by the Sizing:Zone object for the same zone. @@ -1047,6 +954,68 @@ namespace energyplus { } if (createZvs) { + double zvRateForPeople = 0.0; + double zvRateForArea = 0.0; + double zvRate = 0.0; + double zvRateForVolume = 0.0; + double totVolume = 0.0; + for (const Space& space : modelObject.spacesWithDesignSpecificationOutdoorAir()) { + auto dsoa = space.designSpecificationOutdoorAir().get(); + + // create zone ventilation if needed + // TODO: we could remove all this code if we used ZoneHVAC:IdealLoadsAirSystem instead of HVACTemplate:Zone:IdealLoadsAirSystem + // We have space level stuff, that we need to write at Zone-level. So we compute the rate for each component (per Person, Floor Area, + // absolute and ACH) by looping on spaces. Then we'll write that by dividing by the total zone number of people, floor area, 1, and volume + // This amount to computing a weighted average + + // TODO: do we need the multiplier here?! + + totVolume += space.volume(); + + double rateForPeople = space.numberOfPeople() * dsoa.outdoorAirFlowperPerson(); + double rateForArea = space.floorArea() * dsoa.outdoorAirFlowperFloorArea(); + double rate = dsoa.outdoorAirFlowRate(); + // ACH * volume = m3/hour, divide by 3600 s/hr to get m3/s + double rateForVolume = space.volume() * dsoa.outdoorAirFlowAirChangesperHour() / 3600.0; + + std::string outdoorAirMethod = dsoa.outdoorAirMethod(); + if (istringEqual(outdoorAirMethod, "Maximum")) { + + double biggestRate = std::max({rateForPeople, rateForArea, rate, rateForVolume}); + + if (rateForPeople == biggestRate) { + //rateForPeople = 0.0; + rateForArea = 0.0; + rate = 0.0; + rateForVolume = 0.0; + } else if (rateForArea == biggestRate) { + rateForPeople = 0.0; + //rateForArea = 0.0; + rate = 0.0; + rateForVolume = 0.0; + } else if (rate == biggestRate) { + rateForPeople = 0.0; + rateForArea = 0.0; + //rate = 0.0; + rateForVolume = 0.0; + } else { + //rateForVolume == biggestRate + rateForPeople = 0.0; + rateForArea = 0.0; + rate = 0.0; + //rateForVolume = 0.0; + } + + } else { + // sum + } + + zvRateForPeople += rateForPeople; + zvRateForArea += rateForArea; + zvRate += rate; + zvRateForVolume += rateForVolume; + } + if (zvRateForPeople > 0) { // TODO: improve this? // find first people schedule diff --git a/src/energyplus/ForwardTranslator/ForwardTranslateZoneHVACIdealLoadsAirSystem.cpp b/src/energyplus/ForwardTranslator/ForwardTranslateZoneHVACIdealLoadsAirSystem.cpp index d742cc1ba47..a9bb567661f 100644 --- a/src/energyplus/ForwardTranslator/ForwardTranslateZoneHVACIdealLoadsAirSystem.cpp +++ b/src/energyplus/ForwardTranslator/ForwardTranslateZoneHVACIdealLoadsAirSystem.cpp @@ -208,22 +208,9 @@ namespace energyplus { // get the zone that this piece of equipment is connected to boost::optional zone = modelObject.thermalZone(); if (zone) { - // get this zone's space - std::vector spaces = zone->spaces(); - // get the space's design specification outdoor air, if one exists - if (!spaces.empty()) { - boost::optional designSpecificationOutdoorAir; - designSpecificationOutdoorAir = spaces[0].designSpecificationOutdoorAir(); - if (designSpecificationOutdoorAir) { - // translate the design specification outdoor air to idf - boost::optional designSpecificationOutdoorAirIdf; - designSpecificationOutdoorAirIdf = translateAndMapModelObject(*designSpecificationOutdoorAir); - // the translation should complete successfully - OS_ASSERT(designSpecificationOutdoorAirIdf); - // set the field to reference the design specification outdoor air - zoneHVACIdealLoadsAirSystem.setString(ZoneHVAC_IdealLoadsAirSystemFields::DesignSpecificationOutdoorAirObjectName, - designSpecificationOutdoorAirIdf->name().get()); - } + if (auto dsoaOrList_ = getOrCreateThermalZoneDSOA(zone->cast())) { + // set the field to reference the design specification outdoor air + zoneHVACIdealLoadsAirSystem.setString(ZoneHVAC_IdealLoadsAirSystemFields::DesignSpecificationOutdoorAirObjectName, dsoaOrList_->nameString()); } } diff --git a/src/energyplus/Test/ControllerOutdoorAir_GTest.cpp b/src/energyplus/Test/ControllerOutdoorAir_GTest.cpp index 145d7139938..370878fda74 100644 --- a/src/energyplus/Test/ControllerOutdoorAir_GTest.cpp +++ b/src/energyplus/Test/ControllerOutdoorAir_GTest.cpp @@ -16,7 +16,6 @@ #include "../../model/AirTerminalSingleDuctConstantVolumeNoReheat.hpp" #include "../../model/ControllerMechanicalVentilation.hpp" #include "../../model/DesignSpecificationOutdoorAir.hpp" -#include "../../model/DesignDay.hpp" #include "../../model/Node.hpp" #include "../../model/Schedule.hpp" #include "../../model/Space.hpp" @@ -44,10 +43,6 @@ TEST_F(EnergyPlusFixture, ForwardTranslator_ControllerOutdoorAir) { Model m; ForwardTranslator ft; - // TODO: Without a DesignDay, the translateSizingZone is never called, and in turn the Controller:MechanicalVentilation does NOT receive the DSOAs - // This has been the case since the first ever commit of OS SDK on github, but it is wrong IMHO. - DesignDay d(m); - ControllerOutdoorAir controller_oa(m); ControllerMechanicalVentilation controller_mv = controller_oa.controllerMechanicalVentilation(); @@ -136,3 +131,150 @@ TEST_F(EnergyPlusFixture, ForwardTranslator_ControllerOutdoorAir) { } } } + +TEST_F(EnergyPlusFixture, ForwardTranslator_ControllerOutdoorAir_MechanicalVentilation) { + // Test for #3984 + Model m; + ForwardTranslator ft; + + ControllerOutdoorAir controller_oa(m); + ControllerMechanicalVentilation controller_mv = controller_oa.controllerMechanicalVentilation(); + + AirLoopHVAC a(m); + AirLoopHVACOutdoorAirSystem oa_sys(m, controller_oa); + Node supplyOutlet = a.supplyOutletNode(); + EXPECT_TRUE(oa_sys.addToNode(supplyOutlet)); + + // y (=North) + // ▲ + // │ building height = 3m + // 10├────────┼────────┼────────┤ + // │ │ │ │ + // │ │ │ │ + // │ Space 1│ Space 2│ Space 3│ + // │ │ │ │ + // └────────┴────────┴────────┴────► x + // 0 10 20 30 + + constexpr double width = 10.0; + constexpr double height = 3.6; // It's convenient for ACH, since 3600 s/hr + + // Counterclockwise points + std::vector floorPointsSpace1{{0.0, 0.0, 0.0}, {0.0, width, 0.0}, {width, width, 0.0}, {width, 0.0, 0.0}}; + + // 3 spaces in a two zones + auto space1 = Space::fromFloorPrint(floorPointsSpace1, height, m, "Space 1").get(); + auto space2 = Space::fromFloorPrint(floorPointsSpace1, height, m, "Space 2").get(); + space2.setXOrigin(width); + auto space3 = Space::fromFloorPrint(floorPointsSpace1, height, m, "Space 3").get(); + space3.setXOrigin(width * 2); + + ThermalZone z12(m); + EXPECT_TRUE(space1.setThermalZone(z12)); + EXPECT_TRUE(space2.setThermalZone(z12)); + + ThermalZone z3(m); + EXPECT_TRUE(space3.setThermalZone(z3)); + + auto alwaysOn = m.alwaysOnDiscreteSchedule(); + AirTerminalSingleDuctConstantVolumeNoReheat atu12(m, alwaysOn); + EXPECT_TRUE(a.addBranchForZone(z12, atu12)); + + AirTerminalSingleDuctConstantVolumeNoReheat atu3(m, alwaysOn); + EXPECT_TRUE(a.addBranchForZone(z3, atu3)); + + EXPECT_FALSE(controller_mv.hasZonesWithDesignSpecificationOutdoorAir()); + { + Workspace w = ft.translateModel(m); + + WorkspaceObjectVector idf_controller_oas(w.getObjectsByType(IddObjectType::Controller_OutdoorAir)); + EXPECT_EQ(1, idf_controller_oas.size()); + EXPECT_EQ(1, w.getObjectsByType(IddObjectType::AirLoopHVAC).size()); + EXPECT_EQ(1, w.getObjectsByType(IddObjectType::AirLoopHVAC_OutdoorAirSystem).size()); + + // Zero zones with a DSOA: no Controller:MechanicalVentilation should have been written + auto& idf_controller_oa = idf_controller_oas.front(); + EXPECT_FALSE(idf_controller_oa.getTarget(Controller_OutdoorAirFields::MechanicalVentilationControllerName)); + EXPECT_EQ(0, w.getObjectsByType(IddObjectType::Controller_MechanicalVentilation).size()); + } + + // Create an Office Space Type. Assign to Space 1 & 2 only (not 3), it also has a DSOA + SpaceType officeSpaceType(m); + officeSpaceType.setName("officeSpaceType"); + DesignSpecificationOutdoorAir dsoaOffice(m); + dsoaOffice.setName("dsoaOffice"); + EXPECT_TRUE(officeSpaceType.setDesignSpecificationOutdoorAir(dsoaOffice)); + EXPECT_TRUE(space1.setSpaceType(officeSpaceType)); + EXPECT_TRUE(space2.setSpaceType(officeSpaceType)); + + // Space 3 has a space-level DSOA + DesignSpecificationOutdoorAir dsoaSpace3(m); + dsoaSpace3.setName("dsoaSpace3"); + EXPECT_TRUE(space3.setDesignSpecificationOutdoorAir(dsoaSpace3)); + + EXPECT_TRUE(controller_mv.hasZonesWithDesignSpecificationOutdoorAir()); + EXPECT_EQ(2, z12.spacesWithDesignSpecificationOutdoorAir().size()); + EXPECT_EQ(1, z3.spacesWithDesignSpecificationOutdoorAir().size()); + { + Workspace w = ft.translateModel(m); + + WorkspaceObjectVector idf_controller_oas(w.getObjectsByType(IddObjectType::Controller_OutdoorAir)); + EXPECT_EQ(1, idf_controller_oas.size()); + EXPECT_EQ(1, w.getObjectsByType(IddObjectType::AirLoopHVAC).size()); + EXPECT_EQ(1, w.getObjectsByType(IddObjectType::AirLoopHVAC_OutdoorAirSystem).size()); + + // Zones with a DSOA: the Controller:MechanicalVentilation should have been written + auto& idf_controller_oa = idf_controller_oas.front(); + EXPECT_EQ(1, w.getObjectsByType(IddObjectType::Controller_MechanicalVentilation).size()); + ASSERT_TRUE(idf_controller_oa.getTarget(Controller_OutdoorAirFields::MechanicalVentilationControllerName)); + auto idf_controller_mv = idf_controller_oa.getTarget(Controller_OutdoorAirFields::MechanicalVentilationControllerName).get(); + ASSERT_EQ(2, idf_controller_mv.numExtensibleGroups()); + + { + auto w_eg = idf_controller_mv.extensibleGroups()[0].cast(); + EXPECT_EQ(z12.nameString(), w_eg.getString(Controller_MechanicalVentilationExtensibleFields::ZoneorZoneListName).get()); + auto dsoaOrList_ = w_eg.getTarget(Controller_MechanicalVentilationExtensibleFields::DesignSpecificationOutdoorAirObjectName); + ASSERT_TRUE(dsoaOrList_); + EXPECT_EQ(dsoaOrList_->iddObject().type(), IddObjectType::DesignSpecification_OutdoorAir_SpaceList); + EXPECT_EQ(z12.nameString() + " DSOA Space List", dsoaOrList_->nameString()); + } + { + auto w_eg = idf_controller_mv.extensibleGroups()[1].cast(); + EXPECT_EQ(z3.nameString(), w_eg.getString(Controller_MechanicalVentilationExtensibleFields::ZoneorZoneListName).get()); + auto dsoaOrList_ = w_eg.getTarget(Controller_MechanicalVentilationExtensibleFields::DesignSpecificationOutdoorAirObjectName); + ASSERT_TRUE(dsoaOrList_); + EXPECT_EQ(dsoaOrList_->iddObject().type(), IddObjectType::DesignSpecification_OutdoorAir); + EXPECT_EQ(dsoaSpace3.nameString(), dsoaOrList_->nameString()); + } + } + + // We should NOT end up with zones that have no DSOAs on it + space3.resetDesignSpecificationOutdoorAir(); + EXPECT_TRUE(controller_mv.hasZonesWithDesignSpecificationOutdoorAir()); + EXPECT_EQ(2, z12.spacesWithDesignSpecificationOutdoorAir().size()); + EXPECT_EQ(0, z3.spacesWithDesignSpecificationOutdoorAir().size()); + { + Workspace w = ft.translateModel(m); + + WorkspaceObjectVector idf_controller_oas(w.getObjectsByType(IddObjectType::Controller_OutdoorAir)); + EXPECT_EQ(1, idf_controller_oas.size()); + EXPECT_EQ(1, w.getObjectsByType(IddObjectType::AirLoopHVAC).size()); + EXPECT_EQ(1, w.getObjectsByType(IddObjectType::AirLoopHVAC_OutdoorAirSystem).size()); + + // Zones with a DSOA: the Controller:MechanicalVentilation should have been written + auto& idf_controller_oa = idf_controller_oas.front(); + EXPECT_EQ(1, w.getObjectsByType(IddObjectType::Controller_MechanicalVentilation).size()); + ASSERT_TRUE(idf_controller_oa.getTarget(Controller_OutdoorAirFields::MechanicalVentilationControllerName)); + auto idf_controller_mv = idf_controller_oa.getTarget(Controller_OutdoorAirFields::MechanicalVentilationControllerName).get(); + ASSERT_EQ(1, idf_controller_mv.numExtensibleGroups()); + + { + auto w_eg = idf_controller_mv.extensibleGroups()[0].cast(); + EXPECT_EQ(z12.nameString(), w_eg.getString(Controller_MechanicalVentilationExtensibleFields::ZoneorZoneListName).get()); + auto dsoaOrList_ = w_eg.getTarget(Controller_MechanicalVentilationExtensibleFields::DesignSpecificationOutdoorAirObjectName); + ASSERT_TRUE(dsoaOrList_); + EXPECT_EQ(dsoaOrList_->iddObject().type(), IddObjectType::DesignSpecification_OutdoorAir_SpaceList); + EXPECT_EQ(z12.nameString() + " DSOA Space List", dsoaOrList_->nameString()); + } + } +}