Repository: NREL/OpenStudio (language: python)

Issue:
terminal clones with airloop but then cannot be connected to zone
I'm cloning an airloop with a terminal. The terminal and some connections (zone splitter and mixer) come through but it is not connected to any zone. It makes sense that it would not be connected to a zone but the problem is that I can't seem to find a way to connect it to a zone once it has been cloned over. I tried:

1. Using addBranchForZone directly (no error but the terminal didn't get connected to the zone)
2. First using disconnect and then addBranchForZone (same outcome)

To get things to work I had to remove and rebuild the terminal from scratch and then use addBranchForZone 

I've attached the model I get after running clone so you can see what connections come through.

[Terminal_Bug.zip](https://github.com/NREL/OpenStudio/files/9445840/Terminal_Bug.zip)

terminal clones with airloop but then cannot be connected to zone
I'm cloning an airloop with a terminal. The terminal and some connections (zone splitter and mixer) come through but it is not connected to any zone. It makes sense that it would not be connected to a zone but the problem is that I can't seem to find a way to connect it to a zone once it has been cloned over. I tried:

1. Using addBranchForZone directly (no error but the terminal didn't get connected to the zone)
2. First using disconnect and then addBranchForZone (same outcome)

To get things to work I had to remove and rebuild the terminal from scratch and then use addBranchForZone 

I've attached the model I get after running clone so you can see what connections come through.

[Terminal_Bug.zip](https://github.com/NREL/OpenStudio/files/9445840/Terminal_Bug.zip)

multi speed DX coil stage mapping does not clone properly with airloop
Issue overview
--------------

## Current Behavior

When I clone an airloop with multi speed DX coils using the OS API, the coils themselves are cloned properly, as well as their corresponding stages (dx multi speed stage data objects). But the stage assignments do not map over (the stage fields in the coil object are blank)

## Expected Behavior

It should work.

## Steps to Reproduce

Let me know what details you need to investigate. I don't think it makes sense to attach a model will blank fields as evidence. It wouldn't really show anything...

## Possible Solution
<!--- Optional, but if you can, suggest a fix/reason for the bug, -->

## Details

### Environment

Some additional details about your environment for this issue (if relevant):
 - Version of OpenStudio (if using an intermediate build, include SHA): 3.4.0


Discussion / hints:
I cannot reproduce this. How are you trying to call addBranchForZone on the cloned loop? Are you also trying to supply the cloned ATU?

Here the code that I used to test this.

```ruby
include OpenStudio::Model

m = Model.new

a = AirLoopHVAC.new(m)

z = ThermalZone.new(m)
atu = AirTerminalSingleDuctConstantVolumeNoReheat.new(m, m.alwaysOnDiscreteSchedule)

a.addBranchForZone(z, atu)

a.supplyInletNode.setName("#{a.name} Supply Inlet Node")
a.supplyOutletNode.setName("#{a.name} Supply Outlet Node")
if a.mixedAirNode.is_initialized
  a.mixedAirNode.get.setName("#{a.name} Mixed Air Node")
end
a.demandInletNode.setName("#{a.name} Demand Inlet Node")
a.demandOutletNode.setName("#{a.name} Demand Outlet Node")

a.zoneMixer.setName("#{a.name} Demand ConnectorMixer")
a.zoneSplitter.setName("#{a.name} Demand ConnectorSplitter")

# Rename Zone Air Nodes
m.getThermalZones.each { |z| z.zoneAirNode.setName("#{z.name} Zone Air Node") }

puts "Original Loop Components: #{a.components.map{|c| c.nameString}}"

z2 = ThermalZone.new(m)
a2 = a.clone(m).to_AirLoopHVAC.get()

puts "\nClone Loop Components before adding zone: #{a2.components.map{|c| c.nameString}}"
a2.addBranchForZone(z2)
puts "\nClone Loop Components after adding zone: #{a2.components.map{|c| c.nameString}}"
puts "\nz2.equipment:"
puts z2.equipment
```

```ruby
Original Loop Components: ["Air Loop HVAC 1 Supply Inlet Node", "Air Loop HVAC 1 Supply Outlet Node", "Air Loop HVAC 1 Demand Inlet Node", "Air Loop HVAC 1 Demand ConnectorSplitter", "Node 8", "Air Terminal Single Duct Constant Volume No Reheat 1", "Node 5", "Thermal Zone 1", "Node 7", "Air Loop HVAC 1 Demand ConnectorMixer", "Air Loop HVAC 1 Demand Outlet Node"]

Clone Loop Components before adding zone: ["Node 2", "Node 3", "Node 4", "Air Loop HVAC Zone Splitter 1", "Node 10", "Air Terminal Single Duct Constant Volume No Reheat 2", "Node 9", "Air Loop HVAC Zone Mixer 1", "Node 6"]

Clone Loop Components after adding zone: ["Node 2", "Node 3", "Node 4", "Air Loop HVAC Zone Splitter 1", "Node 10", "Air Terminal Single Duct Constant Volume No Reheat 2", "Node 9", "Thermal Zone 2", "Node 11", "Air Loop HVAC Zone Mixer 1", "Node 6"]

z2.equipment:
OS:AirTerminal:SingleDuct:ConstantVolume:NoReheat,
  {4d4c549d-8bd1-4d4a-a25b-9800587065ba}, !- Handle
  Air Terminal Single Duct Constant Volume No Reheat 2, !- Name
  {f99680f9-7263-41d7-b4e0-5c086f88b033}, !- Availability Schedule Name
  {5d50ed5d-3de1-4271-b471-9f107a3b28e6}, !- Air Inlet Node Name
  {73b57f63-3370-4cff-885c-044f5e9fca16}, !- Air Outlet Node Name
  AutoSize;                               !- Maximum Air Flow Rate {m3/s}
```
The AirLoopHVACs are somewhat weird if you're not used to them...

Basically we have two overloads of AirLoopHVAC::addBranchForZone:

```
    bool addBranchForZone(openstudio::model::ThermalZone& thermalZone)

    bool addBranchForZone(ThermalZone& thermalZone, HVACComponent& airTerminal);
```

First `addBranchForZone(ThermalZone& thermalZone, HVACComponent& airTerminal)` will return false if you pass an airTerminal that is already on an AirLoopHVAC. So if you pass the cloned ATU, which is already on the cloned loop, it will not do anything.

`addBranchForZone(ThermalZone)` will find the last air terminal (if any) and then either:
* If the terminal isn't already connected to a zone (the connection node is right after it), it will connect to it. 
* If the terminal already has a zone, it clones that terminal and attaches to it.

(PS: in the future, **if** you do this with code, please supply some when you open an issue, thanks!)
I cannot reproduce this. How are you trying to call addBranchForZone on the cloned loop? Are you also trying to supply the cloned ATU?

Here the code that I used to test this.

```ruby
include OpenStudio::Model

m = Model.new

a = AirLoopHVAC.new(m)

z = ThermalZone.new(m)
atu = AirTerminalSingleDuctConstantVolumeNoReheat.new(m, m.alwaysOnDiscreteSchedule)

a.addBranchForZone(z, atu)

a.supplyInletNode.setName("#{a.name} Supply Inlet Node")
a.supplyOutletNode.setName("#{a.name} Supply Outlet Node")
if a.mixedAirNode.is_initialized
  a.mixedAirNode.get.setName("#{a.name} Mixed Air Node")
end
a.demandInletNode.setName("#{a.name} Demand Inlet Node")
a.demandOutletNode.setName("#{a.name} Demand Outlet Node")

a.zoneMixer.setName("#{a.name} Demand ConnectorMixer")
a.zoneSplitter.setName("#{a.name} Demand ConnectorSplitter")

# Rename Zone Air Nodes
m.getThermalZones.each { |z| z.zoneAirNode.setName("#{z.name} Zone Air Node") }

puts "Original Loop Components: #{a.components.map{|c| c.nameString}}"

z2 = ThermalZone.new(m)
a2 = a.clone(m).to_AirLoopHVAC.get()

puts "\nClone Loop Components before adding zone: #{a2.components.map{|c| c.nameString}}"
a2.addBranchForZone(z2)
puts "\nClone Loop Components after adding zone: #{a2.components.map{|c| c.nameString}}"
puts "\nz2.equipment:"
puts z2.equipment
```

```ruby
Original Loop Components: ["Air Loop HVAC 1 Supply Inlet Node", "Air Loop HVAC 1 Supply Outlet Node", "Air Loop HVAC 1 Demand Inlet Node", "Air Loop HVAC 1 Demand ConnectorSplitter", "Node 8", "Air Terminal Single Duct Constant Volume No Reheat 1", "Node 5", "Thermal Zone 1", "Node 7", "Air Loop HVAC 1 Demand ConnectorMixer", "Air Loop HVAC 1 Demand Outlet Node"]

Clone Loop Components before adding zone: ["Node 2", "Node 3", "Node 4", "Air Loop HVAC Zone Splitter 1", "Node 10", "Air Terminal Single Duct Constant Volume No Reheat 2", "Node 9", "Air Loop HVAC Zone Mixer 1", "Node 6"]

Clone Loop Components after adding zone: ["Node 2", "Node 3", "Node 4", "Air Loop HVAC Zone Splitter 1", "Node 10", "Air Terminal Single Duct Constant Volume No Reheat 2", "Node 9", "Thermal Zone 2", "Node 11", "Air Loop HVAC Zone Mixer 1", "Node 6"]

z2.equipment:
OS:AirTerminal:SingleDuct:ConstantVolume:NoReheat,
  {4d4c549d-8bd1-4d4a-a25b-9800587065ba}, !- Handle
  Air Terminal Single Duct Constant Volume No Reheat 2, !- Name
  {f99680f9-7263-41d7-b4e0-5c086f88b033}, !- Availability Schedule Name
  {5d50ed5d-3de1-4271-b471-9f107a3b28e6}, !- Air Inlet Node Name
  {73b57f63-3370-4cff-885c-044f5e9fca16}, !- Air Outlet Node Name
  AutoSize;                               !- Maximum Air Flow Rate {m3/s}
```
The AirLoopHVACs are somewhat weird if you're not used to them...

Basically we have two overloads of AirLoopHVAC::addBranchForZone:

```
    bool addBranchForZone(openstudio::model::ThermalZone& thermalZone)

    bool addBranchForZone(ThermalZone& thermalZone, HVACComponent& airTerminal);
```

First `addBranchForZone(ThermalZone& thermalZone, HVACComponent& airTerminal)` will return false if you pass an airTerminal that is already on an AirLoopHVAC. So if you pass the cloned ATU, which is already on the cloned loop, it will not do anything.

`addBranchForZone(ThermalZone)` will find the last air terminal (if any) and then either:
* If the terminal isn't already connected to a zone (the connection node is right after it), it will connect to it. 
* If the terminal already has a zone, it clones that terminal and attaches to it.

(PS: in the future, **if** you do this with code, please supply some when you open an issue, thanks!)
Could you supply a (ideally minimum, complete, verifiable) example of your code that highlight the bug please? 

I've tested a simple clone of the CoilCoolingDXMultiSpeed (not via AirLoopHVAC) and it's already wrong but a different kind of wrong than yours: the stages aren't blank, they are duplicated....

```ruby
include OpenStudio::Model

m = Model.new

cc = CoilCoolingDXMultiSpeed.new(m)
stage1 = CoilCoolingDXMultiSpeedStageData.new(m)
stage2 = CoilCoolingDXMultiSpeedStageData.new(m)
cc.setStages([stage1, stage2])
puts "cc has #{cc.numberOfStages} stages: #{cc.stages.map{|c| c.nameString}}"
=> cc has 2 stages: ["Coil Cooling DX Multi Speed Stage Data 1", "Coil Cooling DX Multi Speed Stage Data 2"]

cc2 = cc.clone(m).to_CoilCoolingDXMultiSpeed.get
```

```
[utilities.idf.Workspace] <-1> Renamed Object of type 'OS:Coil:Cooling:DX:MultiSpeed' and named 'Coil Cooling DX Multi Speed 1' to 'Coil Cooling DX Multi Speed 2' to avoid a name conflict upon WorkspaceObject addition.
[utilities.idf.Workspace] <-1> Renamed Object of type 'OS:Coil:Cooling:DX:MultiSpeed:StageData' and named 'Coil Cooling DX Multi Speed Stage Data 1' to 'Coil Cooling DX Multi Speed Stage Data 3' to avoid a name conflict upon WorkspaceObject addition.
[utilities.idf.Workspace] <-1> Renamed Object of type 'OS:Coil:Cooling:DX:MultiSpeed:StageData' and named 'Coil Cooling DX Multi Speed Stage Data 2' to 'Coil Cooling DX Multi Speed Stage Data 4' to avoid a name conflict upon WorkspaceObject addition.
```

```ruby
puts "cc2 has #{cc2.numberOfStages} stages: #{cc2.stages.map{|c| c.nameString}}"
=> cc2 has 4 stages: ["Coil Cooling DX Multi Speed Stage Data 1", "Coil Cooling DX Multi Speed Stage Data 2", "Coil Cooling DX Multi Speed Stage Data 3", "Coil Cooling DX Multi Speed Stage Data 4"]
```


Fix patch:
diff --git a/src/model/CoilCoolingDXMultiSpeed.cpp b/src/model/CoilCoolingDXMultiSpeed.cpp
index 4e1bd064b0c..898170eadb3 100644
--- a/src/model/CoilCoolingDXMultiSpeed.cpp
+++ b/src/model/CoilCoolingDXMultiSpeed.cpp
@@ -285,6 +285,8 @@ namespace model {
     ModelObject CoilCoolingDXMultiSpeed_Impl::clone(Model model) const {
       auto t_clone = StraightComponent_Impl::clone(model).cast<CoilCoolingDXMultiSpeed>();
 
+      // Deal with the stages: we want them cloned, so first clear and then clone + assign each
+      t_clone.removeAllStages();
       for (const auto& stage : stages()) {
         auto stageClone = stage.clone(model).cast<CoilCoolingDXMultiSpeedStageData>();
         t_clone.addStage(stageClone);


Test patch:
diff --git a/src/model/test/AirLoopHVAC_GTest.cpp b/src/model/test/AirLoopHVAC_GTest.cpp
index e8fd6a8a7d0..4422779f7f4 100644
--- a/src/model/test/AirLoopHVAC_GTest.cpp
+++ b/src/model/test/AirLoopHVAC_GTest.cpp
@@ -48,6 +48,7 @@
 #include "../AirTerminalSingleDuctConstantVolumeNoReheat_Impl.hpp"
 
 #include "../PlantLoop.hpp"
+#include "../PlantLoop_Impl.hpp"
 #include "../CoilCoolingWater.hpp"
 #include "../CoilCoolingWater_Impl.hpp"
 #include "../CoilHeatingWater.hpp"
@@ -72,6 +73,7 @@
 #include "../AirTerminalDualDuctConstantVolume.hpp"
 
 #include "../ThermalZone.hpp"
+#include "../ThermalZone_Impl.hpp"
 #include "../ScheduleCompact.hpp"
 #include "../ScheduleTypeLimits.hpp"
 #include "../ScheduleRuleset.hpp"
@@ -1478,6 +1480,198 @@ TEST_F(ModelFixture, AirLoopHVAC_singleDuct_Clone) {
   EXPECT_EQ(2u, aClone.supplyComponents().size());
 }
 
+TEST_F(ModelFixture, AirLoopHVAC_singleDuct_Clone_WithComponents) {
+  Model m;
+
+  PlantLoop hw_loop(m);
+  PlantLoop chw_loop(m);
+
+  EXPECT_EQ(12, m.getConcreteModelObjects<Node>().size());
+  EXPECT_EQ(4, m.getModelObjects<Splitter>().size());
+  EXPECT_EQ(4, m.getModelObjects<Mixer>().size());
+
+  EXPECT_EQ(6, chw_loop.components(openstudio::IddObjectType::OS_Node).size());
+  EXPECT_EQ(6, hw_loop.components(openstudio::IddObjectType::OS_Node).size());
+
+  AirLoopHVAC a(m, false);
+  EXPECT_FALSE(a.isDualDuct());
+
+  EXPECT_EQ(1, a.supplyOutletNodes().size());
+  EXPECT_EQ(2, a.supplyComponents().size());
+
+  EXPECT_EQ(0, a.supplySplitterOutletNodes().size());
+  EXPECT_FALSE(a.supplySplitterInletNode());
+  EXPECT_FALSE(a.supplySplitter());
+
+  EXPECT_EQ(5, a.components(openstudio::IddObjectType::OS_Node).size());
+  EXPECT_EQ(17, m.getConcreteModelObjects<Node>().size());
+  EXPECT_EQ(5, m.getModelObjects<Splitter>().size());
+  EXPECT_EQ(5, m.getModelObjects<Mixer>().size());
+
+  auto supplyOutletNode = a.supplyOutletNode();
+
+  CoilCoolingWater coolingCoil(m);
+  EXPECT_TRUE(coolingCoil.addToNode(supplyOutletNode));
+  EXPECT_EQ(17, m.getConcreteModelObjects<Node>().size());
+  EXPECT_EQ(6, chw_loop.components(openstudio::IddObjectType::OS_Node).size());
+  EXPECT_EQ(6, hw_loop.components(openstudio::IddObjectType::OS_Node).size());
+  EXPECT_EQ(5, a.components(openstudio::IddObjectType::OS_Node).size());
+  {
+    auto mo = coolingCoil.airOutletModelObject();
+    ASSERT_TRUE(mo);
+    auto node = mo->optionalCast<Node>();
+    ASSERT_TRUE(node);
+    ASSERT_EQ(supplyOutletNode, node.get());
+    EXPECT_TRUE(chw_loop.addDemandBranchForComponent(coolingCoil));
+    EXPECT_EQ(18, m.getConcreteModelObjects<Node>().size());
+    EXPECT_EQ(7, chw_loop.components(openstudio::IddObjectType::OS_Node).size());
+    EXPECT_EQ(6, hw_loop.components(openstudio::IddObjectType::OS_Node).size());
+    EXPECT_EQ(5, a.components(openstudio::IddObjectType::OS_Node).size());
+  }
+
+  CoilHeatingWater heatingCoil(m);
+  EXPECT_TRUE(heatingCoil.addToNode(supplyOutletNode));
+  EXPECT_EQ(19, m.getConcreteModelObjects<Node>().size());
+  EXPECT_EQ(7, chw_loop.components(openstudio::IddObjectType::OS_Node).size());
+  EXPECT_EQ(6, hw_loop.components(openstudio::IddObjectType::OS_Node).size());
+  EXPECT_EQ(6, a.components(openstudio::IddObjectType::OS_Node).size());
+  {
+    auto mo = heatingCoil.airOutletModelObject();
+    ASSERT_TRUE(mo);
+    auto node = mo->optionalCast<Node>();
+    ASSERT_TRUE(node);
+    ASSERT_EQ(supplyOutletNode, node.get());
+    EXPECT_TRUE(hw_loop.addDemandBranchForComponent(heatingCoil));
+    EXPECT_EQ(20, m.getConcreteModelObjects<Node>().size());
+    EXPECT_EQ(7, chw_loop.components(openstudio::IddObjectType::OS_Node).size());
+    EXPECT_EQ(7, hw_loop.components(openstudio::IddObjectType::OS_Node).size());
+    EXPECT_EQ(6, a.components(openstudio::IddObjectType::OS_Node).size());
+  }
+
+  EXPECT_EQ(1, a.supplyOutletNodes().size());
+  EXPECT_EQ(5, a.supplyComponents().size());  // o----CC----o----HC----o
+  EXPECT_EQ(5, a.demandComponents().size());  // 3 nodes, one splitter, one mixer
+
+  EXPECT_FALSE(a.supplySplitter());
+
+  ThermalZone z(m);
+  auto alwaysOn = m.alwaysOnDiscreteSchedule();
+  AirTerminalSingleDuctConstantVolumeNoReheat atu(m, alwaysOn);
+  EXPECT_TRUE(a.addBranchForZone(z, atu));
+
+  EXPECT_EQ(2, m.getConcreteModelObjects<PlantLoop>().size());
+  EXPECT_EQ(1, m.getConcreteModelObjects<CoilCoolingWater>().size());
+  EXPECT_EQ(1, m.getConcreteModelObjects<CoilHeatingWater>().size());
+  EXPECT_EQ(23, m.getConcreteModelObjects<Node>().size());
+  EXPECT_EQ(7, chw_loop.components(openstudio::IddObjectType::OS_Node).size());
+  EXPECT_EQ(7, hw_loop.components(openstudio::IddObjectType::OS_Node).size());
+  EXPECT_EQ(8, a.components(openstudio::IddObjectType::OS_Node).size());
+  EXPECT_EQ(1, a.supplyOutletNodes().size());
+  EXPECT_EQ(5, a.supplyComponents().size());  // o----CC----o----HC----o
+  EXPECT_EQ(9, a.demandComponents().size());  // o----Splitter-----o----ATU----o---Zone----o --- mixer ----o
+  ASSERT_TRUE(coolingCoil.plantLoop());
+  EXPECT_EQ(chw_loop, coolingCoil.plantLoop().get());
+  ASSERT_TRUE(heatingCoil.plantLoop());
+  EXPECT_EQ(hw_loop, heatingCoil.plantLoop().get());
+  {
+    Model m2;
+    // PlantLoops aren't taken with
+    auto a2 = a.clone(m2).cast<AirLoopHVAC>();
+
+    EXPECT_EQ(1, m2.getConcreteModelObjects<AirLoopHVAC>().size());
+    EXPECT_EQ(0, m2.getConcreteModelObjects<PlantLoop>().size());
+    EXPECT_EQ(1, m2.getConcreteModelObjects<AirTerminalSingleDuctConstantVolumeNoReheat>().size());
+    EXPECT_EQ(0, m2.getConcreteModelObjects<ThermalZone>().size());
+    EXPECT_EQ(1, m2.getConcreteModelObjects<CoilCoolingWater>().size());
+    EXPECT_EQ(1, m2.getConcreteModelObjects<CoilHeatingWater>().size());
+
+    EXPECT_EQ(7, m2.getConcreteModelObjects<Node>().size());
+
+    EXPECT_EQ(7, chw_loop.components(openstudio::IddObjectType::OS_Node).size());
+    EXPECT_EQ(7, hw_loop.components(openstudio::IddObjectType::OS_Node).size());
+    EXPECT_EQ(7, a2.components(openstudio::IddObjectType::OS_Node).size());
+    EXPECT_EQ(1, a2.supplyOutletNodes().size());
+    EXPECT_EQ(5, a2.supplyComponents().size());  // o----CC----o----HC----o
+    EXPECT_EQ(7, a2.demandComponents().size());  // o----Splitter-----o----ATU----o --- mixer ----o
+
+    // Ensure the ATU is connectable (cf #4663)
+    auto z2 = z.clone(m2).cast<ThermalZone>();
+    EXPECT_EQ(8, m2.getConcreteModelObjects<Node>().size());
+    EXPECT_EQ(1, m2.getConcreteModelObjects<ThermalZone>().size());
+    EXPECT_EQ(0, z2.equipment().size());
+
+    EXPECT_TRUE(a2.addBranchForZone(z2));
+
+    EXPECT_EQ(9, m2.getConcreteModelObjects<Node>().size());
+    EXPECT_EQ(8, a2.components(openstudio::IddObjectType::OS_Node).size());
+    EXPECT_EQ(1, a2.supplyOutletNodes().size());
+    EXPECT_EQ(5, a2.supplyComponents().size());  // o----CC----o----HC----o
+    EXPECT_EQ(9, a2.demandComponents().size());  // o----Splitter-----o----ATU----o---Zone----o --- mixer ----o
+    EXPECT_EQ(1, z2.equipment().size());
+
+    ASSERT_EQ(1, a2.components(openstudio::IddObjectType::OS_Coil_Cooling_Water).size());
+    auto coolingCoil2 = a2.components(openstudio::IddObjectType::OS_Coil_Cooling_Water)[0].cast<CoilCoolingWater>();
+    EXPECT_FALSE(coolingCoil2.plantLoop());
+
+    ASSERT_EQ(1, a2.components(openstudio::IddObjectType::OS_Coil_Heating_Water).size());
+    auto heatingCoil2 = a2.components(openstudio::IddObjectType::OS_Coil_Heating_Water)[0].cast<CoilHeatingWater>();
+    EXPECT_FALSE(heatingCoil2.plantLoop());
+  }
+
+  {
+    // Same model
+    auto a2 = a.clone(m).cast<AirLoopHVAC>();
+
+    EXPECT_EQ(2, m.getConcreteModelObjects<AirLoopHVAC>().size());
+    EXPECT_EQ(2, m.getConcreteModelObjects<PlantLoop>().size());
+    EXPECT_EQ(2, m.getConcreteModelObjects<AirTerminalSingleDuctConstantVolumeNoReheat>().size());
+    EXPECT_EQ(1, m.getConcreteModelObjects<ThermalZone>().size());
+    EXPECT_EQ(2, m.getConcreteModelObjects<CoilCoolingWater>().size());
+    EXPECT_EQ(2, m.getConcreteModelObjects<CoilHeatingWater>().size());
+
+    // 23 + 7 (new loop) + 2 loops x 2 nodes
+    EXPECT_EQ(34, m.getConcreteModelObjects<Node>().size());
+    EXPECT_EQ(9, chw_loop.components(openstudio::IddObjectType::OS_Node).size());
+    EXPECT_EQ(9, hw_loop.components(openstudio::IddObjectType::OS_Node).size());
+    EXPECT_EQ(8, a.components(openstudio::IddObjectType::OS_Node).size());
+    EXPECT_EQ(1, a.supplyOutletNodes().size());
+    EXPECT_EQ(5, a.supplyComponents().size());  // o----CC----o----HC----o
+    EXPECT_EQ(9, a.demandComponents().size());  // o----Splitter-----o----ATU----o---Zone----o --- mixer ----o
+    EXPECT_EQ(7, a2.components(openstudio::IddObjectType::OS_Node).size());
+    EXPECT_EQ(1, a2.supplyOutletNodes().size());
+    EXPECT_EQ(5, a2.supplyComponents().size());  // o----CC----o----HC----o
+    EXPECT_EQ(7, a2.demandComponents().size());  // o----Splitter-----o----ATU----o --- mixer ----o
+
+    // Ensure the ATU is connectable (cf #4663)
+    auto z2 = z.clone(m).cast<ThermalZone>();
+    EXPECT_EQ(35, m.getConcreteModelObjects<Node>().size());
+    EXPECT_EQ(2, m.getConcreteModelObjects<ThermalZone>().size());
+    EXPECT_EQ(1, z.equipment().size());
+    EXPECT_EQ(0, z2.equipment().size());
+
+    EXPECT_TRUE(a2.addBranchForZone(z2));
+
+    EXPECT_EQ(36, m.getConcreteModelObjects<Node>().size());
+    EXPECT_EQ(8, a2.components(openstudio::IddObjectType::OS_Node).size());
+    EXPECT_EQ(1, a2.supplyOutletNodes().size());
+    EXPECT_EQ(5, a2.supplyComponents().size());  // o----CC----o----HC----o
+    EXPECT_EQ(9, a2.demandComponents().size());  // o----Splitter-----o----ATU----o---Zone----o --- mixer ----o
+    EXPECT_EQ(1, z.equipment().size());
+    EXPECT_EQ(1, z2.equipment().size());
+
+    // Coils are also reconnected to plant loops
+    ASSERT_EQ(1, a2.components(openstudio::IddObjectType::OS_Coil_Cooling_Water).size());
+    auto coolingCoil2 = a2.components(openstudio::IddObjectType::OS_Coil_Cooling_Water)[0].cast<CoilCoolingWater>();
+    ASSERT_TRUE(coolingCoil2.plantLoop());
+    EXPECT_EQ(chw_loop, coolingCoil2.plantLoop().get());
+
+    ASSERT_EQ(1, a2.components(openstudio::IddObjectType::OS_Coil_Heating_Water).size());
+    auto heatingCoil2 = a2.components(openstudio::IddObjectType::OS_Coil_Heating_Water)[0].cast<CoilHeatingWater>();
+    ASSERT_TRUE(heatingCoil2.plantLoop());
+    EXPECT_EQ(hw_loop, heatingCoil2.plantLoop().get());
+  }
+}
+
 TEST_F(ModelFixture, AirLoopHVAC_dualDuct_Clone) {
   // Test for #4060
   Model m;
diff --git a/src/model/test/CoilCoolingDXMultiSpeed_GTest.cpp b/src/model/test/CoilCoolingDXMultiSpeed_GTest.cpp
index 6f19da08ef8..11310be2bfe 100644
--- a/src/model/test/CoilCoolingDXMultiSpeed_GTest.cpp
+++ b/src/model/test/CoilCoolingDXMultiSpeed_GTest.cpp
@@ -38,6 +38,8 @@
 #include "../CurveQuadratic.hpp"
 #include "../CurveQuadratic_Impl.hpp"
 
+#include <algorithm>
+
 using namespace openstudio;
 using namespace openstudio::model;
 
@@ -270,3 +272,38 @@ TEST_F(ModelFixture, CoilCoolingDXMultiSpeed_Remove) {
   auto curves = m.getModelObjects<model::Curve>();
   EXPECT_EQ(curves.size(), m.modelObjects().size());
 }
+
+TEST_F(ModelFixture, CoilCoolingDXMultiSpeed_Clone) {
+  // Ref #4663
+  Model m;
+  CoilCoolingDXMultiSpeed cc(m);
+
+  std::vector<CoilCoolingDXMultiSpeedStageData> stages{CoilCoolingDXMultiSpeedStageData{m}, CoilCoolingDXMultiSpeedStageData{m}};
+  EXPECT_TRUE(cc.setStages(stages));
+
+  EXPECT_EQ(2u, cc.stages().size());
+  EXPECT_EQ(1u, m.getConcreteModelObjects<CoilCoolingDXMultiSpeed>().size());
+  EXPECT_EQ(2u, m.getConcreteModelObjects<CoilCoolingDXMultiSpeedStageData>().size());
+
+  {
+    // Clone in another model first, so we don't affect the original one
+    Model m2;
+    auto cc2 = cc.clone(m2).cast<CoilCoolingDXMultiSpeed>();
+    EXPECT_EQ(2u, cc2.stages().size());
+    EXPECT_EQ(1u, m2.getConcreteModelObjects<CoilCoolingDXMultiSpeed>().size());
+    EXPECT_EQ(2u, m2.getConcreteModelObjects<CoilCoolingDXMultiSpeedStageData>().size());
+  }
+
+  {
+    // Same model: we expect stages to be cloned and reassigned to the clone
+    auto cc2 = cc.clone(m).cast<CoilCoolingDXMultiSpeed>();
+    std::vector<CoilCoolingDXMultiSpeedStageData> stages2 = cc2.stages();
+    EXPECT_EQ(2u, stages2.size());
+    EXPECT_EQ(2u, m.getConcreteModelObjects<CoilCoolingDXMultiSpeed>().size());
+    EXPECT_EQ(4u, m.getConcreteModelObjects<CoilCoolingDXMultiSpeedStageData>().size());
+    for (auto& stage : stages) {
+      EXPECT_TRUE(std::find_if(stages2.cbegin(), stages2.cend(), [&stage](const auto& s) { return s.handle() == stage.handle(); }) == stages2.cend())
+        << "Did not expect to find, in the cloned Coil, the CoilCoolingDXMultiSpeedStageData '" << stage.nameString() << "'";
+    }
+  }
+}