From c64e294f3efffe7bd4ac23d6883a52226d98d52a Mon Sep 17 00:00:00 2001 From: Ski90Moo Date: Tue, 14 Jul 2026 11:42:35 +0200 Subject: [PATCH 1/3] feat: add hot-gas reheat unitary system with Coil:Heating:Desuperheater Adds 'Unitary - Single Speed DX cooling - Elec heat - CAV - Hotgas reheat' to the default HVAC library, using CoolReheat dehumidification control with a Coil:Heating:Desuperheater as the reheat coil. The desuperheater reclaims waste heat from the unitary system's own DX cooling coil rather than using resistance reheat, avoiding the extra energy cost of electric reheat. Also fixes AirLoopHVACUnitarySystem cloning (drag-from-library and "copy system"): ModelObject::clone() only remaps parent-child ownership, but CoilHeatingDesuperheater::heatingSource() is a lateral reference to a sibling coil, so it was left unset on the clone. fixupClonedDesuperheaterHeatingSource() re-links the cloned desuperheater coil to the cloned cooling coil. Co-Authored-By: Claude Sonnet 5 --- .../Resources/default/hvac_library.osm | 58 +++++++++++++++++++ src/openstudio_lib/HVACSystemsController.cpp | 36 ++++++++++++ 2 files changed, 94 insertions(+) diff --git a/src/openstudio_app/Resources/default/hvac_library.osm b/src/openstudio_app/Resources/default/hvac_library.osm index 54c76447f..f23fb1598 100644 --- a/src/openstudio_app/Resources/default/hvac_library.osm +++ b/src/openstudio_app/Resources/default/hvac_library.osm @@ -6999,6 +6999,64 @@ OS:AirLoopHVAC:UnitarySystem, , !- Heat Recovery Water Outlet Node Name ; !- Design Specification Multispeed Object Name +OS:Coil:Heating:Desuperheater, + {bdb03443-be0a-4953-8eb4-e6014bbced83}, !- Handle + Desuperheater Reheat Coil, !- Name + {9f54092d-a4a8-41b8-a381-c4c332ecb843}, !- Availability Schedule Name + 0.25, !- Heat Reclaim Recovery Efficiency + , !- Air Inlet Node Name + , !- Air Outlet Node Name + {983b9c20-5a9e-49eb-9770-5c1545cc6b65}, !- Heating Source Name + 0; !- On Cycle Parasitic Electric Load {W} + +OS:AirLoopHVAC:UnitarySystem, + {dbc42aea-40d9-42a6-9016-87cc16813b11}, !- Handle + Unitary - Single Speed DX cooling - Elec heat - CAV - Hotgas reheat, !- Name + Load, !- Control Type + , !- Controlling Zone or Thermostat Location + CoolReheat, !- Dehumidification Control Type + {9f54092d-a4a8-41b8-a381-c4c332ecb843}, !- Availability Schedule Name + , !- Air Inlet Node Name + , !- Air Outlet Node Name + {0c016d27-85cb-4254-8b85-9a16c4586de5}, !- Supply Fan Name + BlowThrough, !- Fan Placement + {9f54092d-a4a8-41b8-a381-c4c332ecb843}, !- Supply Air Fan Operating Mode Schedule Name + {ca3b3fed-58b0-4f64-85f9-257f45d033a5}, !- Heating Coil Name + 1, !- DX Heating Coil Sizing Ratio + {983b9c20-5a9e-49eb-9770-5c1545cc6b65}, !- Cooling Coil Name + No, !- Use DOAS DX Cooling Coil + 2, !- DOAS DX Cooling Coil Leaving Minimum Air Temperature {C} + LatentOrSensibleLoadControl, !- Latent Load Control + {bdb03443-be0a-4953-8eb4-e6014bbced83}, !- Supplemental Heating Coil Name + SupplyAirFlowRate, !- Supply Air Flow Rate Method During Cooling Operation + autosize, !- Supply Air Flow Rate During Cooling Operation {m3/s} + , !- Supply Air Flow Rate Per Floor Area During Cooling Operation {m3/s-m2} + , !- Fraction of Autosized Design Cooling Supply Air Flow Rate + , !- Design Supply Air Flow Rate Per Unit of Capacity During Cooling Operation {m3/s-W} + SupplyAirFlowRate, !- Supply Air Flow Rate Method During Heating Operation + autosize, !- Supply Air Flow Rate During Heating Operation {m3/s} + , !- Supply Air Flow Rate Per Floor Area during Heating Operation {m3/s-m2} + , !- Fraction of Autosized Design Heating Supply Air Flow Rate + , !- Design Supply Air Flow Rate Per Unit of Capacity During Heating Operation {m3/s-W} + None, !- Supply Air Flow Rate Method When No Cooling or Heating is Required + , !- Supply Air Flow Rate When No Cooling or Heating is Required {m3/s} + , !- Supply Air Flow Rate Per Floor Area When No Cooling or Heating is Required {m3/s-m2} + , !- Fraction of Autosized Design Cooling Supply Air Flow Rate When No Cooling or Heating is Required + , !- Fraction of Autosized Design Heating Supply Air Flow Rate When No Cooling or Heating is Required + , !- Design Supply Air Flow Rate Per Unit of Capacity During Cooling Operation When No Cooling or Heating is Required {m3/s-W} + , !- Design Supply Air Flow Rate Per Unit of Capacity During Heating Operation When No Cooling or Heating is Required {m3/s-W} + Yes, !- No Load Supply Air Flow Rate Control Set To Low Speed + 80, !- Maximum Supply Air Temperature {C} + 21, !- Maximum Outdoor Dry-Bulb Temperature for Supplemental Heater Operation {C} + , !- Outdoor Dry-Bulb Temperature Sensor Node Name + 0, !- Ancilliary On-Cycle Electric Power {W} + 0, !- Ancilliary Off-Cycle Electric Power {W} + , !- Design Heat Recovery Water Flow Rate {m3/s} + 80, !- Maximum Temperature for Heat Recovery {C} + , !- Heat Recovery Water Inlet Node Name + , !- Heat Recovery Water Outlet Node Name + ; !- Design Specification Multispeed Object Name + OS:Fan:OnOff, {e803e540-d848-409c-9bad-0c0dc9c7b50a}, !- Handle Fan On Off 14, !- Name diff --git a/src/openstudio_lib/HVACSystemsController.cpp b/src/openstudio_lib/HVACSystemsController.cpp index f5f498e8e..a20b811e3 100644 --- a/src/openstudio_lib/HVACSystemsController.cpp +++ b/src/openstudio_lib/HVACSystemsController.cpp @@ -92,6 +92,12 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include #include #include @@ -519,6 +525,34 @@ void HVACLayoutController::addLibraryObjectToTopLevel(const OSItemId& itemId) { message.exec(); } +namespace { + // ModelObject::clone() only remaps true parent-child ownership (children()), so it correctly + // duplicates a UnitarySystem's fan/coils. But CoilHeatingDesuperheater::heatingSource() is a + // lateral reference to a sibling coil rather than a child, so it is left unset by the clone. + // If that sibling is the unitary system's own cooling coil, re-link the clone to it. + // Walks the full cloned subtree since the UnitarySystem may be nested (e.g. cloning a whole + // AirLoopHVAC via "copy system") rather than being the clone root itself. + void fixupClonedDesuperheaterHeatingSource(model::ModelObject clonedObject) { + if (boost::optional unitary = clonedObject.optionalCast()) { + if (boost::optional supplementalCoil = unitary->supplementalHeatingCoil()) { + boost::optional desuperheater = supplementalCoil->optionalCast(); + if (desuperheater && !desuperheater->heatingSource()) { + if (boost::optional coolingCoil = unitary->coolingCoil()) { + desuperheater->setHeatingSource(coolingCoil.get()); + } + } + } + } + + // children() is declared on ParentObject, not the generic ModelObject, so cast before recursing. + if (boost::optional parent = clonedObject.optionalCast()) { + for (const model::ModelObject& child : parent->children()) { + fixupClonedDesuperheaterHeatingSource(child); + } + } + } +} // namespace + void HVACLayoutController::addLibraryObjectToModelNode(const OSItemId& itemId, model::HVACComponent& comp) { model::OptionalModelObject object; bool remove = false; @@ -528,6 +562,7 @@ void HVACLayoutController::addLibraryObjectToModelNode(const OSItemId& itemId, m if (object) { if (!doc->fromModel(itemId)) { object = object->clone(comp.model()); + fixupClonedDesuperheaterHeatingSource(object.get()); remove = true; } } @@ -893,6 +928,7 @@ void HVACSystemsController::onCopySystemClicked() { auto loop = currentLoop(); if (loop) { auto clone = loop->clone(loop->model()); + fixupClonedDesuperheaterHeatingSource(clone); setCurrentHandle(toQString(clone.handle())); } } From a3459ff576c49ff6dc60f514bcd19141083757b3 Mon Sep 17 00:00:00 2001 From: Ski90Moo Date: Tue, 14 Jul 2026 21:13:57 +0200 Subject: [PATCH 2/3] refactor: generalize clone lateral-reference fixup beyond desuperheater ModelObject::clone() clones each child individually and reattaches it via setParent(), so parent-child edges are correctly remapped, but lateral (sibling-to-sibling) object-list references within the cloned subtree are left pointing at the original objects. The previous fix special-cased just CoilHeatingDesuperheater::heatingSource(), but the same bug applies to any lateral reference (e.g. SetpointManager:MixedAir's node fields). Replace it with a generic fixup: build an old-handle -> clone map by walking the original and cloned subtrees in parallel, then use IddObject::objectLists()/WorkspaceObject::getTarget()/setPointer() to rewrite any object-list field in the clone that still points into the original subtree. This fixes the desuperheater case as before, and any future lateral-reference case, without needing a new special case per object type. Co-Authored-By: Claude Sonnet 5 --- src/openstudio_lib/HVACSystemsController.cpp | 78 ++++++++++++++------ 1 file changed, 56 insertions(+), 22 deletions(-) diff --git a/src/openstudio_lib/HVACSystemsController.cpp b/src/openstudio_lib/HVACSystemsController.cpp index a20b811e3..6e72b565c 100644 --- a/src/openstudio_lib/HVACSystemsController.cpp +++ b/src/openstudio_lib/HVACSystemsController.cpp @@ -92,10 +92,6 @@ #include #include #include -#include -#include -#include -#include #include #include #include @@ -132,6 +128,8 @@ #include #include +#include + #include #include #include @@ -526,31 +524,66 @@ void HVACLayoutController::addLibraryObjectToTopLevel(const OSItemId& itemId) { } namespace { - // ModelObject::clone() only remaps true parent-child ownership (children()), so it correctly - // duplicates a UnitarySystem's fan/coils. But CoilHeatingDesuperheater::heatingSource() is a - // lateral reference to a sibling coil rather than a child, so it is left unset by the clone. - // If that sibling is the unitary system's own cooling coil, re-link the clone to it. - // Walks the full cloned subtree since the UnitarySystem may be nested (e.g. cloning a whole - // AirLoopHVAC via "copy system") rather than being the clone root itself. - void fixupClonedDesuperheaterHeatingSource(model::ModelObject clonedObject) { - if (boost::optional unitary = clonedObject.optionalCast()) { - if (boost::optional supplementalCoil = unitary->supplementalHeatingCoil()) { - boost::optional desuperheater = supplementalCoil->optionalCast(); - if (desuperheater && !desuperheater->heatingSource()) { - if (boost::optional coolingCoil = unitary->coolingCoil()) { - desuperheater->setHeatingSource(coolingCoil.get()); - } + // ModelObject::clone() clones each child individually and reattaches it via setParent(), so + // true parent-child edges are correctly remapped to point within the new subtree. But children + // are cloned one at a time rather than as a batch sharing a single old->new handle table, so any + // *lateral* object-list reference between two siblings in the cloned subtree (e.g. + // CoilHeatingDesuperheater::heatingSource() pointing at a sibling cooling coil, or a + // SetpointManager's node references) is left pointing at the original objects instead of their + // clones. The functions below fix that up generically, for any object type, rather than special + // casing each lateral-reference field as it's discovered. + + // Walks `original` and `clone` in parallel -- children() order is deterministic and preserved by + // clone() -- building a map from each original object's handle to its corresponding clone. + void buildCloneHandleMap(const model::ModelObject& original, const model::ModelObject& clone, + std::map& handleMap) { + handleMap.emplace(original.handle(), clone); + + boost::optional originalParent = original.optionalCast(); + boost::optional cloneParent = clone.optionalCast(); + if (originalParent && cloneParent) { + std::vector originalChildren = originalParent->children(); + std::vector cloneChildren = cloneParent->children(); + if (originalChildren.size() == cloneChildren.size()) { + for (size_t i = 0; i < originalChildren.size(); ++i) { + buildCloneHandleMap(originalChildren[i], cloneChildren[i], handleMap); } } } + } + + // Rewrites every object-list field on clonedObject (and recursively its children) that still + // points at an object handleMap has a clone for, to point at that clone instead. + void remapLateralReferences(model::ModelObject clonedObject, const std::map& handleMap) { + IddObject iddObject = clonedObject.iddObject(); + for (unsigned index = 0; index < clonedObject.numFields(); ++index) { + if (iddObject.objectLists(index).empty()) { + continue; + } + boost::optional target = clonedObject.getTarget(index); + if (!target) { + continue; + } + auto it = handleMap.find(target->handle()); + if (it != handleMap.end() && it->second.handle() != target->handle()) { + clonedObject.setPointer(index, it->second.handle()); + } + } - // children() is declared on ParentObject, not the generic ModelObject, so cast before recursing. if (boost::optional parent = clonedObject.optionalCast()) { for (const model::ModelObject& child : parent->children()) { - fixupClonedDesuperheaterHeatingSource(child); + remapLateralReferences(child, handleMap); } } } + + // `original` is the subtree that was cloned; `clonedObject` is its clone. Re-links any lateral + // reference in the clone that still points into the original subtree. + void fixupClonedReferences(const model::ModelObject& original, model::ModelObject clonedObject) { + std::map handleMap; + buildCloneHandleMap(original, clonedObject, handleMap); + remapLateralReferences(clonedObject, handleMap); + } } // namespace void HVACLayoutController::addLibraryObjectToModelNode(const OSItemId& itemId, model::HVACComponent& comp) { @@ -561,8 +594,9 @@ void HVACLayoutController::addLibraryObjectToModelNode(const OSItemId& itemId, m object = doc->getModelObject(itemId); if (object) { if (!doc->fromModel(itemId)) { + model::ModelObject original = object.get(); object = object->clone(comp.model()); - fixupClonedDesuperheaterHeatingSource(object.get()); + fixupClonedReferences(original, object.get()); remove = true; } } @@ -928,7 +962,7 @@ void HVACSystemsController::onCopySystemClicked() { auto loop = currentLoop(); if (loop) { auto clone = loop->clone(loop->model()); - fixupClonedDesuperheaterHeatingSource(clone); + fixupClonedReferences(loop.get(), clone); setCurrentHandle(toQString(clone.handle())); } } From 27efb2cc4b3f880e41a27ab8e857470fa78c0139 Mon Sep 17 00:00:00 2001 From: Ski90Moo Date: Wed, 15 Jul 2026 13:22:16 +0200 Subject: [PATCH 3/3] chore: gitignore in-source CMake/Conan/Qt build artifacts This repo is built in-place at the repo root rather than in an out-of-source build/ dir, so CMake, Conan, MSBuild, and Qt's moc/rcc all drop generated files directly alongside tracked source. None of that was covered by the existing (out-of-source-build-oriented) ignore rules, so every local build left ~150+ untracked files/dirs in git status. Also ignores the handful of configure_file()-generated files produced from tracked .in templates -- notably AnalyticsHelperSecrets.hxx, which is populated with real secret values at configure time and should never be committed. Co-Authored-By: Claude Sonnet 5 --- .gitignore | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index b0d7e27d8..50c7fc7a1 100644 --- a/.gitignore +++ b/.gitignore @@ -28,8 +28,15 @@ profile/ *.sublime-workspace -# Qt-generated files -src/openstudio_lib/moc_*.cpp +# Qt moc/rcc-generated files, wherever CMake drops them in the tree +moc_*.cpp +moc_*.cpp_parameters +qrc_*.cpp +*.qrc.depends +**/__/ +*_autogen/ +.lupdate/ +.qt/ *.sublime-project cmake-build-debug @@ -45,3 +52,61 @@ clang_format.patch conan-cache .ccache CMakeUserPresets.json + +# This repo is built in-place at the repo root (not an out-of-source build/ dir), so +# CMake/Conan/MSBuild drop generated files alongside tracked source. Ignore those. +/CMakeCache.txt +/CMakePresets.json +/CPackConfig.cmake +/CPackSourceConfig.cmake +/cmakedeps_macros.cmake +/conan_toolchain.cmake +/conanbuild.bat +/conanbuildenv-*.bat +/conandeps_legacy.cmake +/conanrun.bat +/conanrunenv-*.bat +/deactivate_conanbuild.bat +/deactivate_conanrun.bat +/*-Target-release.cmake +/*-release-x86_64-data.cmake +/*Config.cmake +/*ConfigVersion.cmake +/*Targets.cmake +/*-config.cmake +/*-config-version.cmake +/Find*.cmake +/module-*.cmake +/get_output_vars.rb +/output_vars_list.txt +/aqtinstall.log +/build_output.txt +/Products/ +/_deps/ +/OpenStudio-3.11.0/ + +# CMake-generated Visual Studio projects/solution, wherever they land +CMakeFiles/ +cmake_install.cmake +*.vcxproj +*.vcxproj.filters +*.sln + +# Compiled translation binaries (built from the tracked .ts sources) +translations/*.qm + +__pycache__/ + +# EnergyPlus local-docs workflow output (see ENERGYPLUS_DOCS_WORKFLOW.md, itself gitignored) +/ENERGYPLUS_DOCS_WORKFLOW.md +/EnergyPlus/ +/EnergyPlusDocsArchive/ + +# configure_file()-generated from tracked .in templates; AnalyticsHelperSecrets.hxx in +# particular is populated with real secret values at configure time -- never commit it. +src/openstudio_lib/AnalyticsHelperSecrets.hxx +src/openstudio_lib/AnalyticsHelperSecrets.hxx.tmp +src/openstudio_app/AboutBox.hpp +src/model_editor/AboutBox.hpp +src/openstudio_app/OpenStudioApp.rc +src/utilities/OpenStudioApplicationPathHelpers.cxx