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 diff --git a/src/openstudio_app/Resources/default/hvac_library.osm b/src/openstudio_app/Resources/default/hvac_library.osm index 54c76447f..5737c1576 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 - Desuperheater 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..2abc5d2e9 100644 --- a/src/openstudio_lib/HVACSystemsController.cpp +++ b/src/openstudio_lib/HVACSystemsController.cpp @@ -92,6 +92,8 @@ #include #include #include +#include +#include #include #include #include @@ -126,6 +128,8 @@ #include #include +#include + #include #include #include @@ -519,6 +523,117 @@ void HVACLayoutController::addLibraryObjectToTopLevel(const OSItemId& itemId) { message.exec(); } +namespace { + // 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 broken on the clone -- for some field/type + // combinations still pointing at the original object, for others (empirically, heatingSource()) + // cleared to empty outright. The functions below fix that up generically, for any object type, + // rather than special casing each lateral-reference field as it's discovered. + + // ParentObject::children() is true ownership (e.g. a UnitarySystem's 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 reachable via supplyComponents(), not children(). The same + // gap exists one level deeper: equipment on an AirLoopHVACOutdoorAirSystem's outdoor-air/relief + // branches (e.g. an evaporative cooler, or a SetpointManager:MixedAir sitting on one of those + // nodes) is reachable only via oaComponents()/reliefComponents(), not children() either. Cloning + // a whole loop (the "copy system" toolbar action) needs all of these, or the branch equipment -- + // and anything lateral-referenced from inside it -- is never visited at all. + // + // Returned as separate groups, each matched and recursed into independently: cloning a loop does + // not carry over the connected thermal zones on the demand side, so original vs. clone + // demandComponents() can legitimately have different sizes. Treating everything as one combined + // list would let a benign demand-side mismatch abort recursion into the other groups too, where + // the equipment (and any lateral references inside it) *does* line up 1:1 with the original. + std::vector> childSubtreeObjectGroups(const model::ModelObject& object) { + std::vector> groups; + if (boost::optional loop = object.optionalCast()) { + groups.push_back(loop->supplyComponents()); + groups.push_back(loop->demandComponents()); + } + if (boost::optional oaSystem = object.optionalCast()) { + groups.push_back(oaSystem->oaComponents()); + groups.push_back(oaSystem->reliefComponents()); + } + if (boost::optional parent = object.optionalCast()) { + groups.push_back(parent->children()); + } + return groups; + } + + // Walks `original` and `clone` in parallel -- childSubtreeObjectGroups() 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); + + std::vector> originalGroups = childSubtreeObjectGroups(original); + std::vector> cloneGroups = childSubtreeObjectGroups(clone); + for (size_t g = 0; g < originalGroups.size() && g < cloneGroups.size(); ++g) { + const std::vector& originalChildren = originalGroups[g]; + const std::vector& cloneChildren = cloneGroups[g]; + if (originalChildren.size() == cloneChildren.size()) { + for (size_t i = 0; i < originalChildren.size(); ++i) { + buildCloneHandleMap(originalChildren[i], cloneChildren[i], handleMap); + } + } + } + } + + // For every lateral object-list field on `original` whose target was itself cloned (i.e. has an + // entry in handleMap), forces the corresponding field on `clonedObject` to point at that clone. + // Fields are read from `original`, not from `clonedObject`: clone() doesn't necessarily leave a + // lateral reference pointing at the stale original object -- for CoilHeatingDesuperheater's + // heatingSource() it clears the field outright -- so the only reliable source of "what this + // field is supposed to point at" is the original. + void remapLateralReferences(const model::ModelObject& original, model::ModelObject clonedObject, + const std::map& handleMap) { + IddObject iddObject = original.iddObject(); + for (unsigned index = 0; index < original.numFields(); ++index) { + if (iddObject.objectLists(index).empty()) { + continue; + } + boost::optional originalTarget = original.getTarget(index); + if (!originalTarget) { + continue; + } + auto it = handleMap.find(originalTarget->handle()); + if (it == handleMap.end()) { + continue; + } + boost::optional clonedTarget = clonedObject.getTarget(index); + if (clonedTarget && clonedTarget->handle() == it->second.handle()) { + continue; + } + clonedObject.setPointer(index, it->second.handle()); + } + + std::vector> originalGroups = childSubtreeObjectGroups(original); + std::vector> cloneGroups = childSubtreeObjectGroups(clonedObject); + for (size_t g = 0; g < originalGroups.size() && g < cloneGroups.size(); ++g) { + const std::vector& originalChildren = originalGroups[g]; + const std::vector& cloneChildren = cloneGroups[g]; + if (originalChildren.size() == cloneChildren.size()) { + for (size_t i = 0; i < originalChildren.size(); ++i) { + remapLateralReferences(originalChildren[i], cloneChildren[i], handleMap); + } + } + } + } + + // `original` is the subtree that was cloned; `clonedObject` is its clone. Re-links any lateral + // reference in the clone that still points into (or should point into) the original subtree. + void fixupClonedReferences(const model::ModelObject& original, model::ModelObject clonedObject) { + std::map handleMap; + buildCloneHandleMap(original, clonedObject, handleMap); + remapLateralReferences(original, clonedObject, handleMap); + } +} // namespace + void HVACLayoutController::addLibraryObjectToModelNode(const OSItemId& itemId, model::HVACComponent& comp) { model::OptionalModelObject object; bool remove = false; @@ -527,7 +642,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()); + fixupClonedReferences(original, object.get()); remove = true; } } @@ -893,6 +1010,7 @@ void HVACSystemsController::onCopySystemClicked() { auto loop = currentLoop(); if (loop) { auto clone = loop->clone(loop->model()); + fixupClonedReferences(loop.get(), clone); setCurrentHandle(toQString(clone.handle())); } }