diff --git a/src/EnergyPlus/ZoneTempPredictorCorrector.cc b/src/EnergyPlus/ZoneTempPredictorCorrector.cc index 508aa29956c..c8177a80250 100644 --- a/src/EnergyPlus/ZoneTempPredictorCorrector.cc +++ b/src/EnergyPlus/ZoneTempPredictorCorrector.cc @@ -47,6 +47,7 @@ // C++ Headers #include +#include #include // ObjexxFCL Headers @@ -7021,35 +7022,77 @@ void FillPredefinedTableOnThermostatSchedules(EnergyPlusData &state) // J.Glazer - March 2024 using OutputReportPredefined::PreDefTableEntry; auto &orp = state.dataOutRptPredefined; + + // Helper struct so we can sort to ensure a consistent order. + // No matter the order in which the multiple Field Sets (Control Object Type, Control Name), the same thing is reported to the tabular outputs + struct ControlTypeInfo + { + // HVAC::ThermostatType tType = HVAC::ThermostatType::Invalid; + std::string thermostatType; + std::string controlTypeName; + std::string heatSchName; + std::string coolSchName; + + // Only need the operator<, and we use C++17 so I can't use a defaulted 3-way operator<=> + bool operator<(const ControlTypeInfo &other) const + { + return std::tie(this->thermostatType, this->controlTypeName, this->heatSchName, this->coolSchName) < + std::tie(other.thermostatType, other.controlTypeName, other.heatSchName, other.coolSchName); + } + }; + using ControlTypeInfoMemPtr = std::string ControlTypeInfo::*; + + auto joinStrings = [](const std::vector &infos, ControlTypeInfoMemPtr memPtr) -> std::string { + std::vector result; + result.reserve(infos.size()); + for (const auto &info : infos) { + std::string val = info.*memPtr; + if (val.empty()) { + continue; + } + result.emplace_back(std::move(val)); + } + return fmt::format("{}", fmt::join(result, ", ")); + }; + for (int idx = 1; idx <= state.dataZoneCtrls->NumTempControlledZones; ++idx) { auto &tcz = state.dataZoneCtrls->TempControlledZone(idx); PreDefTableEntry(state, orp->pdchStatName, tcz.ZoneName, tcz.Name); PreDefTableEntry(state, orp->pdchStatCtrlTypeSchd, tcz.ZoneName, tcz.ControlTypeSchedName); + + std::vector infos; + infos.reserve(tcz.NumControlTypes); for (int ctInx = 1; ctInx <= tcz.NumControlTypes; ++ctInx) { - PreDefTableEntry(state, orp->pdchStatSchdType1, tcz.ZoneName, HVAC::thermostatTypeNames[(int)tcz.ControlTypeEnum(ctInx)]); - PreDefTableEntry(state, orp->pdchStatSchdTypeName1, tcz.ZoneName, tcz.ControlTypeName(1)); + ControlTypeInfo info; + info.thermostatType = HVAC::thermostatTypeNames[(int)tcz.ControlTypeEnum(ctInx)]; + info.controlTypeName = tcz.ControlTypeName(ctInx); switch (tcz.ControlTypeEnum(ctInx)) { case HVAC::ThermostatType::DualSetPointWithDeadBand: - PreDefTableEntry( - state, orp->pdchStatSchdHeatName, tcz.ZoneName, ScheduleManager::GetScheduleName(state, tcz.SchIndx_DualSetPointWDeadBandHeat)); - PreDefTableEntry( - state, orp->pdchStatSchdCoolName, tcz.ZoneName, ScheduleManager::GetScheduleName(state, tcz.SchIndx_DualSetPointWDeadBandCool)); + info.coolSchName = ScheduleManager::GetScheduleName(state, tcz.SchIndx_DualSetPointWDeadBandCool); + info.heatSchName = ScheduleManager::GetScheduleName(state, tcz.SchIndx_DualSetPointWDeadBandHeat); break; case HVAC::ThermostatType::SingleHeatCool: - PreDefTableEntry( - state, orp->pdchStatSchdHeatName, tcz.ZoneName, ScheduleManager::GetScheduleName(state, tcz.SchIndx_SingleHeatCoolSetPoint)); - PreDefTableEntry( - state, orp->pdchStatSchdCoolName, tcz.ZoneName, ScheduleManager::GetScheduleName(state, tcz.SchIndx_SingleHeatCoolSetPoint)); + info.coolSchName = ScheduleManager::GetScheduleName(state, tcz.SchIndx_SingleHeatCoolSetPoint); + info.heatSchName = ScheduleManager::GetScheduleName(state, tcz.SchIndx_SingleHeatCoolSetPoint); break; case HVAC::ThermostatType::SingleCooling: - PreDefTableEntry( - state, orp->pdchStatSchdHeatName, tcz.ZoneName, ScheduleManager::GetScheduleName(state, tcz.SchIndx_SingleCoolSetPoint)); + info.coolSchName = ScheduleManager::GetScheduleName(state, tcz.SchIndx_SingleCoolSetPoint); break; case HVAC::ThermostatType::SingleHeating: - PreDefTableEntry( - state, orp->pdchStatSchdCoolName, tcz.ZoneName, ScheduleManager::GetScheduleName(state, tcz.SchIndx_SingleHeatSetPoint)); + info.heatSchName = ScheduleManager::GetScheduleName(state, tcz.SchIndx_SingleHeatSetPoint); break; } + infos.emplace_back(std::move(info)); + } + std::sort(infos.begin(), infos.end()); + + PreDefTableEntry(state, orp->pdchStatSchdType1, tcz.ZoneName, joinStrings(infos, &ControlTypeInfo::thermostatType)); + PreDefTableEntry(state, orp->pdchStatSchdTypeName1, tcz.ZoneName, joinStrings(infos, &ControlTypeInfo::controlTypeName)); + if (auto heatSchNames = joinStrings(infos, &ControlTypeInfo::heatSchName); !heatSchNames.empty()) { + PreDefTableEntry(state, orp->pdchStatSchdHeatName, tcz.ZoneName, heatSchNames); + } + if (auto coolSchNames = joinStrings(infos, &ControlTypeInfo::coolSchName); !coolSchNames.empty()) { + PreDefTableEntry(state, orp->pdchStatSchdCoolName, tcz.ZoneName, coolSchNames); } } } diff --git a/tst/EnergyPlus/unit/ZoneTempPredictorCorrector.unit.cc b/tst/EnergyPlus/unit/ZoneTempPredictorCorrector.unit.cc index 1cb3ba2df7f..82cc707483e 100644 --- a/tst/EnergyPlus/unit/ZoneTempPredictorCorrector.unit.cc +++ b/tst/EnergyPlus/unit/ZoneTempPredictorCorrector.unit.cc @@ -1792,6 +1792,17 @@ TEST_F(EnergyPlusFixture, FillPredefinedTableOnThermostatSchedules_Test) using namespace EnergyPlus::OutputReportPredefined; state->dataScheduleMgr->Schedule.allocate(5); + constexpr int SingleHeatingSchIndex = 1; + constexpr int SingleCoolingSchIndex = 2; + constexpr int SingleHeatCoolSchIndex = 3; + constexpr int DualSetPointWDeadBandHeatSchIndex = 4; + constexpr int DualSetPointWDeadBandCoolSchIndex = 5; + state->dataScheduleMgr->Schedule(SingleHeatingSchIndex).Name = "SINGLEHEATINGSCH"; + state->dataScheduleMgr->Schedule(SingleCoolingSchIndex).Name = "SINGLECOOLINGSCH"; + state->dataScheduleMgr->Schedule(SingleHeatCoolSchIndex).Name = "SINGLEHEATCOOLSCH"; + state->dataScheduleMgr->Schedule(DualSetPointWDeadBandHeatSchIndex).Name = "DUALSETPOINTWDEADBANDHEATSCH"; + state->dataScheduleMgr->Schedule(DualSetPointWDeadBandCoolSchIndex).Name = "DUALSETPOINTWDEADBANDCOOLSCH"; + state->dataScheduleMgr->ScheduleInputProcessed = true; auto &orp = *state->dataOutRptPredefined; @@ -1810,8 +1821,7 @@ TEST_F(EnergyPlusFixture, FillPredefinedTableOnThermostatSchedules_Test) dzc.TempControlledZone(1).ControlTypeName.allocate(dzc.TempControlledZone(1).NumControlTypes); dzc.TempControlledZone(1).ControlTypeEnum(1) = HVAC::ThermostatType::SingleHeating; dzc.TempControlledZone(1).ControlTypeName(1) = "control A"; - dzc.TempControlledZone(1).SchIndx_SingleHeatSetPoint = 1; - state->dataScheduleMgr->Schedule(1).Name = "schA"; + dzc.TempControlledZone(1).SchIndx_SingleHeatSetPoint = SingleHeatingSchIndex; dzc.TempControlledZone(2).ZoneName = "zoneB"; dzc.TempControlledZone(2).Name = "stat B"; @@ -1821,8 +1831,7 @@ TEST_F(EnergyPlusFixture, FillPredefinedTableOnThermostatSchedules_Test) dzc.TempControlledZone(2).ControlTypeName.allocate(dzc.TempControlledZone(1).NumControlTypes); dzc.TempControlledZone(2).ControlTypeEnum(1) = HVAC::ThermostatType::SingleCooling; dzc.TempControlledZone(2).ControlTypeName(1) = "control B"; - dzc.TempControlledZone(2).SchIndx_SingleCoolSetPoint = 2; - state->dataScheduleMgr->Schedule(2).Name = "schB"; + dzc.TempControlledZone(2).SchIndx_SingleCoolSetPoint = SingleCoolingSchIndex; dzc.TempControlledZone(3).ZoneName = "zoneC"; dzc.TempControlledZone(3).Name = "stat C"; @@ -1832,8 +1841,7 @@ TEST_F(EnergyPlusFixture, FillPredefinedTableOnThermostatSchedules_Test) dzc.TempControlledZone(3).ControlTypeName.allocate(dzc.TempControlledZone(1).NumControlTypes); dzc.TempControlledZone(3).ControlTypeEnum(1) = HVAC::ThermostatType::SingleHeatCool; dzc.TempControlledZone(3).ControlTypeName(1) = "control C"; - dzc.TempControlledZone(3).SchIndx_SingleHeatCoolSetPoint = 3; - state->dataScheduleMgr->Schedule(3).Name = "schC"; + dzc.TempControlledZone(3).SchIndx_SingleHeatCoolSetPoint = SingleHeatCoolSchIndex; dzc.TempControlledZone(4).ZoneName = "zoneD"; dzc.TempControlledZone(4).Name = "stat D"; @@ -1843,10 +1851,8 @@ TEST_F(EnergyPlusFixture, FillPredefinedTableOnThermostatSchedules_Test) dzc.TempControlledZone(4).ControlTypeName.allocate(dzc.TempControlledZone(1).NumControlTypes); dzc.TempControlledZone(4).ControlTypeEnum(1) = HVAC::ThermostatType::DualSetPointWithDeadBand; dzc.TempControlledZone(4).ControlTypeName(1) = "control D"; - dzc.TempControlledZone(4).SchIndx_DualSetPointWDeadBandHeat = 4; - dzc.TempControlledZone(4).SchIndx_DualSetPointWDeadBandCool = 5; - state->dataScheduleMgr->Schedule(4).Name = "schD"; - state->dataScheduleMgr->Schedule(5).Name = "schE"; + dzc.TempControlledZone(4).SchIndx_DualSetPointWDeadBandHeat = DualSetPointWDeadBandHeatSchIndex; + dzc.TempControlledZone(4).SchIndx_DualSetPointWDeadBandCool = DualSetPointWDeadBandCoolSchIndex; FillPredefinedTableOnThermostatSchedules(*state); @@ -1854,27 +1860,114 @@ TEST_F(EnergyPlusFixture, FillPredefinedTableOnThermostatSchedules_Test) EXPECT_EQ("control schedule A", RetrievePreDefTableEntry(*state, orp.pdchStatCtrlTypeSchd, "zoneA")); EXPECT_EQ("SingleHeating", RetrievePreDefTableEntry(*state, orp.pdchStatSchdType1, "zoneA")); EXPECT_EQ("control A", RetrievePreDefTableEntry(*state, orp.pdchStatSchdTypeName1, "zoneA")); - EXPECT_EQ("schA", RetrievePreDefTableEntry(*state, orp.pdchStatSchdCoolName, "zoneA")); - EXPECT_EQ("NOT FOUND", RetrievePreDefTableEntry(*state, orp.pdchStatSchdHeatName, "zoneA")); + EXPECT_EQ("SINGLEHEATINGSCH", RetrievePreDefTableEntry(*state, orp.pdchStatSchdHeatName, "zoneA")); + EXPECT_EQ("NOT FOUND", RetrievePreDefTableEntry(*state, orp.pdchStatSchdCoolName, "zoneA")); EXPECT_EQ("stat B", RetrievePreDefTableEntry(*state, orp.pdchStatName, "zoneB")); EXPECT_EQ("control schedule B", RetrievePreDefTableEntry(*state, orp.pdchStatCtrlTypeSchd, "zoneB")); EXPECT_EQ("SingleCooling", RetrievePreDefTableEntry(*state, orp.pdchStatSchdType1, "zoneB")); EXPECT_EQ("control B", RetrievePreDefTableEntry(*state, orp.pdchStatSchdTypeName1, "zoneB")); - EXPECT_EQ("NOT FOUND", RetrievePreDefTableEntry(*state, orp.pdchStatSchdCoolName, "zoneB")); - EXPECT_EQ("schB", RetrievePreDefTableEntry(*state, orp.pdchStatSchdHeatName, "zoneB")); + EXPECT_EQ("NOT FOUND", RetrievePreDefTableEntry(*state, orp.pdchStatSchdHeatName, "zoneB")); + EXPECT_EQ("SINGLECOOLINGSCH", RetrievePreDefTableEntry(*state, orp.pdchStatSchdCoolName, "zoneB")); EXPECT_EQ("stat C", RetrievePreDefTableEntry(*state, orp.pdchStatName, "zoneC")); EXPECT_EQ("control schedule C", RetrievePreDefTableEntry(*state, orp.pdchStatCtrlTypeSchd, "zoneC")); EXPECT_EQ("SingleHeatCool", RetrievePreDefTableEntry(*state, orp.pdchStatSchdType1, "zoneC")); EXPECT_EQ("control C", RetrievePreDefTableEntry(*state, orp.pdchStatSchdTypeName1, "zoneC")); - EXPECT_EQ("schC", RetrievePreDefTableEntry(*state, orp.pdchStatSchdCoolName, "zoneC")); - EXPECT_EQ("schC", RetrievePreDefTableEntry(*state, orp.pdchStatSchdHeatName, "zoneC")); + EXPECT_EQ("SINGLEHEATCOOLSCH", RetrievePreDefTableEntry(*state, orp.pdchStatSchdHeatName, "zoneC")); + EXPECT_EQ("SINGLEHEATCOOLSCH", RetrievePreDefTableEntry(*state, orp.pdchStatSchdCoolName, "zoneC")); EXPECT_EQ("stat D", RetrievePreDefTableEntry(*state, orp.pdchStatName, "zoneD")); EXPECT_EQ("control schedule D", RetrievePreDefTableEntry(*state, orp.pdchStatCtrlTypeSchd, "zoneD")); EXPECT_EQ("DualSetPointWithDeadBand", RetrievePreDefTableEntry(*state, orp.pdchStatSchdType1, "zoneD")); EXPECT_EQ("control D", RetrievePreDefTableEntry(*state, orp.pdchStatSchdTypeName1, "zoneD")); - EXPECT_EQ("schE", RetrievePreDefTableEntry(*state, orp.pdchStatSchdCoolName, "zoneD")); - EXPECT_EQ("schD", RetrievePreDefTableEntry(*state, orp.pdchStatSchdHeatName, "zoneD")); + EXPECT_EQ("DUALSETPOINTWDEADBANDHEATSCH", RetrievePreDefTableEntry(*state, orp.pdchStatSchdHeatName, "zoneD")); + EXPECT_EQ("DUALSETPOINTWDEADBANDCOOLSCH", RetrievePreDefTableEntry(*state, orp.pdchStatSchdCoolName, "zoneD")); +} + +TEST_F(EnergyPlusFixture, FillPredefinedTableOnThermostatSchedules_MultipleControls) +{ + using namespace EnergyPlus::OutputReportPredefined; + + state->dataScheduleMgr->Schedule.allocate(6); + constexpr int SingleHeatingSchIndex = 1; + constexpr int SingleCoolingSchIndex = 2; + constexpr int SingleHeatCoolSchIndex = 3; + constexpr int DualSetPointWDeadBandHeatSchIndex = 4; + constexpr int DualSetPointWDeadBandCoolSchIndex = 5; + constexpr int CTSchedIndex = 6; + state->dataScheduleMgr->Schedule(SingleHeatingSchIndex).Name = "SINGLEHEATINGSCH"; + state->dataScheduleMgr->Schedule(SingleCoolingSchIndex).Name = "SINGLECOOLINGSCH"; + state->dataScheduleMgr->Schedule(SingleHeatCoolSchIndex).Name = "SINGLEHEATCOOLSCH"; + state->dataScheduleMgr->Schedule(DualSetPointWDeadBandHeatSchIndex).Name = "DUALSETPOINTWDEADBANDHEATSCH"; + state->dataScheduleMgr->Schedule(DualSetPointWDeadBandCoolSchIndex).Name = "DUALSETPOINTWDEADBANDCOOLSCH"; + state->dataScheduleMgr->Schedule(CTSchedIndex).Name = "CONTROL SCHEDULE"; + + state->dataScheduleMgr->ScheduleInputProcessed = true; + + auto &orp = *state->dataOutRptPredefined; + auto &dzc = *state->dataZoneCtrls; + + SetPredefinedTables(*state); + + constexpr int NumControlTypes = 4; + dzc.NumTempControlledZones = NumControlTypes; + dzc.TempControlledZone.allocate(dzc.NumTempControlledZones); + + // [1, 2, 3, 4] + std::vector order(NumControlTypes); + std::iota(order.begin(), order.end(), 1); + for (size_t i = 0; i < order.size(); ++i) { + char zoneLetter = char(int('A') + i); + // Simple left rotate: [2, 3, 4, 1], etc + std::rotate(order.begin(), std::next(order.begin()), order.end()); + auto &tcz = dzc.TempControlledZone(i + 1); + + const std::string ZoneName = fmt::format("ZONE {}", zoneLetter); + tcz.ZoneName = ZoneName; + tcz.Name = fmt::format("TSTAT {}", zoneLetter); + tcz.ControlTypeSchedName = state->dataScheduleMgr->Schedule(CTSchedIndex).Name; + tcz.CTSchedIndex = CTSchedIndex; + tcz.NumControlTypes = NumControlTypes; + tcz.ControlTypeEnum.allocate(NumControlTypes); + tcz.ControlTypeName.allocate(NumControlTypes); + + tcz.ControlTypeEnum(order.at(0)) = HVAC::ThermostatType::SingleHeating; + tcz.ControlTypeName(order.at(0)) = "SINGLEHEATING CTRL"; + tcz.SchIndx_SingleHeatSetPoint = SingleHeatingSchIndex; + + tcz.ControlTypeEnum(order.at(1)) = HVAC::ThermostatType::SingleCooling; + tcz.ControlTypeName(order.at(1)) = "SINGLECOOLING CTRL"; + tcz.SchIndx_SingleCoolSetPoint = SingleCoolingSchIndex; + + tcz.ControlTypeEnum(order.at(2)) = HVAC::ThermostatType::SingleHeatCool; + tcz.ControlTypeName(order.at(2)) = "SINGLEHEATCOOL CTRL"; + tcz.SchIndx_SingleHeatCoolSetPoint = SingleHeatCoolSchIndex; + + tcz.ControlTypeEnum(order.at(3)) = HVAC::ThermostatType::DualSetPointWithDeadBand; + tcz.ControlTypeName(order.at(3)) = "DUALSETPOINTWITHDEADBAND CTRL"; + tcz.SchIndx_DualSetPointWDeadBandHeat = DualSetPointWDeadBandHeatSchIndex; + tcz.SchIndx_DualSetPointWDeadBandCool = DualSetPointWDeadBandCoolSchIndex; + } + + FillPredefinedTableOnThermostatSchedules(*state); + + for (size_t i = 0; i < order.size(); ++i) { + char zoneLetter = char(int('A') + i); + const std::string ZoneName = fmt::format("ZONE {}", zoneLetter); + EXPECT_EQ(fmt::format("TSTAT {}", zoneLetter), RetrievePreDefTableEntry(*state, orp.pdchStatName, ZoneName)) << "Failed for " << ZoneName; + EXPECT_EQ("CONTROL SCHEDULE", RetrievePreDefTableEntry(*state, orp.pdchStatCtrlTypeSchd, ZoneName)) << "Failed for " << ZoneName; + EXPECT_EQ("DualSetPointWithDeadBand, SingleCooling, SingleHeatCool, SingleHeating", + RetrievePreDefTableEntry(*state, orp.pdchStatSchdType1, ZoneName)) + << "Failed for " << ZoneName; + EXPECT_EQ("DUALSETPOINTWITHDEADBAND CTRL, SINGLECOOLING CTRL, SINGLEHEATCOOL CTRL, SINGLEHEATING CTRL", + RetrievePreDefTableEntry(*state, orp.pdchStatSchdTypeName1, ZoneName)) + << "Failed for " << ZoneName; + EXPECT_EQ("DUALSETPOINTWDEADBANDHEATSCH, SINGLEHEATCOOLSCH, SINGLEHEATINGSCH", + RetrievePreDefTableEntry(*state, orp.pdchStatSchdHeatName, ZoneName)) + << "Failed for " << ZoneName; + EXPECT_EQ("DUALSETPOINTWDEADBANDCOOLSCH, SINGLECOOLINGSCH, SINGLEHEATCOOLSCH", + RetrievePreDefTableEntry(*state, orp.pdchStatSchdCoolName, ZoneName)) + << "Failed for " << ZoneName; + } }