From a102deeff2d61870c3b7ba243b6022a21536027f Mon Sep 17 00:00:00 2001 From: Pranay Bhange Date: Fri, 23 Oct 2026 21:07:48 +0530 Subject: [PATCH] Fix ZoneHVACPackagedTerminalHeatPump writing a garbage DX Heating Coil Sizing Ratio dXHeatingCoilSizingRatio() called OS_ASSERT then .get() on the optional returned by getDouble(), which is undefined behavior when the field is unset and has no IDD default (it's a \required-field with no \default). In that case the model object silently returns whatever bytes happen to sit in the optional's internal storage, which the ForwardTranslator then writes straight into the IDF as a nonsensical value, causing EnergyPlus to reject the file. This field is normally always set by the constructor, so the unset case only shows up on objects built outside the normal constructor path (eg: a .clone() of a template that itself predates this field being set). Mirror the existing self-healing pattern used by supplyAirFanOperatingModeSchedule() just above it: log an error and fall back to the documented default of 1.0 instead of reading uninitialized memory. Fixes #5635 --- src/model/ZoneHVACPackagedTerminalHeatPump.cpp | 6 ++++++ .../ZoneHVACPackagedTerminalHeatPump_GTest.cpp | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/model/ZoneHVACPackagedTerminalHeatPump.cpp b/src/model/ZoneHVACPackagedTerminalHeatPump.cpp index 40cda15284f..59bc6aaa74b 100644 --- a/src/model/ZoneHVACPackagedTerminalHeatPump.cpp +++ b/src/model/ZoneHVACPackagedTerminalHeatPump.cpp @@ -359,6 +359,12 @@ namespace model { double ZoneHVACPackagedTerminalHeatPump_Impl::dXHeatingCoilSizingRatio() const { boost::optional value = getDouble(OS_ZoneHVAC_PackagedTerminalHeatPumpFields::DXHeatingCoilSizingRatio, true); + if (!value) { + // it is an error if we get here (eg: cloned from an object that had it unset), however we don't want to crash + LOG(Error, "Required DX Heating Coil Sizing Ratio not set, defaulting to 1.0"); + const_cast(this)->setDXHeatingCoilSizingRatio(1.0); // NOLINT + value = getDouble(OS_ZoneHVAC_PackagedTerminalHeatPumpFields::DXHeatingCoilSizingRatio, true); + } OS_ASSERT(value); return value.get(); } diff --git a/src/model/test/ZoneHVACPackagedTerminalHeatPump_GTest.cpp b/src/model/test/ZoneHVACPackagedTerminalHeatPump_GTest.cpp index 0b1dee273df..e5067e4d810 100644 --- a/src/model/test/ZoneHVACPackagedTerminalHeatPump_GTest.cpp +++ b/src/model/test/ZoneHVACPackagedTerminalHeatPump_GTest.cpp @@ -40,6 +40,7 @@ #include "../BoilerHotWater.hpp" #include +#include using namespace openstudio; @@ -308,6 +309,23 @@ TEST_F(ModelFixture, ZoneHVACPackagedTerminalHeatPump_CoilSystemCoolingDXHeatExc model::ZoneHVACPackagedTerminalHeatPump pthp(m, availabilitySchedule, fan, heatingCoil, coolingCoil, supplementalHeatingCoil); } +TEST_F(ModelFixture, ZoneHVACPackagedTerminalHeatPump_DXHeatingCoilSizingRatio_Unset) { + // Test for #5635 - cloning from an object that never had this required field set (eg: an + // older template) must not return whatever garbage memory the underlying getter finds + model::Model m; + model::CoilHeatingDXVariableSpeed heatingCoil(m); + model::CoilCoolingDXVariableSpeed coolingCoil(m); + model::ScheduleCompact availabilitySchedule(m); + model::FanConstantVolume fan(m, availabilitySchedule); + model::CoilHeatingElectric supplementalHeatingCoil(m, availabilitySchedule); + + model::ZoneHVACPackagedTerminalHeatPump pthp(m, availabilitySchedule, fan, heatingCoil, coolingCoil, supplementalHeatingCoil); + EXPECT_EQ(1.0, pthp.dXHeatingCoilSizingRatio()); + + ASSERT_TRUE(pthp.setString(OS_ZoneHVAC_PackagedTerminalHeatPumpFields::DXHeatingCoilSizingRatio, "")); + EXPECT_EQ(1.0, pthp.dXHeatingCoilSizingRatio()); +} + TEST_F(ModelFixture, ZoneHVACPackagedTerminalHeatPump_SupplyAirFanOpSch) { model::Model m; model::CoilHeatingDXVariableSpeed heatingCoil(m);