Add hot-gas reheat unitary system (Coil:Heating:Desuperheater) - #891
Merged
macumber merged 9 commits intoJul 26, 2026
Merged
Conversation
Closed
5 tasks
macumber
reviewed
Jul 19, 2026
macumber
reviewed
Jul 19, 2026
macumber
reviewed
Jul 19, 2026
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Rename the library template to "Desuperheater reheat" for clarity.
Testing the generalized fixup against the real SDK (loading
hvac_library.osm and cloning the new UnitarySystem both within the same
model and across models) showed remapLateralReferences was a no-op for
CoilHeatingDesuperheater::heatingSource(): clone() doesn't leave that
field pointing at the stale original object, it clears it outright. The
previous implementation only rewrote fields that already had some target
set on the clone, so it silently did nothing for this case -- reproduced
live in the app as a fatal EnergyPlus error (missing heating_source_name
/ heating_source_object_type) when simulating the new system.
Fixed by reading the field to remap from `original` instead of from the
clone, and unconditionally forcing the clone's field to match whenever
the original's target was itself part of the cloned subtree. Verified
against the SDK directly (Ruby) for both same-model ("copy system") and
cross-model (drag-from-library) clone scenarios, and confirmed the
simulation now runs cleanly in the rebuilt app.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ParentObject::children() is true ownership (e.g. a UnitarySystem's own
fan/coils) but does not include HVACComponents placed on a Loop's
supply/demand branches. A UnitarySystem sitting on an AirLoopHVAC's
supply branch is only reachable via supplyComponents(), not children().
Cloning a whole loop ("Copy System") therefore never visited the branch
equipment at all -- including the desuperheater's heating source inside
it -- because buildCloneHandleMap/remapLateralReferences only recursed
via children().
Reproduced live: copying an existing (working) desuperheater airloop and
reassigning zones still hit the same fatal EnergyPlus error, since the
copy's desuperheater was never touched by the fixup.
Fixed by walking children(), supplyComponents(), and demandComponents()
as independently size-matched groups rather than one combined list --
demand-side counts legitimately differ between original and clone
(connected thermal zones are never duplicated by clone(), by design), so
that mismatch must not block fixing up the supply side, where the
equipment lines up 1:1 with the original.
Verified against the SDK directly (Ruby) by reproducing the actual
AirLoopHVAC topology (OA system, unitary system, zone, terminal) and
confirmed live in the app: both "drag from library" and "Copy System" on
a whole airloop now correctly re-wire the desuperheater's heating source
and a manually-added SetpointManager:MixedAir's node references.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same traversal gap as loop supply/demand branches, one level deeper: equipment placed on an AirLoopHVACOutdoorAirSystem's outdoor-air or relief branch (e.g. an evaporative cooler, or a SetpointManager:MixedAir sitting on one of those nodes) is reachable only via oaComponents()/reliefComponents(), not children() and not the loop's own supplyComponents()/demandComponents() (which only see the OA system as a single opaque object on the main branch). Reproduced with the stock File > Examples > Example Model: its airloop's outdoor air system has an evaporative cooler with a SetpointManager: MixedAir on it. Copying that airloop left the copy's setpoint manager's node fields empty, since the fixup never visited that branch at all. Fixed by adding oaComponents()/reliefComponents() as two more independently size-matched groups in childSubtreeObjectGroups(). Verified against the SDK directly (Ruby, using OpenStudio::Model.exampleModel) -- all 4 of the example model's SetpointManagerMixedAir objects correctly resolve their node references on the clone -- and confirmed live in the rebuilt app. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ski90Moo
force-pushed
the
feature/desuperheater-hotgas-reheat
branch
from
July 21, 2026 05:56
39e82f5 to
e334fed
Compare
CI flagged the anonymous namespace body as over-indented; clang-format doesn't indent namespace contents under this repo's style.
cppcheck flagged the unnecessary copy -- the parameter is only forwarded into buildCloneHandleMap/remapLateralReferences, both of which already accept it by const reference.
macumber
reviewed
Jul 22, 2026
macumber
reviewed
Jul 22, 2026
macumber
reviewed
Jul 22, 2026
macumber
reviewed
Jul 22, 2026
Move fixupClonedReferences and its helpers from HVACSystemsController.cpp
into src/utilities/CloneFixup.{hpp,cpp} and add unit tests covering the
CoilHeatingDesuperheater::heatingSource() case. Also, per review:
merge the duplicated tree-walk in buildCloneHandleMap/remapLateralReferences
into a single recursive pass (collectClonedObjectPairs), pass clonedObject
by reference instead of copying it down the recursion, and switch the
handle map to std::unordered_map (ordering was never relied on).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
clonedObject is only ever forwarded into collectClonedObjectPairs's const-ref parameter here; the actual mutation happens later on the copy stored in the pairs vector, which is safe since ModelObject shares its underlying object via shared_ptr regardless of wrapper constness. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Coil:Heating:Desuperheateras the supplemental/reheat coil underCoolReheatdehumidification control, so reheat is partially sourced from the cooling coil's waste heat instead of just resistance heat.AirLoopHVACUnitarySystem/loop cloning (both "drag from library" and "Copy System"):ModelObject::clone()only remaps true parent-child relationships. Lateral (sibling-to-sibling) object-list references — likeCoilHeatingDesuperheater::heatingSource(), or aSetpointManager:MixedAir's node fields — are left broken on the clone, sometimes stale, sometimes cleared outright. Added a generic post-clone reference fixup (buildCloneHandleMap+remapLateralReferencesinHVACSystemsController.cpp, usingIddObject::objectLists()/WorkspaceObject::getTarget()/setPointer()) rather than a Desuperheater-specific special case.children(), a loop'ssupplyComponents()/demandComponents(), and an outdoor air system'soaComponents()/reliefComponents()as independently size-matched groups, since branch equipment (units on a loop, or equipment like an evaporative cooler on an OA system's branches) isn't reachable viachildren()alone, and demand-side counts can legitimately differ between original and clone since connected thermal zones are never duplicated.Context
Modeling dehumidification via hot-gas/desuperheater reheat is a recurring real-world need — see this UnmetHours thread, where
CoilCoolingDXTwoStageWithHumidityControlModeproved awkward to control directly and commenters pointed towardCoil:Heating:Desuperheateras a workaround. Worth noting the thread's caveat: a desuperheater coil reclaims waste heat from the refrigerant's superheat and doesn't affect compressor performance, so it's a lower-fidelity approximation of "true" hot-gas reheat, not an exact substitute — but it's a standard, supported EnergyPlus object and a reasonable default-library addition for this use case.Proof this was broken before this fix
You don't need the new desuperheater system to see the underlying bug — it reproduces with a completely stock model:
File > Examples > Example Modelto generate a fresh example model.SetpointManager:MixedAirattached — but on the copy, that setpoint manager's Reference Setpoint / Fan Inlet / Fan Outlet Node fields are all empty (shown as warning icons in the inspector), even though the original's are filled in correctly.That's
ModelObject::clone()'s lateral-reference bug on a totally unmodified example model — this PR's fixup resolves it for both this stock case and the new desuperheater system.Test plan
cmake --build . --target openstudio_lib --config ReleasesucceedsOpenStudio::Model.exampleModel's stock outdoor-air-system evaporative cooler +SetpointManager:MixedAir