Skip to content

Add hot-gas reheat unitary system (Coil:Heating:Desuperheater)#3

Closed
Ski90Moo wants to merge 5 commits into
developfrom
feature/desuperheater-hotgas-reheat
Closed

Add hot-gas reheat unitary system (Coil:Heating:Desuperheater)#3
Ski90Moo wants to merge 5 commits into
developfrom
feature/desuperheater-hotgas-reheat

Conversation

@Ski90Moo

Copy link
Copy Markdown
Owner

Summary

  • Adds "Unitary - Single Speed DX cooling - Elec heat - CAV - Desuperheater reheat" to the default HVAC library: the existing single-speed DX cooling coil paired with a new Coil:Heating:Desuperheater as the supplemental/reheat coil under CoolReheat dehumidification control, so reheat is sourced from the cooling coil's waste heat instead of resistance heat.
  • Fixes AirLoopHVACUnitarySystem cloning (both "drag from library" and "Copy System"): ModelObject::clone() only remaps true parent-child relationships. Lateral (sibling-to-sibling) object-list references — like CoilHeatingDesuperheater::heatingSource() — are left broken on the clone: sometimes still pointing at the original object, sometimes (empirically) cleared outright. Added a generic post-clone reference fixup (buildCloneHandleMap + remapLateralReferences in HVACSystemsController.cpp, using IddObject::objectLists()/WorkspaceObject::getTarget()/setPointer()) rather than a Desuperheater-specific special case, so this covers the bug class generally — verified it also correctly fixes a manually-added SetpointManager:MixedAir's node references.
  • The traversal walks children(), supplyComponents(), and demandComponents() as independently size-matched groups: equipment sitting on a loop's supply/demand branch (e.g. a UnitarySystem on an AirLoopHVAC) isn't reachable via children() alone, and demand-side component counts legitimately differ between original and clone since connected thermal zones are never duplicated by clone().

Test plan

  • cmake --build . --target openstudio_lib --config Release succeeds
  • Dragged the new system onto a zone in the app and ran the simulation successfully
  • Used "Copy System" on that airloop, reassigned zones on the copy, and ran the simulation successfully
  • Built a standalone test case (a second airloop with a manually-wired SetpointManager:MixedAir) via the openstudio-mcp server, confirmed via "Copy System" that its node references correctly re-point to the clone's own nodes
  • Verified against the SDK directly (Ruby) for both same-model and cross-model clone scenarios before porting each fix to C++

🤖 Generated with Claude Code

Ski90Moo and others added 5 commits July 15, 2026 14:02
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

Copy link
Copy Markdown
Owner Author

Superseded — opened against the actual upstream repo instead: openstudiocoalition#891

@Ski90Moo Ski90Moo closed this Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant