Repository: NREL/OpenStudio (language: python)

Issue:
AirLoopHVAC::clone will duplicate components that are on the relief and intake air streams of an OutdoorAirSystem
Issue overview
--------------

## Current Behavior

Cloning an airloop with air-to-air heat exchanger doubles the number of heat exchangers:
![2023-04-13](https://user-images.githubusercontent.com/69771412/231789411-8e74088b-856e-4c49-8c39-8ea3f452fffa.png)

## Expected Behavior

Shouldn't be cloned twice.

## Steps to Reproduce

```ruby
m = Model.new
a = AirLoopHVAC.new(m)
controller = OpenStudio::Model::ControllerOutdoorAir.new(m)
oas = OpenStudio::Model::AirLoopHVACOutdoorAirSystem.new(m, controller)
oas.addToNode(a.supplyOutletNode)
erv = HeatExchangerAirToAirSensibleAndLatent.new(m)
erv.addToNode(oas.outboardOANode.get)

a.supplyComponents.map(&:nameString)
=> ["Node 1", "Air Loop HVAC Outdoor Air System 1", "Node 2"
oas.components.map(&:nameString)
=> ["Node 6", "Heat Exchanger Air To Air Sensible And Latent 1", "Node 9", "Node 10", "Heat Exchanger Air To Air Sensible And Latent 1", "Node 7"]

a2 = a.clone(m).to_AirLoopHVAC.get

a2.supplyComponents.map(&:nameString)
=> ["Node 8", "Air Loop HVAC Outdoor Air System 2", "Node 11"]

oas2 = a2.airLoopHVACOutdoorAirSystem.get
oas2.components.map(&:nameString)
=> ["Node 15",
 "Heat Exchanger Air To Air Sensible And Latent 3",
 "Node 20",
 "Heat Exchanger Air To Air Sensible And Latent 2",
 "Node 18",
 "Node 19",
 "Heat Exchanger Air To Air Sensible And Latent 2",
 "Node 21",
 "Heat Exchanger Air To Air Sensible And Latent 3",
 "Node 16"]
```

## Possible Solution



## Details

### Environment

Some additional details about your environment for this issue (if relevant):
 - Platform (Operating system, version): All
 - Version of OpenStudio (if using an intermediate build, include SHA): 3.6.0-rc2

### Context

Initially reported at https://github.com/openstudiocoalition/OpenStudioApplication/issues/600

AirLoopHVAC::clone will duplicate components that are on the relief and intake air streams of an OutdoorAirSystem
Issue overview
--------------

## Current Behavior

Cloning an airloop with air-to-air heat exchanger doubles the number of heat exchangers:
![2023-04-13](https://user-images.githubusercontent.com/69771412/231789411-8e74088b-856e-4c49-8c39-8ea3f452fffa.png)

## Expected Behavior

Shouldn't be cloned twice.

## Steps to Reproduce

```ruby
m = Model.new
a = AirLoopHVAC.new(m)
controller = OpenStudio::Model::ControllerOutdoorAir.new(m)
oas = OpenStudio::Model::AirLoopHVACOutdoorAirSystem.new(m, controller)
oas.addToNode(a.supplyOutletNode)
erv = HeatExchangerAirToAirSensibleAndLatent.new(m)
erv.addToNode(oas.outboardOANode.get)

a.supplyComponents.map(&:nameString)
=> ["Node 1", "Air Loop HVAC Outdoor Air System 1", "Node 2"
oas.components.map(&:nameString)
=> ["Node 6", "Heat Exchanger Air To Air Sensible And Latent 1", "Node 9", "Node 10", "Heat Exchanger Air To Air Sensible And Latent 1", "Node 7"]

a2 = a.clone(m).to_AirLoopHVAC.get

a2.supplyComponents.map(&:nameString)
=> ["Node 8", "Air Loop HVAC Outdoor Air System 2", "Node 11"]

oas2 = a2.airLoopHVACOutdoorAirSystem.get
oas2.components.map(&:nameString)
=> ["Node 15",
 "Heat Exchanger Air To Air Sensible And Latent 3",
 "Node 20",
 "Heat Exchanger Air To Air Sensible And Latent 2",
 "Node 18",
 "Node 19",
 "Heat Exchanger Air To Air Sensible And Latent 2",
 "Node 21",
 "Heat Exchanger Air To Air Sensible And Latent 3",
 "Node 16"]
```

## Possible Solution



## Details

### Environment

Some additional details about your environment for this issue (if relevant):
 - Platform (Operating system, version): All
 - Version of OpenStudio (if using an intermediate build, include SHA): 3.6.0-rc2

### Context

Initially reported at https://github.com/openstudiocoalition/OpenStudioApplication/issues/600

AirLoopHVAC::clone will duplicate components that are on the relief and intake air streams of an OutdoorAirSystem
Issue overview
--------------

## Current Behavior

Cloning an airloop with air-to-air heat exchanger doubles the number of heat exchangers:
![2023-04-13](https://user-images.githubusercontent.com/69771412/231789411-8e74088b-856e-4c49-8c39-8ea3f452fffa.png)

## Expected Behavior

Shouldn't be cloned twice.

## Steps to Reproduce

```ruby
m = Model.new
a = AirLoopHVAC.new(m)
controller = OpenStudio::Model::ControllerOutdoorAir.new(m)
oas = OpenStudio::Model::AirLoopHVACOutdoorAirSystem.new(m, controller)
oas.addToNode(a.supplyOutletNode)
erv = HeatExchangerAirToAirSensibleAndLatent.new(m)
erv.addToNode(oas.outboardOANode.get)

a.supplyComponents.map(&:nameString)
=> ["Node 1", "Air Loop HVAC Outdoor Air System 1", "Node 2"
oas.components.map(&:nameString)
=> ["Node 6", "Heat Exchanger Air To Air Sensible And Latent 1", "Node 9", "Node 10", "Heat Exchanger Air To Air Sensible And Latent 1", "Node 7"]

a2 = a.clone(m).to_AirLoopHVAC.get

a2.supplyComponents.map(&:nameString)
=> ["Node 8", "Air Loop HVAC Outdoor Air System 2", "Node 11"]

oas2 = a2.airLoopHVACOutdoorAirSystem.get
oas2.components.map(&:nameString)
=> ["Node 15",
 "Heat Exchanger Air To Air Sensible And Latent 3",
 "Node 20",
 "Heat Exchanger Air To Air Sensible And Latent 2",
 "Node 18",
 "Node 19",
 "Heat Exchanger Air To Air Sensible And Latent 2",
 "Node 21",
 "Heat Exchanger Air To Air Sensible And Latent 3",
 "Node 16"]
```

## Possible Solution



## Details

### Environment

Some additional details about your environment for this issue (if relevant):
 - Platform (Operating system, version): All
 - Version of OpenStudio (if using an intermediate build, include SHA): 3.6.0-rc2

### Context

Initially reported at https://github.com/openstudiocoalition/OpenStudioApplication/issues/600



Discussion / hints:
The issue is with the AirLoopHVACOutdoorAirSystem::clone, and I'm not surprised we didn't catch it given the "clone" test here which does essentially nothing: https://github.com/NREL/OpenStudio/blob/313d435e996b1e83ac9facbc7d640dc08a4fdcbb/src/model/test/AirLoopHVACOutdoorAirSystem_GTest.cpp#L67-L80
That is one OLD file... time to modernize and avoid optional casting an object that's already the right type... And this shoudl be AirLoopHVAC as an override from HVACComponent

https://github.com/NREL/OpenStudio/blob/313d435e996b1e83ac9facbc7d640dc08a4fdcbb/src/model/AirLoopHVACOutdoorAirSystem.cpp#L395-L411
The issue is with the AirLoopHVACOutdoorAirSystem::clone, and I'm not surprised we didn't catch it given the "clone" test here which does essentially nothing: https://github.com/NREL/OpenStudio/blob/313d435e996b1e83ac9facbc7d640dc08a4fdcbb/src/model/test/AirLoopHVACOutdoorAirSystem_GTest.cpp#L67-L80
That is one OLD file... time to modernize and avoid optional casting an object that's already the right type... And this shoudl be AirLoopHVAC as an override from HVACComponent

https://github.com/NREL/OpenStudio/blob/313d435e996b1e83ac9facbc7d640dc08a4fdcbb/src/model/AirLoopHVACOutdoorAirSystem.cpp#L395-L411
The issue is with the AirLoopHVACOutdoorAirSystem::clone, and I'm not surprised we didn't catch it given the "clone" test here which does essentially nothing: https://github.com/NREL/OpenStudio/blob/313d435e996b1e83ac9facbc7d640dc08a4fdcbb/src/model/test/AirLoopHVACOutdoorAirSystem_GTest.cpp#L67-L80
That is one OLD file... time to modernize and avoid optional casting an object that's already the right type... And this shoudl be AirLoopHVAC as an override from HVACComponent

https://github.com/NREL/OpenStudio/blob/313d435e996b1e83ac9facbc7d640dc08a4fdcbb/src/model/AirLoopHVACOutdoorAirSystem.cpp#L395-L411

Fix patch:
diff --git a/src/model/AirLoopHVAC.cpp b/src/model/AirLoopHVAC.cpp
index 397a2ee9a4d..708f664b60e 100644
--- a/src/model/AirLoopHVAC.cpp
+++ b/src/model/AirLoopHVAC.cpp
@@ -263,9 +263,7 @@ namespace model {
       ModelObjectVector modelObjects;
       modelObjects = supplyComponents(openstudio::IddObjectType::OS_AirLoopHVAC_OutdoorAirSystem);
       if (modelObjects.size() == 1) {
-        if (OptionalAirLoopHVACOutdoorAirSystem oaSystem = modelObjects.front().optionalCast<AirLoopHVACOutdoorAirSystem>()) {
-          result = oaSystem;
-        }
+        return modelObjects.front().optionalCast<AirLoopHVACOutdoorAirSystem>();
       }
 
       return result;
@@ -900,9 +898,9 @@ namespace model {
       return {demandComps.begin(), end};
     }
 
-    std::vector<ModelObject> AirLoopHVAC_Impl::oaComponents(openstudio::IddObjectType /*type*/) const {
+    std::vector<ModelObject> AirLoopHVAC_Impl::oaComponents(openstudio::IddObjectType type) const {
       if (airLoopHVACOutdoorAirSystem()) {
-        return airLoopHVACOutdoorAirSystem()->components();
+        return airLoopHVACOutdoorAirSystem()->components(type);
       } else {
         return {};
       }
diff --git a/src/model/AirLoopHVACOutdoorAirSystem.cpp b/src/model/AirLoopHVACOutdoorAirSystem.cpp
index e47a6af07ed..6651285f7a7 100644
--- a/src/model/AirLoopHVACOutdoorAirSystem.cpp
+++ b/src/model/AirLoopHVACOutdoorAirSystem.cpp
@@ -53,6 +53,7 @@
 
 #include "../utilities/core/Assert.hpp"
 #include "../utilities/core/Compare.hpp"
+#include "../utilities/core/ContainersMove.hpp"
 #include "../utilities/data/DataEnums.hpp"
 #include "../utilities/idf/IdfExtensibleGroup.hpp"
 
@@ -61,6 +62,7 @@
 #include <utilities/idd/OS_Controller_OutdoorAir_FieldEnums.hxx>
 #include <utilities/idd/IddEnums.hxx>
 
+#include <algorithm>
 #include <utility>
 
 namespace openstudio {
@@ -69,6 +71,20 @@ namespace model {
 
   namespace detail {
 
+    // Helpers
+    std::pair<bool, boost::optional<ModelObject>> getOutletNodeForComponent(const ModelObject& modelObject) {
+      if (auto comp_ = modelObject.optionalCast<StraightComponent>()) {
+        return {true, comp_->outletModelObject()};
+      } else if (auto comp_ = modelObject.optionalCast<AirToAirComponent>()) {
+        return {true, comp_->secondaryAirOutletModelObject()};
+      } else if (auto comp_ = modelObject.optionalCast<WaterToAirComponent>()) {
+        return {true, comp_->airOutletModelObject()};
+      } else if (auto comp_ = modelObject.optionalCast<ZoneHVACComponent>()) {
+        return {true, comp_->airOutletModelObject()};
+      }
+      return {false, boost::none};
+    }
+
     AirLoopHVACOutdoorAirSystem_Impl::AirLoopHVACOutdoorAirSystem_Impl(const IdfObject& idfObject, Model_Impl* model, bool keepHandle)
       : HVACComponent_Impl(idfObject, model, keepHandle) {
       OS_ASSERT(idfObject.iddObject().type() == AirLoopHVACOutdoorAirSystem::iddObjectType());
@@ -171,7 +187,7 @@ namespace model {
       model.connect(oaclone, oaclone.reliefAirPort(), reliefNodeClone, reliefNodeClone.inletPort());
 
       // Clone oa stream comps
-
+      // Note that in the case of AirToAirComponent, this also does stuff to the relief side!
       auto oaComps = oaComponents();
       std::reverse(oaComps.begin(), oaComps.end());
       std::vector<Node> oaNodes;
@@ -204,14 +220,47 @@ namespace model {
       // Clone relief stream comps
 
       auto reliefComps = reliefComponents();
+
+      auto reliefAirToAirComps = subsetCastVector<AirToAirComponent>(reliefComps);
+
       std::vector<Node> reliefNodes;
 
-      for (const auto& comp : reliefComps) {
-        if (comp.iddObjectType() == Node::iddObjectType()) {
-          reliefNodes.push_back(comp.cast<Node>());
-        } else {
-          auto compClone = comp.clone(model).cast<HVACComponent>();
-          compClone.addToNode(reliefNodeClone);
+      if (reliefAirToAirComps.empty()) {
+        // Business as usual, but this is faster this way as we keep the same node to connect to throughout
+        std::vector<Node> reliefNodes;
+
+        for (const auto& comp : reliefComps) {
+          if (comp.iddObjectType() == Node::iddObjectType()) {
+            reliefNodes.push_back(comp.cast<Node>());
+          } else {
+            auto compClone = comp.clone(model).cast<HVACComponent>();
+            compClone.addToNode(reliefNodeClone);
+          }
+        }
+      } else {
+        auto targetNode = oaclone.reliefAirModelObject()->cast<Node>();
+
+        for (const auto& comp : reliefComps) {
+          if (comp.iddObjectType() == Node::iddObjectType()) {
+            reliefNodes.push_back(comp.cast<Node>());
+          } else if (comp.optionalCast<AirToAirComponent>()) {
+            auto reliefCloneMo_ = targetNode.outletModelObject();
+            OS_ASSERT(reliefCloneMo_);
+            auto reliefCloneAirToAir_ = reliefCloneMo_->optionalCast<AirToAirComponent>();
+            OS_ASSERT(reliefCloneAirToAir_);
+            auto mo_ = reliefCloneAirToAir_->secondaryAirOutletModelObject();
+            OS_ASSERT(mo_);
+            targetNode = mo_->cast<Node>();
+          } else {
+            auto compClone = comp.clone(model).cast<HVACComponent>();
+            compClone.addToNode(targetNode);
+            auto [ok, mo_] = getOutletNodeForComponent(compClone);
+            if (!ok) {
+              LOG_AND_THROW("For " << briefDescription() << ", unexpected reliefComponent found: " << compClone.briefDescription());
+            }
+            OS_ASSERT(mo_);
+            targetNode = mo_->cast<Node>();
+          }
         }
       }
 
@@ -253,7 +302,7 @@ namespace model {
       model().disconnect(getObject<ModelObject>(), outdoorAirPort());
       model().disconnect(getObject<ModelObject>(), reliefAirPort());
 
-      if (this->airLoopHVAC()) {
+      if (auto airLoop_ = this->airLoopHVAC()) {
         OptionalNode targetModelObject;
         OptionalNode sourceModelObject;
         targetModelObject = this->mixedAirModelObject()->optionalCast<Node>();
@@ -266,9 +315,8 @@ namespace model {
         OptionalUnsigned target2Port = targetModelObject->connectedObjectPort(targetModelObject->outletPort());
         OptionalUnsigned source2Port = sourceModelObject->connectedObjectPort(sourceModelObject->inletPort());
 
-        OptionalAirLoopHVAC airLoop = this->airLoop();
-        OptionalNode supplyInletNode = airLoop->supplyInletNode();
-        OptionalNode supplyOutletNode = OptionalNode(airLoop->supplyOutletNodes().front());
+        OptionalNode supplyInletNode = airLoop_->supplyInletNode();
+        OptionalNode supplyOutletNode = OptionalNode(airLoop_->supplyOutletNodes().front());
 
         model().disconnect(getObject<ModelObject>(), returnAirPort());
         model().disconnect(getObject<ModelObject>(), mixedAirPort());
@@ -292,7 +340,7 @@ namespace model {
 
       getControllerOutdoorAir().remove();
 
-      return ModelObject_Impl::remove();
+      return ModelObject_Impl::remove();  // NOLINT(bugprone-parent-virtual-call)
     }
 
     std::vector<HVACComponent> AirLoopHVACOutdoorAirSystem_Impl::edges(const boost::optional<HVACComponent>& /*prev*/) {
@@ -305,268 +353,231 @@ namespace model {
       return edges;
     }
 
-    std::vector<ModelObject> AirLoopHVACOutdoorAirSystem_Impl::oaComponents() const {
+    std::vector<ModelObject> AirLoopHVACOutdoorAirSystem_Impl::oaComponents(openstudio::IddObjectType type) const {
       std::vector<ModelObject> modelObjects;
+      modelObjects.reserve(10);  // Abritrary, but it wouldn't be common to have more objects than that
 
-      OptionalModelObject modelObject;
-
-      modelObject = this->outdoorAirModelObject();
+      OptionalModelObject modelObject = this->outdoorAirModelObject();
 
       while (modelObject) {
-        if (OptionalStraightComponent comp = modelObject->optionalCast<StraightComponent>()) {
-          modelObjects.insert(modelObjects.begin(), *comp);
-          modelObject = comp->inletModelObject();
-        } else if (boost::optional<AirToAirComponent> comp = modelObject->optionalCast<AirToAirComponent>()) {
-          modelObjects.insert(modelObjects.begin(), *comp);
-          modelObject = comp->primaryAirInletModelObject();
-        } else if (boost::optional<WaterToAirComponent> comp = modelObject->optionalCast<WaterToAirComponent>()) {
-          modelObjects.insert(modelObjects.begin(), *comp);
-          modelObject = comp->airInletModelObject();
-        } else if (auto comp = modelObject->optionalCast<ZoneHVACComponent>()) {
+        if (auto comp_ = modelObject->optionalCast<StraightComponent>()) {
+          modelObject = comp_->inletModelObject();
+          modelObjects.insert(modelObjects.begin(), std::move(*comp_));
+        } else if (auto comp_ = modelObject->optionalCast<AirToAirComponent>()) {
+          modelObject = comp_->primaryAirInletModelObject();
+          modelObjects.insert(modelObjects.begin(), std::move(*comp_));
+        } else if (auto comp_ = modelObject->optionalCast<WaterToAirComponent>()) {
+          modelObject = comp_->airInletModelObject();
+          modelObjects.insert(modelObjects.begin(), std::move(*comp_));
+        } else if (auto comp_ = modelObject->optionalCast<ZoneHVACComponent>()) {
           // For AirLoopHVACUnitarySystem and ZoneHVACTerminalUnitVariableRefrigerantFlow
-          modelObjects.insert(modelObjects.begin(), *comp);
-          modelObject = comp->inletNode();
+          modelObject = comp_->inletNode();
+          modelObjects.insert(modelObjects.begin(), std::move(*comp_));
         } else {
-          break;
-          // log unhandled component
+          LOG_AND_THROW("For " << briefDescription() << ", unexpected oaComponent found: " << modelObject->briefDescription());
         }
       }
+      if (type != IddObjectType::Catchall) {
+        modelObjects.erase(
+          std::remove_if(modelObjects.begin(), modelObjects.end(), [&](const auto& mo) -> bool { return mo.iddObjectType() != type; }),
+          modelObjects.end());
+      }
       return modelObjects;
     }
 
-    std::vector<ModelObject> AirLoopHVACOutdoorAirSystem_Impl::reliefComponents() const {
+    std::vector<ModelObject> AirLoopHVACOutdoorAirSystem_Impl::reliefComponents(openstudio::IddObjectType type) const {
       std::vector<ModelObject> modelObjects;
+      modelObjects.reserve(10);  // Abritrary, but it wouldn't be common to have more objects than that
 
-      OptionalModelObject modelObject;
-
-      modelObject = this->reliefAirModelObject();
+      OptionalModelObject modelObject = this->reliefAirModelObject();
 
       while (modelObject) {
-        OptionalNode node = modelObject->optionalCast<Node>();
-        OptionalStraightComponent comp = modelObject->optionalCast<StraightComponent>();
-        if (node) {
-          modelObjects.push_back(*node);
-          modelObject = node->outletModelObject();
-        } else if (comp) {
-          modelObjects.push_back(*comp);
-          modelObject = comp->outletModelObject();
-        } else if (boost::optional<AirToAirComponent> comp = modelObject->optionalCast<AirToAirComponent>()) {
-          modelObjects.push_back(*comp);
-          modelObject = comp->secondaryAirOutletModelObject();
-        } else if (boost::optional<WaterToAirComponent> comp = modelObject->optionalCast<WaterToAirComponent>()) {
-          modelObjects.push_back(*comp);
-          modelObject = comp->airOutletModelObject();
-        } else if (auto comp = modelObject->optionalCast<ZoneHVACComponent>()) {
-          modelObjects.push_back(*comp);
-          modelObject = comp->outletNode();
-        } else {
-          // log unhandled component
+        auto [ok, mo_] = getOutletNodeForComponent(*modelObject);
+        if (!ok) {
+          LOG_AND_THROW("For " << briefDescription() << ", unexpected reliefComponent found: " << modelObject->briefDescription());
         }
+        modelObjects.emplace_back(std::move(*modelObject));
+        modelObject = mo_;
+      }
+
+      if (type != IddObjectType::Catchall) {
+        modelObjects.erase(
+          std::remove_if(modelObjects.begin(), modelObjects.end(), [&](const auto& mo) -> bool { return mo.iddObjectType() != type; }),
+          modelObjects.end());
       }
       return modelObjects;
     }
-    boost::optional<Node> AirLoopHVACOutdoorAirSystem_Impl::outboardOANode() const {
-      OptionalNode result;
 
-      ModelObjectVector modelObjects;
-      modelObjects = this->oaComponents();
+    boost::optional<Node> AirLoopHVACOutdoorAirSystem_Impl::outboardOANode() const {
+      auto modelObjects = this->oaComponents();
 
       if (!modelObjects.empty()) {
-        ModelObject modelObject = modelObjects.front();
-        result = modelObject.optionalCast<Node>();
+        return modelObjects.front().optionalCast<Node>();
       }
 
-      return result;
+      return boost::none;
     }
-    boost::optional<Node> AirLoopHVACOutdoorAirSystem_Impl::outboardReliefNode() const {
-      OptionalNode result;
 
-      ModelObjectVector modelObjects;
-      modelObjects = this->reliefComponents();
+    boost::optional<Node> AirLoopHVACOutdoorAirSystem_Impl::outboardReliefNode() const {
+      auto modelObjects = this->reliefComponents();
 
       if (!modelObjects.empty()) {
-        ModelObject modelObject = modelObjects.back();
-        result = modelObject.optionalCast<Node>();
+        return modelObjects.back().optionalCast<Node>();
       }
 
-      return result;
+      return boost::none;
     }
 
-    OptionalAirLoopHVAC AirLoopHVACOutdoorAirSystem_Impl::airLoop() const {
-      OptionalAirLoopHVAC result;
+    boost::optional<AirLoopHVAC> AirLoopHVACOutdoorAirSystem_Impl::airLoopHVAC() const {
 
-      AirLoopHVACVector airLoops = this->model().getConcreteModelObjects<AirLoopHVAC>();
-      AirLoopHVACVector::iterator it;
+      if (m_airLoopHVAC) {  // HVACComponent_Impl caches
+        return m_airLoopHVAC;
+      }
 
-      for (it = airLoops.begin(); it != airLoops.end(); ++it) {
-        OptionalAirLoopHVAC airLoop = it->optionalCast<AirLoopHVAC>();
-        if (airLoop) {
-          if (airLoop->component(this->handle())) {
-            return airLoop;
-          }
+      for (auto& airLoop : this->model().getConcreteModelObjects<AirLoopHVAC>()) {
+        if (airLoop.supplyComponent(this->handle())) {
+          m_airLoopHVAC = airLoop;
+          return airLoop;
         }
       }
 
-      return result;
+      return boost::none;
     }
 
-    std::vector<ModelObject> AirLoopHVACOutdoorAirSystem_Impl::components() const {
-      std::vector<ModelObject> result;
-      result = this->oaComponents();
+    std::vector<ModelObject> AirLoopHVACOutdoorAirSystem_Impl::components(openstudio::IddObjectType type) const {
+      return openstudio::concat<ModelObject>(this->oaComponents(type), this->reliefComponents(type));
+    }
 
-      std::vector<ModelObject> reliefComponents = this->reliefComponents();
+    boost::optional<ModelObject> AirLoopHVACOutdoorAirSystem_Impl::component(openstudio::Handle handle) const {
+      auto comps = components();
 
-      result.insert(result.end(), reliefComponents.begin(), reliefComponents.end());
+      auto it = std::find_if(comps.begin(), comps.end(), [&](const ModelObject& comp) { return (comp.handle() == handle); });
+      if (it != comps.end()) {
+        return *it;
+      }
 
-      return result;
+      return boost::none;
     }
 
-    boost::optional<ModelObject> AirLoopHVACOutdoorAirSystem_Impl::component(openstudio::Handle handle) {
-      OptionalModelObject result;
+    boost::optional<ModelObject> AirLoopHVACOutdoorAirSystem_Impl::oaComponent(openstudio::Handle handle) const {
+      auto comps = oaComponents();
 
-      ModelObjectVector allComponents = components();
-      ModelObjectVector::iterator it;
-      for (it = allComponents.begin(); it != allComponents.end(); ++it) {
-        if (it->handle() == handle) {
-          return OptionalModelObject(*it);
-        }
+      auto it = std::find_if(comps.begin(), comps.end(), [&](const ModelObject& comp) { return (comp.handle() == handle); });
+      if (it != comps.end()) {
+        return *it;
       }
-      return result;
+
+      return boost::none;
     }
 
-    boost::optional<ModelObject> AirLoopHVACOutdoorAirSystem_Impl::oaComponent(openstudio::Handle handle) {
-      OptionalModelObject result;
+    boost::optional<ModelObject> AirLoopHVACOutdoorAirSystem_Impl::reliefComponent(openstudio::Handle handle) const {
+      auto comps = reliefComponents();
 
-      ModelObjectVector allComponents = oaComponents();
-      ModelObjectVector::iterator it;
-      for (it = allComponents.begin(); it != allComponents.end(); ++it) {
-        if (it->handle() == handle) {
-          return OptionalModelObject(*it);
-        }
+      auto it = std::find_if(comps.begin(), comps.end(), [&](const ModelObject& comp) { return (comp.handle() == handle); });
+      if (it != comps.end()) {
+        return *it;
       }
-      return result;
+
+      return boost::none;
     }
 
-    boost::optional<ModelObject> AirLoopHVACOutdoorAirSystem_Impl::reliefComponent(openstudio::Handle handle) {
-      OptionalModelObject result;
+    bool AirLoopHVACOutdoorAirSystem_Impl::addToNode(Node& node) {
 
-      ModelObjectVector allComponents = reliefComponents();
-      ModelObjectVector::iterator it;
-      for (it = allComponents.begin(); it != allComponents.end(); ++it) {
-        if (it->handle() == handle) {
-          return OptionalModelObject(*it);
-        }
+      auto airLoop_ = node.airLoopHVAC();
+
+      if (!airLoop_) {
+        return false;
+      }
+
+      if (!airLoop_->supplyComponents(this->iddObjectType()).empty()) {
+        LOG(Warn, "AirLoopHVAC " << airLoop_->nameString() << " already has an AirLoopHVACOutdoorAirSystem assigned.");
+        return false;
+      }
+
+      if (!airLoop_->supplyComponent(node.handle())) {
+        return false;
       }
-      return result;
-    }
 
-    bool AirLoopHVACOutdoorAirSystem_Impl::addToNode(Node& node) {
       Model _model = node.model();
       auto thisModelObject = getObject<ModelObject>();
 
-      if (OptionalAirLoopHVAC optionalAirLoop = node.airLoopHVAC()) {
-        AirLoopHVAC airLoop = optionalAirLoop.get();
+      if (node == airLoop_->supplyOutletNodes().front() && node.inletModelObject().get() == airLoop_->supplyInletNode()) {
+        const unsigned oldOutletPort = node.connectedObjectPort(node.inletPort()).get();
+        const unsigned oldInletPort = node.inletPort();
+        ModelObject oldSourceModelObject = node.connectedObject(node.inletPort()).get();
+        ModelObject oldTargetModelObject = node;
 
-        if (!airLoop.supplyComponents(this->iddObjectType()).empty()) {
-          return false;
-        }
+        _model.connect(std::move(oldSourceModelObject), oldOutletPort, thisModelObject, returnAirPort());
+        _model.connect(std::move(thisModelObject), mixedAirPort(), std::move(oldTargetModelObject), oldInletPort);
+        return true;
+      }
 
-        if (airLoop.supplyComponent(node.handle())) {
-          if (node == airLoop.supplyOutletNodes().front() && node.inletModelObject().get() == airLoop.supplyInletNode()) {
-            unsigned oldOutletPort = node.connectedObjectPort(node.inletPort()).get();
-            unsigned oldInletPort = node.inletPort();
-            ModelObject oldSourceModelObject = node.connectedObject(node.inletPort()).get();
-            ModelObject oldTargetModelObject = node;
+      if (node == airLoop_->supplyInletNode() && node.outletModelObject().get() == airLoop_->supplyOutletNodes().front()) {
+        const unsigned oldOutletPort = node.outletPort();
+        const unsigned oldInletPort = node.connectedObjectPort(node.outletPort()).get();
+        ModelObject oldSourceModelObject = node;
+        ModelObject oldTargetModelObject = node.outletModelObject().get();
 
-            _model.connect(oldSourceModelObject, oldOutletPort, thisModelObject, returnAirPort());
-            _model.connect(thisModelObject, mixedAirPort(), oldTargetModelObject, oldInletPort);
-            return true;
-          }
-          if (node == airLoop.supplyInletNode() && node.outletModelObject().get() == airLoop.supplyOutletNodes().front()) {
-            unsigned oldOutletPort = node.outletPort();
-            unsigned oldInletPort = node.connectedObjectPort(node.outletPort()).get();
-            ModelObject oldSourceModelObject = node;
-            ModelObject oldTargetModelObject = node.outletModelObject().get();
-
-            _model.connect(oldSourceModelObject, oldOutletPort, thisModelObject, returnAirPort());
-            _model.connect(thisModelObject, mixedAirPort(), oldTargetModelObject, oldInletPort);
-            return true;
-          } else if (node == airLoop.supplyOutletNodes().front()) {
-            unsigned oldOutletPort = node.connectedObjectPort(node.inletPort()).get();
-            unsigned oldInletPort = node.inletPort();
-            ModelObject oldSourceModelObject = node.connectedObject(node.inletPort()).get();
-            ModelObject oldTargetModelObject = node;
-
-            Node newNode(_model);
-            _model.connect(oldSourceModelObject, oldOutletPort, newNode, newNode.inletPort());
-            _model.connect(newNode, newNode.outletPort(), thisModelObject, returnAirPort());
-            _model.connect(thisModelObject, mixedAirPort(), oldTargetModelObject, oldInletPort);
-            return true;
-          } else {
-            unsigned oldOutletPort = node.outletPort();
-            unsigned oldInletPort = node.connectedObjectPort(node.outletPort()).get();
-            ModelObject oldSourceModelObject = node;
-            ModelObject oldTargetModelObject = node.connectedObject(node.outletPort()).get();
-
-            Node newNode(_model);
-            _model.connect(oldSourceModelObject, oldOutletPort, thisModelObject, returnAirPort());
-            _model.connect(thisModelObject, mixedAirPort(), newNode, newNode.inletPort());
-            _model.connect(newNode, newNode.outletPort(), oldTargetModelObject, oldInletPort);
-            return true;
-          }
-        } else {
-          return false;
-        }
-      } else {
-        return false;
+        _model.connect(std::move(oldSourceModelObject), oldOutletPort, thisModelObject, returnAirPort());
+        _model.connect(std::move(thisModelObject), mixedAirPort(), std::move(oldTargetModelObject), oldInletPort);
+        return true;
       }
-    }
 
-    boost::optional<ModelObject> AirLoopHVACOutdoorAirSystem_Impl::controllerOutdoorAirAsModelObject() const {
-      OptionalModelObject result = getControllerOutdoorAir();
-      return result;
-    }
+      if (node == airLoop_->supplyOutletNodes().front()) {
+        const unsigned oldOutletPort = node.connectedObjectPort(node.inletPort()).get();
+        const unsigned oldInletPort = node.inletPort();
+        ModelObject oldSourceModelObject = node.connectedObject(node.inletPort()).get();
+        ModelObject oldTargetModelObject = node;
+
+        Node newNode(_model);
+        const unsigned newNodeInletPort = newNode.inletPort();
+        const unsigned newNodeOutletPort = newNode.outletPort();
+        _model.connect(std::move(oldSourceModelObject), oldOutletPort, newNode, newNodeInletPort);
+        _model.connect(std::move(newNode), newNodeOutletPort, thisModelObject, returnAirPort());
+        _model.connect(std::move(thisModelObject), mixedAirPort(), std::move(oldTargetModelObject), oldInletPort);
+        return true;
+      }
 
-    std::vector<ModelObject> AirLoopHVACOutdoorAirSystem_Impl::oaComponentsAsModelObjects() const {
-      ModelObjectVector result = castVector<ModelObject>(oaComponents());
-      return result;
+      {
+        const unsigned oldOutletPort = node.outletPort();
+        const unsigned oldInletPort = node.connectedObjectPort(node.outletPort()).get();
+        ModelObject oldSourceModelObject = node;
+        ModelObject oldTargetModelObject = node.connectedObject(node.outletPort()).get();
+
+        Node newNode(_model);
+        const unsigned newNodeInletPort = newNode.inletPort();
+        const unsigned newNodeOutletPort = newNode.outletPort();
+        _model.connect(std::move(oldSourceModelObject), oldOutletPort, thisModelObject, returnAirPort());
+        _model.connect(std::move(thisModelObject), mixedAirPort(), newNode, newNodeInletPort);
+        _model.connect(std::move(newNode), newNodeOutletPort, std::move(oldTargetModelObject), oldInletPort);
+        return true;
+      }
     }
 
-    std::vector<ModelObject> AirLoopHVACOutdoorAirSystem_Impl::reliefComponentsAsModelObjects() const {
-      ModelObjectVector result = castVector<ModelObject>(reliefComponents());
+    boost::optional<ModelObject> AirLoopHVACOutdoorAirSystem_Impl::controllerOutdoorAirAsModelObject() const {
+      OptionalModelObject result = getControllerOutdoorAir();
       return result;
     }
 
     boost::optional<ModelObject> AirLoopHVACOutdoorAirSystem_Impl::outboardOANodeAsModelObject() const {
-      OptionalModelObject result;
-      OptionalNode intermediate = outboardOANode();
-      if (intermediate) {
-        result = *intermediate;
+      if (auto mo_ = this->outboardOANode()) {
+        return std::move(*mo_);
       }
-      return result;
+      return boost::none;
     }
 
     boost::optional<ModelObject> AirLoopHVACOutdoorAirSystem_Impl::outboardReliefNodeAsModelObject() const {
-      OptionalModelObject result;
-      OptionalNode intermediate = outboardReliefNode();
-      if (intermediate) {
-        result = *intermediate;
+      if (auto mo_ = this->outboardReliefNode()) {
+        return std::move(*mo_);
       }
-      return result;
+      return boost::none;
     }
 
     boost::optional<ModelObject> AirLoopHVACOutdoorAirSystem_Impl::airLoopAsModelObject() const {
-      OptionalModelObject result;
-      OptionalAirLoopHVAC intermediate = airLoop();
-      if (intermediate) {
-        result = *intermediate;
+      if (auto mo_ = this->airLoopHVAC()) {
+        return std::move(*mo_);
       }
-      return result;
-    }
-
-    std::vector<ModelObject> AirLoopHVACOutdoorAirSystem_Impl::componentsAsModelObjects() const {
-      ModelObjectVector result = castVector<ModelObject>(components());
-      return result;
+      return boost::none;
     }
 
     bool AirLoopHVACOutdoorAirSystem_Impl::setControllerOutdoorAirAsModelObject(const boost::optional<ModelObject>& modelObject) {
@@ -585,7 +596,7 @@ namespace model {
       if (opt) {
         return opt.get();
       }
-      return AirflowNetworkDistributionNode(model(), handle());
+      return {model(), handle()};
     }
 
     boost::optional<AirflowNetworkDistributionNode> AirLoopHVACOutdoorAirSystem_Impl::airflowNetworkDistributionNode() const {
@@ -677,17 +688,30 @@ namespace model {
     : HVACComponent(AirLoopHVACOutdoorAirSystem::iddObjectType(), model) {
     OS_ASSERT(getImpl<detail::AirLoopHVACOutdoorAirSystem_Impl>());
 
-    AirLoopHVACOutdoorAirSystem outdoorAirSystem = AirLoopHVACOutdoorAirSystem(getImpl<detail::AirLoopHVACOutdoorAirSystem_Impl>());
+    setControllerOutdoorAir(controller);
+
+    Node oaNode(model);
+    oaNode.setName("Outboard OA Node");
+    model.connect(oaNode, oaNode.outletPort(), *this, outdoorAirPort());
 
-    // Children.
+    Node reliefNode(model);
+    reliefNode.setName("Relief Node");
+    model.connect(*this, reliefAirPort(), reliefNode, reliefNode.inletPort());
+  }
+
+  AirLoopHVACOutdoorAirSystem::AirLoopHVACOutdoorAirSystem(Model& model) : HVACComponent(AirLoopHVACOutdoorAirSystem::iddObjectType(), model) {
+    OS_ASSERT(getImpl<detail::AirLoopHVACOutdoorAirSystem_Impl>());
 
+    ControllerOutdoorAir controller(model);
     setControllerOutdoorAir(controller);
 
     Node oaNode(model);
-    model.connect(oaNode, oaNode.outletPort(), outdoorAirSystem, outdoorAirPort());
+    oaNode.setName("Outboard OA Node");
+    model.connect(oaNode, oaNode.outletPort(), *this, outdoorAirPort());
 
     Node reliefNode(model);
-    model.connect(outdoorAirSystem, reliefAirPort(), reliefNode, reliefNode.inletPort());
+    reliefNode.setName("Relief Node");
+    model.connect(*this, reliefAirPort(), reliefNode, reliefNode.inletPort());
   }
 
   AirLoopHVACOutdoorAirSystem::AirLoopHVACOutdoorAirSystem(std::shared_ptr<detail::AirLoopHVACOutdoorAirSystem_Impl> impl)
@@ -733,14 +757,6 @@ namespace model {
     return getImpl<detail::AirLoopHVACOutdoorAirSystem_Impl>()->setControllerOutdoorAir(controllerOutdoorAir);
   }
 
-  ModelObject AirLoopHVACOutdoorAirSystem::clone(Model model) const {
-    return getImpl<detail::AirLoopHVACOutdoorAirSystem_Impl>()->clone(model);
-  }
-
-  std::vector<IdfObject> AirLoopHVACOutdoorAirSystem::remove() {
-    return getImpl<detail::AirLoopHVACOutdoorAirSystem_Impl>()->remove();
-  }
-
   boost::optional<Node> AirLoopHVACOutdoorAirSystem::outboardOANode() const {
     return getImpl<detail::AirLoopHVACOutdoorAirSystem_Impl>()->outboardOANode();
   }
@@ -749,41 +765,36 @@ namespace model {
     return getImpl<detail::AirLoopHVACOutdoorAirSystem_Impl>()->outboardReliefNode();
   }
 
-  std::vector<ModelObject> AirLoopHVACOutdoorAirSystem::oaComponents() const {
-    return getImpl<detail::AirLoopHVACOutdoorAirSystem_Impl>()->oaComponents();
+  std::vector<ModelObject> AirLoopHVACOutdoorAirSystem::oaComponents(openstudio::IddObjectType type) const {
+    return getImpl<detail::AirLoopHVACOutdoorAirSystem_Impl>()->oaComponents(type);
   }
 
-  std::vector<ModelObject> AirLoopHVACOutdoorAirSystem::reliefComponents() const {
-    return getImpl<detail::AirLoopHVACOutdoorAirSystem_Impl>()->reliefComponents();
+  std::vector<ModelObject> AirLoopHVACOutdoorAirSystem::reliefComponents(openstudio::IddObjectType type) const {
+    return getImpl<detail::AirLoopHVACOutdoorAirSystem_Impl>()->reliefComponents(type);
   }
 
-  std::vector<ModelObject> AirLoopHVACOutdoorAirSystem::components() const {
-    return getImpl<detail::AirLoopHVACOutdoorAirSystem_Impl>()->components();
+  std::vector<ModelObject> AirLoopHVACOutdoorAirSystem::components(openstudio::IddObjectType type) const {
+    return getImpl<detail::AirLoopHVACOutdoorAirSystem_Impl>()->components(type);
   }
 
-  boost::optional<ModelObject> AirLoopHVACOutdoorAirSystem::component(openstudio::Handle handle) {
+  boost::optional<ModelObject> AirLoopHVACOutdoorAirSystem::component(openstudio::Handle handle) const {
     return getImpl<detail::AirLoopHVACOutdoorAirSystem_Impl>()->component(handle);
   }
 
-  boost::optional<ModelObject> AirLoopHVACOutdoorAirSystem::oaComponent(openstudio::Handle handle) {
+  boost::optional<ModelObject> AirLoopHVACOutdoorAirSystem::oaComponent(openstudio::Handle handle) const {
     return getImpl<detail::AirLoopHVACOutdoorAirSystem_Impl>()->oaComponent(handle);
   }
 
-  boost::optional<ModelObject> AirLoopHVACOutdoorAirSystem::reliefComponent(openstudio::Handle handle) {
+  boost::optional<ModelObject> AirLoopHVACOutdoorAirSystem::reliefComponent(openstudio::Handle handle) const {
     return getImpl<detail::AirLoopHVACOutdoorAirSystem_Impl>()->reliefComponent(handle);
   }
 
   boost::optional<AirLoopHVAC> AirLoopHVACOutdoorAirSystem::airLoop() const {
-    return getImpl<detail::AirLoopHVACOutdoorAirSystem_Impl>()->airLoop();
-  }
-
-  bool AirLoopHVACOutdoorAirSystem::addToNode(Node& node) {
-    return getImpl<detail::AirLoopHVACOutdoorAirSystem_Impl>()->addToNode(node);
+    return getImpl<detail::AirLoopHVACOutdoorAirSystem_Impl>()->airLoopHVAC();
   }
 
   IddObjectType AirLoopHVACOutdoorAirSystem::iddObjectType() {
-    IddObjectType result(IddObjectType::OS_AirLoopHVAC_OutdoorAirSystem);
-    return result;
+    return {IddObjectType::OS_AirLoopHVAC_OutdoorAirSystem};
   }
 
   AirflowNetworkDistributionNode AirLoopHVACOutdoorAirSystem::getAirflowNetworkDistributionNode() {
diff --git a/src/model/AirLoopHVACOutdoorAirSystem.hpp b/src/model/AirLoopHVACOutdoorAirSystem.hpp
index b46449f9bd7..196f00d8b73 100644
--- a/src/model/AirLoopHVACOutdoorAirSystem.hpp
+++ b/src/model/AirLoopHVACOutdoorAirSystem.hpp
@@ -63,6 +63,9 @@ namespace model {
    */
     explicit AirLoopHVACOutdoorAirSystem(Model& model, const ControllerOutdoorAir& controller);
 
+    /** A default ControllerOutdoorAir will be created for you */
+    explicit AirLoopHVACOutdoorAirSystem(Model& model);
+
     virtual ~AirLoopHVACOutdoorAirSystem() = default;
     // Default the copy and move operators because the virtual dtor is explicit
     AirLoopHVACOutdoorAirSystem(const AirLoopHVACOutdoorAirSystem& other) = default;
@@ -93,53 +96,53 @@ namespace model {
     /** Returns the optional ModelObject attached to the return air port. **/
     boost::optional<ModelObject> returnAirModelObject();
 
-    /** Returns the optional ModelObject attached to the outdoor air port. **/
+    /** Returns the optional ModelObject attached to the outdoor air port.
+     *  This is NOT the same as the outboardOANode (unless there isn't nothing on the outdoor air stream)
+     *  It is the oa node closest to the OA system **/
     boost::optional<ModelObject> outdoorAirModelObject();
 
-    /** Returns the optional ModelObject attached to the relief air port. **/
+    /** Returns the optional ModelObject attached to the relief air port.
+     *  This is NOT the same as the outboardReliefNode (unless there isn't nothing on the relief stream)
+     *  It is the relief node closest to the OA system **/
     boost::optional<ModelObject> reliefAirModelObject();
 
     /** Returns the optional ModelObject attached to the mixer air port. **/
     boost::optional<ModelObject> mixedAirModelObject();
 
     /** Returns the most outboard outdoor air Node. **/
-    boost::optional<Node> outboardOANode() const;
+    boost::optional<Node> outboardOANode() const;  // TODO: shouldn't be optional
 
     /** Returns the most outboard relief air Node. **/
-    boost::optional<Node> outboardReliefNode() const;
+    boost::optional<Node> outboardReliefNode() const;  // TODO: shouldn't be optional
 
-    /** Returns a vector of model objects that are on the path of the incoming outdoor air stream. **/
-    std::vector<ModelObject> oaComponents() const;
+    /** Returns a vector of model objects that are on the path of the incoming outdoor air stream.
+     * This is orderded like the airflow: from the outdoorOANode (OA Intake) towards the OASystem itself **/
+    std::vector<ModelObject> oaComponents(openstudio::IddObjectType type = openstudio::IddObjectType("Catchall")) const;
 
-    /** Returns a vector of model objects that are on the path of the outgoing relief air stream. **/
-    std::vector<ModelObject> reliefComponents() const;
+    /** Returns a vector of model objects that are on the path of the outgoing relief air stream.
+      * This is orderded like the airflow: from the OASystem itself towards the outboardReliefNode (Relief to Outside) **/
+    std::vector<ModelObject> reliefComponents(openstudio::IddObjectType type = openstudio::IddObjectType("Catchall")) const;
 
     /** Returns a vector that is the concatenation of oaComponents() and reliefComponents(). **/
-    std::vector<ModelObject> components() const;
+    std::vector<ModelObject> components(openstudio::IddObjectType type = openstudio::IddObjectType("Catchall")) const;
 
     /** Returns the optional ModelObject with the Handle given.  The optional
    *  will be false if the given handle does not correspond to the a ModelObject
    *  that is not part of the outdoor air system.
    **/
-    boost::optional<ModelObject> component(openstudio::Handle handle);
+    boost::optional<ModelObject> component(openstudio::Handle handle) const;
 
     /** Returns the optional ModelObject with the Handle given.  The optional
    *  will be false if the given handle does not correspond to the a ModelObject
    *  that is not part of the supply side of the outdoor air system.
    **/
-    boost::optional<ModelObject> oaComponent(openstudio::Handle handle);
+    boost::optional<ModelObject> oaComponent(openstudio::Handle handle) const;
 
     /** Returns the optional ModelObject with the Handle given.  The optional
    *  will be false if the given handle does not correspond to the a ModelObject
    *  that is not part of the supply side of the outdoor air system.
    **/
-    boost::optional<ModelObject> reliefComponent(openstudio::Handle handle);
-
-    virtual bool addToNode(Node& node);
-
-    virtual std::vector<openstudio::IdfObject> remove();
-
-    virtual ModelObject clone(Model model) const;
+    boost::optional<ModelObject> reliefComponent(openstudio::Handle handle) const;
 
     /** Returns the ControllerOutdoorAir object associated with the AirLoopHVACOutdoorAirSystem. **/
     ControllerOutdoorAir getControllerOutdoorAir() const;
@@ -148,7 +151,7 @@ namespace model {
     bool setControllerOutdoorAir(const ControllerOutdoorAir& controllerOutdoorAir);
 
     /** Reimplemented from HVACComponent. **/
-    boost::optional<AirLoopHVAC> airLoop() const;
+    boost::optional<AirLoopHVAC> airLoop() const;  // TODO: this shouldn't exist!!!
 
     AirflowNetworkDistributionNode getAirflowNetworkDistributionNode();
 
diff --git a/src/model/AirLoopHVACOutdoorAirSystem_Impl.hpp b/src/model/AirLoopHVACOutdoorAirSystem_Impl.hpp
index a42ac8d267e..518f14378d0 100644
--- a/src/model/AirLoopHVACOutdoorAirSystem_Impl.hpp
+++ b/src/model/AirLoopHVACOutdoorAirSystem_Impl.hpp
@@ -91,20 +91,23 @@ namespace model {
 
       virtual bool addToNode(Node& node) override;
 
+      // Take a shortcut to avoid also checking each AirLoopHVAC's OutdoorAirSystem's component like HVACComponent logically does
+      virtual boost::optional<AirLoopHVAC> airLoopHVAC() const override;
+
       virtual std::vector<HVACComponent> edges(const boost::optional<HVACComponent>& prev) override;
 
-      std::vector<ModelObject> oaComponents() const;
+      std::vector<ModelObject> oaComponents(openstudio::IddObjectType type = openstudio::IddObjectType("Catchall")) const;
+
+      std::vector<ModelObject> reliefComponents(openstudio::IddObjectType type = openstudio::IddObjectType("Catchall")) const;
 
-      std::vector<ModelObject> reliefComponents() const;
+      std::vector<ModelObject> components(openstudio::IddObjectType type = openstudio::IddObjectType("Catchall")) const;
 
       boost::optional<Node> outboardOANode() const;
       boost::optional<Node> outboardReliefNode() const;
 
-      boost::optional<AirLoopHVAC> airLoop() const;
-      std::vector<ModelObject> components() const;
-      boost::optional<ModelObject> component(openstudio::Handle handle);
-      boost::optional<ModelObject> oaComponent(openstudio::Handle handle);
-      boost::optional<ModelObject> reliefComponent(openstudio::Handle handle);
+      boost::optional<ModelObject> component(openstudio::Handle handle) const;
+      boost::optional<ModelObject> oaComponent(openstudio::Handle handle) const;
+      boost::optional<ModelObject> reliefComponent(openstudio::Handle handle) const;
 
       AirflowNetworkDistributionNode getAirflowNetworkDistributionNode();
 
@@ -121,12 +124,9 @@ namespace model {
       REGISTER_LOGGER("openstudio.model.AirLoopHVACOutdoorAirSystem");
 
       boost::optional<ModelObject> controllerOutdoorAirAsModelObject() const;
-      std::vector<ModelObject> oaComponentsAsModelObjects() const;
-      std::vector<ModelObject> reliefComponentsAsModelObjects() const;
       boost::optional<ModelObject> outboardOANodeAsModelObject() const;
       boost::optional<ModelObject> outboardReliefNodeAsModelObject() const;
       boost::optional<ModelObject> airLoopAsModelObject() const;
-      std::vector<ModelObject> componentsAsModelObjects() const;
 
       bool setControllerOutdoorAirAsModelObject(const boost::optional<ModelObject>& modelObject);
     };
diff --git a/src/model/Loop.cpp b/src/model/Loop.cpp
index 26be514d8c0..f7ba9194f8a 100644
--- a/src/model/Loop.cpp
+++ b/src/model/Loop.cpp
@@ -53,11 +53,15 @@
 #include "ConnectorSplitter_Impl.hpp"
 #include "Model.hpp"
 
-#include <utilities/idd/IddEnums.hxx>
-
 #include "../utilities/core/Assert.hpp"
+#include "../utilities/core/ContainersMove.hpp"
 #include "../utilities/data/DataEnums.hpp"
 
+#include <utilities/idd/IddEnums.hxx>
+
+#include <algorithm>
+#include <functional>
+
 namespace openstudio {
 
 namespace model {
@@ -254,21 +258,15 @@ namespace model {
       } else {
         findModelObjects(outletComp, visited, allPaths, true);
       }
-      std::vector<ModelObject> _demandComponents = std::vector<ModelObject>(allPaths.begin(), allPaths.end());
+      std::vector<ModelObject> modelObjects = std::vector<ModelObject>(allPaths.begin(), allPaths.end());
 
       // Filter modelObjects for type
-      if (type == IddObjectType::Catchall) {
-        return _demandComponents;
+      if (type != IddObjectType::Catchall) {
+        modelObjects.erase(
+          std::remove_if(modelObjects.begin(), modelObjects.end(), [&](const auto& mo) -> bool { return mo.iddObjectType() != type; }),
+          modelObjects.end());
       }
-      std::vector<ModelObject> reducedModelObjects;
-
-      for (const auto& demandComponent : _demandComponents) {
-        if (type == demandComponent.iddObject().type()) {
-          reducedModelObjects.push_back(demandComponent);
-        }
-      }
-
-      return reducedModelObjects;
+      return modelObjects;
     }
 
     template <typename T>
@@ -325,14 +323,7 @@ namespace model {
     }
 
     std::vector<ModelObject> Loop_Impl::components(openstudio::IddObjectType type) const {
-      std::vector<ModelObject> result;
-      result = this->supplyComponents(type);
-
-      std::vector<ModelObject> demandComponents = this->demandComponents(type);
-
-      result.insert(result.end(), demandComponents.begin(), demandComponents.end());
-
-      return result;
+      return openstudio::concat<ModelObject>(this->supplyComponents(type), this->demandComponents(type));
     }
 
     std::vector<ModelObject> Loop_Impl::supplyComponents(const HVACComponent& inletComp, const HVACComponent& outletComp,
@@ -346,21 +337,15 @@ namespace model {
       } else {
         findModelObjects(outletComp, visited, allPaths, false);
       }
-      std::vector<ModelObject> _supplyComponents = std::vector<ModelObject>(allPaths.begin(), allPaths.end());
+      std::vector<ModelObject> modelObjects = std::vector<ModelObject>(allPaths.begin(), allPaths.end());
 
       // Filter modelObjects for type
-      if (type == IddObjectType::Catchall) {
-        return _supplyComponents;
+      if (type != IddObjectType::Catchall) {
+        modelObjects.erase(
+          std::remove_if(modelObjects.begin(), modelObjects.end(), [&](const auto& mo) -> bool { return mo.iddObjectType() != type; }),
+          modelObjects.end());
       }
-      std::vector<ModelObject> reducedModelObjects;
-
-      for (const auto& supplyComponent : _supplyComponents) {
-        if (type == supplyComponent.iddObject().type()) {
-          reducedModelObjects.push_back(supplyComponent);
-        }
-      }
-
-      return reducedModelObjects;
+      return modelObjects;
     }
 
     std::vector<ModelObject> Loop_Impl::components(const HVACComponent& inletComp, const HVACComponent& outletComp,


Test patch:
diff --git a/src/model/test/AirLoopHVACOutdoorAirSystem_GTest.cpp b/src/model/test/AirLoopHVACOutdoorAirSystem_GTest.cpp
index 29e416e8f6e..19ae9bdca72 100644
--- a/src/model/test/AirLoopHVACOutdoorAirSystem_GTest.cpp
+++ b/src/model/test/AirLoopHVACOutdoorAirSystem_GTest.cpp
@@ -27,76 +27,451 @@
 *  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 ***********************************************************************************************************************/
 
-#include <gtest/gtest.h>
 #include "ModelFixture.hpp"
+
 #include "../AirLoopHVACOutdoorAirSystem.hpp"
 #include "../AirLoopHVACOutdoorAirSystem_Impl.hpp"
+
+#include "../AirLoopHVAC.hpp"
+#include "../CoilHeatingElectric.hpp"
+#include "../CoilHeatingElectric_Impl.hpp"
+#include "../CoilCoolingDXVariableSpeed.hpp"
 #include "../ControllerOutdoorAir.hpp"
 #include "../ControllerOutdoorAir_Impl.hpp"
-#include "../HVACComponent.hpp"
-#include "../HVACComponent_Impl.hpp"
-#include "../CoilHeatingElectric.hpp"
-#include "../Schedule.hpp"
+#include "../HeatExchangerAirToAirSensibleAndLatent.hpp"
+#include "../HeatExchangerAirToAirSensibleAndLatent_Impl.hpp"
 #include "../Node.hpp"
-#include "../AirLoopHVAC.hpp"
-#include "../../utilities/idd/IddEnums.hpp"
+#include "../Node_Impl.hpp"
+
 #include <utilities/idd/IddEnums.hxx>
 
+#include <algorithm>
+#include <iterator>
+
+using namespace openstudio;
 using namespace openstudio::model;
 
-TEST_F(ModelFixture, AirLoopHVACOutdoorAirSystem_AirLoopHVACOutdoorAirSystem) {
-  Model model = Model();
-  OptionalModelObject modelObject;
+TEST_F(ModelFixture, AirLoopHVACOutdoorAirSystem_GettersSetters) {
+  Model m;
+
+  ControllerOutdoorAir controller(m);
+  AirLoopHVACOutdoorAirSystem oaSystem(m, controller);
+  oaSystem.setName("My AirLoopHVACOutdoorAirSystem");
+  EXPECT_EQ(controller, oaSystem.getControllerOutdoorAir());
+
+  ControllerOutdoorAir controller2(m);
+  EXPECT_TRUE(oaSystem.setControllerOutdoorAir(controller2));
+  EXPECT_EQ(controller2, oaSystem.getControllerOutdoorAir());
+
+  EXPECT_EQ(2, oaSystem.components().size());
+  EXPECT_EQ(1, oaSystem.reliefComponents().size());
+  ASSERT_TRUE(oaSystem.outboardReliefNode());
+  auto outboardReliefNode = oaSystem.outboardReliefNode().get();
+  outboardReliefNode.setName("Outboard Relief Node");
+
+  ASSERT_TRUE(oaSystem.reliefAirModelObject());
+  EXPECT_EQ(oaSystem.outboardReliefNode().get(), oaSystem.reliefAirModelObject().get());
+
+  EXPECT_EQ(1, oaSystem.oaComponents().size());
+  ASSERT_TRUE(oaSystem.outboardOANode());
+  auto outboardOANode = oaSystem.outboardOANode().get();
+  outboardOANode.setName("Outboard OA Node");
+
+  ASSERT_TRUE(oaSystem.outdoorAirModelObject());
+  EXPECT_EQ(oaSystem.outboardOANode().get(), oaSystem.outdoorAirModelObject().get());
+
+  EXPECT_FALSE(oaSystem.returnAirModelObject());
+  EXPECT_FALSE(oaSystem.mixedAirModelObject());
+
+  EXPECT_FALSE(oaSystem.airLoopHVAC());
+
+  AirLoopHVAC a(m);
+  EXPECT_FALSE(a.mixedAirNode());
+  EXPECT_FALSE(a.returnAirNode());
+  EXPECT_FALSE(a.outdoorAirNode());
+  EXPECT_FALSE(a.reliefAirNode());
+  EXPECT_EQ(2, a.supplyComponents().size());
+  EXPECT_EQ(0, a.supplyComponents(openstudio::IddObjectType::OS_AirLoopHVAC_OutdoorAirSystem).size());
+
+  auto demandInletNode = a.demandInletNode();
+  EXPECT_FALSE(oaSystem.addToNode(demandInletNode));
+  EXPECT_FALSE(a.addBranchForHVACComponent(oaSystem));
+
+  auto supplyInletNode = a.supplyInletNode();
+  EXPECT_TRUE(oaSystem.addToNode(supplyInletNode));
+  ASSERT_TRUE(oaSystem.airLoopHVAC());
+  EXPECT_EQ(a, oaSystem.airLoopHVAC().get());
+
+  EXPECT_EQ(3, a.supplyComponents().size());
+  EXPECT_EQ(1, a.supplyComponents(openstudio::IddObjectType::OS_AirLoopHVAC_OutdoorAirSystem).size());
+  ASSERT_TRUE(a.supplyComponent(oaSystem.handle()));
 
-  ControllerOutdoorAir controller(model);
-  AirLoopHVACOutdoorAirSystem oaSystem = AirLoopHVACOutdoorAirSystem(model, controller);
+  ASSERT_TRUE(a.reliefAirNode());
+  EXPECT_EQ(2, oaSystem.components().size());
+  EXPECT_EQ(1, oaSystem.reliefComponents().size());
+  ASSERT_TRUE(oaSystem.outboardReliefNode());
+  ASSERT_TRUE(oaSystem.reliefAirModelObject());
+  EXPECT_EQ(oaSystem.outboardReliefNode().get(), oaSystem.reliefAirModelObject().get());
+  EXPECT_EQ(a.reliefAirNode().get(), oaSystem.reliefAirModelObject().get());
 
-  ASSERT_EQ(openstudio::IddObjectType::OS_AirLoopHVAC_OutdoorAirSystem, oaSystem.iddObjectType().value());
+  ASSERT_TRUE(a.outdoorAirNode());
+  EXPECT_EQ(1, oaSystem.oaComponents().size());
+  ASSERT_TRUE(oaSystem.outboardOANode());
+  ASSERT_TRUE(oaSystem.outdoorAirModelObject());
+  EXPECT_EQ(oaSystem.outboardOANode().get(), oaSystem.outdoorAirModelObject().get());
+  EXPECT_EQ(a.outdoorAirNode().get(), oaSystem.outdoorAirModelObject().get());
+
+  ASSERT_TRUE(a.mixedAirNode());
+  a.mixedAirNode().get().setName("Mixed Air Node");
+  ASSERT_TRUE(oaSystem.mixedAirModelObject());
+  EXPECT_EQ(a.mixedAirNode().get(), oaSystem.mixedAirModelObject().get());
+
+  ASSERT_TRUE(a.returnAirNode());
+  a.returnAirNode().get().setName("Return Air Node");
+  ASSERT_TRUE(oaSystem.returnAirModelObject());
+  EXPECT_EQ(a.returnAirNode().get(), oaSystem.returnAirModelObject().get());
+
+  CoilHeatingElectric oaCoil(m);
+  EXPECT_TRUE(oaCoil.addToNode(outboardOANode));
+
+  EXPECT_EQ(4, oaSystem.components().size());
+  EXPECT_EQ(1, oaSystem.reliefComponents().size());                    // (Relief)
+  EXPECT_EQ(outboardReliefNode, oaSystem.outboardReliefNode().get());  //
+  EXPECT_EQ(3, oaSystem.oaComponents().size());                        // (OA)-----Coil------o
+  EXPECT_EQ(outboardOANode, oaSystem.outboardOANode().get());
+
+  HeatExchangerAirToAirSensibleAndLatent erv(m);
+  EXPECT_TRUE(erv.addToNode(outboardOANode));
+
+  EXPECT_EQ(8, oaSystem.components().size());
+  EXPECT_EQ(outboardReliefNode, oaSystem.outboardReliefNode().get());
+  EXPECT_EQ(outboardOANode, oaSystem.outboardOANode().get());
+  EXPECT_EQ(3, oaSystem.reliefComponents().size());  //     o-----ERV-----(Relief)
+  EXPECT_EQ(5, oaSystem.oaComponents().size());      //  (OA)-----ERV-----o-----Coil1-----o
+                                                     //  <=>
+                                                     //  (RE)---- ERV---------------------o
+                                                     //            X
+                                                     //  (OA)-----ERV-----o-----Coil1-----o
+
+  CoilHeatingElectric reliefCoil(m);
+  EXPECT_TRUE(reliefCoil.addToNode(outboardReliefNode));
+  EXPECT_EQ(10, oaSystem.components().size());
+  EXPECT_EQ(outboardReliefNode, oaSystem.outboardReliefNode().get());
+  EXPECT_EQ(outboardOANode, oaSystem.outboardOANode().get());
+  EXPECT_EQ(5, oaSystem.reliefComponents().size());  //     o-----ERV-----o-----Coil2----(Relief)
+  EXPECT_EQ(5, oaSystem.oaComponents().size());      //  (OA)-----ERV-----o-----Coil1-----o
+                                                     //  <=>
+                                                     //  (RE)---- Coil2-----o----ERV---------------------o
+                                                     //                           X
+                                                     //  (OA)--------------------ERV-----o-----Coil1-----o
 }
 
-TEST_F(ModelFixture, AirLoopHVACOutdoorAirSystem_controllerOutdoorAir) {
-  Model model = Model();
-  OptionalModelObject modelObject;
+TEST_F(ModelFixture, AirLoopHVACOutdoorAirSystem_Clone_Simple) {
+  Model m;
+
+  ControllerOutdoorAir controller(m);
+  AirLoopHVACOutdoorAirSystem oaSystem(m, controller);
+
+  ASSERT_TRUE(oaSystem.outboardReliefNode());
+  auto outboardReliefNode = oaSystem.outboardReliefNode().get();
+  auto outboardOANode = oaSystem.outboardOANode().get();
+  outboardOANode.setName("Outboard OA Node");
+
+  CoilHeatingElectric oaCoil(m);
+  EXPECT_TRUE(oaCoil.addToNode(outboardOANode));
 
-  ControllerOutdoorAir controller(model);
-  AirLoopHVACOutdoorAirSystem oaSystem = AirLoopHVACOutdoorAirSystem(model, controller);
+  CoilHeatingElectric reliefCoil(m);
+  EXPECT_TRUE(reliefCoil.addToNode(outboardReliefNode));
 
-  ASSERT_EQ(openstudio::IddObjectType::OS_Controller_OutdoorAir, oaSystem.getControllerOutdoorAir().iddObjectType().value());
+  EXPECT_EQ(6, oaSystem.components().size());
+  EXPECT_EQ(3, oaSystem.reliefComponents().size());
+  EXPECT_EQ(3, oaSystem.oaComponents().size());  // (OA)-----Coil------o
+  EXPECT_EQ(1, m.getConcreteModelObjects<AirLoopHVACOutdoorAirSystem>().size());
+  EXPECT_EQ(1, m.getConcreteModelObjects<ControllerOutdoorAir>().size());
+  EXPECT_EQ(4, m.getConcreteModelObjects<Node>().size());
+  EXPECT_EQ(2, m.getConcreteModelObjects<CoilHeatingElectric>().size());
+
+  {
+    // Other model first
+    Model m2;
+    auto oaSystem2 = oaSystem.clone(m2).cast<AirLoopHVACOutdoorAirSystem>();
+
+    EXPECT_NE(controller, oaSystem2.getControllerOutdoorAir());
+
+    EXPECT_EQ(6, oaSystem2.components().size());
+    EXPECT_EQ(3, oaSystem2.reliefComponents().size());
+    EXPECT_EQ(3, oaSystem2.oaComponents().size());  // (OA)-----Coil------o
+    EXPECT_EQ(1, m2.getConcreteModelObjects<AirLoopHVACOutdoorAirSystem>().size());
+    EXPECT_EQ(1, m2.getConcreteModelObjects<ControllerOutdoorAir>().size());
+    EXPECT_EQ(4, m2.getConcreteModelObjects<Node>().size());
+    EXPECT_EQ(2, m2.getConcreteModelObjects<CoilHeatingElectric>().size());
+  }
+
+  {
+    // Same model
+    auto oaSystem2 = oaSystem.clone(m).cast<AirLoopHVACOutdoorAirSystem>();
+
+    EXPECT_NE(controller, oaSystem2.getControllerOutdoorAir());
+
+    EXPECT_EQ(6, oaSystem2.components().size());
+    EXPECT_EQ(3, oaSystem2.reliefComponents().size());
+    EXPECT_EQ(3, oaSystem2.oaComponents().size());  // (OA)-----Coil------o
+    EXPECT_EQ(2, m.getConcreteModelObjects<AirLoopHVACOutdoorAirSystem>().size());
+    EXPECT_EQ(2, m.getConcreteModelObjects<ControllerOutdoorAir>().size());
+    EXPECT_EQ(8, m.getConcreteModelObjects<Node>().size());
+    EXPECT_EQ(4, m.getConcreteModelObjects<CoilHeatingElectric>().size());
+  }
 }
 
-TEST_F(ModelFixture, AirLoopHVACOutdoorAirSystem_clone) {
-  Model model = Model();
-  Model model2 = Model();
-  OptionalModelObject modelObject;
+TEST_F(ModelFixture, AirLoopHVACOutdoorAirSystem_Clone_Shared) {
+  Model m;
+
+  ControllerOutdoorAir controller(m);
+  AirLoopHVACOutdoorAirSystem oaSystem(m, controller);
+
+  ASSERT_TRUE(oaSystem.outboardReliefNode());
+  auto outboardReliefNode = oaSystem.outboardReliefNode().get();
+  auto outboardOANode = oaSystem.outboardOANode().get();
+  outboardOANode.setName("Outboard OA Node");
+
+  CoilHeatingElectric oaCoil(m);
+  oaCoil.setName("OACoil");
+  EXPECT_TRUE(oaCoil.addToNode(outboardOANode));
+
+  HeatExchangerAirToAirSensibleAndLatent erv(m);
+  erv.setName("ERV");
+  EXPECT_TRUE(erv.addToNode(outboardOANode));
 
-  ControllerOutdoorAir controller(model);
-  AirLoopHVACOutdoorAirSystem oaSystem = AirLoopHVACOutdoorAirSystem(model, controller);
+  CoilHeatingElectric reliefCoil(m);
+  reliefCoil.setName("RECoil");
+  EXPECT_TRUE(reliefCoil.addToNode(outboardReliefNode));
 
-  ASSERT_EQ(openstudio::IddObjectType::OS_Controller_OutdoorAir, oaSystem.getControllerOutdoorAir().iddObjectType().value());
+  EXPECT_EQ(10, oaSystem.components().size());       // ERV counted twice
+  EXPECT_EQ(5, oaSystem.reliefComponents().size());  //     o-----ERV-----o-----RECoil----(RE)
+  EXPECT_EQ(5, oaSystem.oaComponents().size());      //  (OA)-----ERV-----o-----OACoil-----o
+                                                     //  <=>
+                                                     //  (RE)---- RECoil-----o-----ERV----------------------o
+                                                     //                             X
+                                                     //  (OA)----------------------ERV-----o-----OACoil-----o
+  EXPECT_EQ(1, m.getConcreteModelObjects<AirLoopHVACOutdoorAirSystem>().size());
+  EXPECT_EQ(1, m.getConcreteModelObjects<ControllerOutdoorAir>().size());
+  EXPECT_EQ(6, m.getConcreteModelObjects<Node>().size());
+  EXPECT_EQ(2, m.getConcreteModelObjects<CoilHeatingElectric>().size());
+  EXPECT_EQ(1, m.getConcreteModelObjects<HeatExchangerAirToAirSensibleAndLatent>().size());
 
-  auto oaSystem2 = oaSystem.clone(model2).cast<AirLoopHVACOutdoorAirSystem>();
+  {
+    // Other model first
+    Model m2;
+    auto oaSystem2 = oaSystem.clone(m2).cast<AirLoopHVACOutdoorAirSystem>();
 
-  ASSERT_EQ(openstudio::IddObjectType::OS_Controller_OutdoorAir, oaSystem2.getControllerOutdoorAir().iddObjectType().value());
+    EXPECT_NE(controller, oaSystem2.getControllerOutdoorAir());
+
+    EXPECT_EQ(10, oaSystem2.components().size());
+    EXPECT_EQ(5, oaSystem2.reliefComponents().size());
+    EXPECT_EQ(5, oaSystem2.oaComponents().size());
+    EXPECT_EQ(1, m2.getConcreteModelObjects<AirLoopHVACOutdoorAirSystem>().size());
+    EXPECT_EQ(1, m2.getConcreteModelObjects<ControllerOutdoorAir>().size());
+    EXPECT_EQ(6, m2.getConcreteModelObjects<Node>().size());
+    EXPECT_EQ(2, m2.getConcreteModelObjects<CoilHeatingElectric>().size());
+    EXPECT_EQ(1, m2.getConcreteModelObjects<HeatExchangerAirToAirSensibleAndLatent>().size());
+  }
+
+  {
+    // Same model
+    auto oaSystem2 = oaSystem.clone(m).cast<AirLoopHVACOutdoorAirSystem>();
+
+    EXPECT_NE(controller, oaSystem2.getControllerOutdoorAir());
+
+    EXPECT_EQ(10, oaSystem2.components().size());
+    EXPECT_EQ(5, oaSystem2.reliefComponents().size());
+    EXPECT_EQ(5, oaSystem2.oaComponents().size());
+    EXPECT_EQ(2, m.getConcreteModelObjects<AirLoopHVACOutdoorAirSystem>().size());
+    EXPECT_EQ(2, m.getConcreteModelObjects<ControllerOutdoorAir>().size());
+    EXPECT_EQ(12, m.getConcreteModelObjects<Node>().size());
+    EXPECT_EQ(4, m.getConcreteModelObjects<CoilHeatingElectric>().size());
+    EXPECT_EQ(2, m.getConcreteModelObjects<HeatExchangerAirToAirSensibleAndLatent>().size());
+  }
 }
 
-//TEST_F(ModelFixture,AirLoopHVACOutdoorAirSystem_edges)
-//{
-//  Model m;
-//  Schedule s = m.alwaysOnDiscreteSchedule();
-//  AirLoopHVAC airLoop(m);
-//  ControllerOutdoorAir controllerOutdoorAir(m);
-//  AirLoopHVACOutdoorAirSystem outdoorAirSystem(m,controllerOutdoorAir);
-//  CoilHeatingElectric coil(m, s);
-//
-//  Node supplyOutletNode = airLoop.supplyOutletNode();
-//  outdoorAirSystem.addToNode(supplyOutletNode);
-//  coil.addToNode(supplyOutletNode);
-//  boost::optional<ModelObject> testObject = airLoop.supplyComponent(outdoorAirSystem.handle());
-//  ASSERT_TRUE(testObject);
-//  EXPECT_EQ(outdoorAirSystem, *testObject);
-//  std::vector<HVACComponent> edges = outdoorAirSystem.getImpl<detail::HVACComponent_Impl>()->edges(false); // should be Node
-//  ASSERT_EQ(1, edges.size());
-//  edges = edges[0].getImpl<detail::HVACComponent_Impl>()->edges(false); // should be CoilHeatingElectric
-//  ASSERT_EQ(1, edges.size());
-//  EXPECT_EQ(coil, edges[0]);
-//}
+TEST_F(ModelFixture, AirLoopHVACOutdoorAirSystem_Clone_SeveralERVs) {
+
+  // Add the OASystem to AirLoopHVAC and save resulting OSM to check the clones in OSApp
+  constexpr bool debug = true;
+
+  Model m;
+
+  ControllerOutdoorAir controller(m);
+  AirLoopHVACOutdoorAirSystem oaSystem(m, controller);
+
+  ASSERT_TRUE(oaSystem.outboardReliefNode());
+  auto outboardReliefNode = oaSystem.outboardReliefNode().get();
+  auto outboardOANode = oaSystem.outboardOANode().get();
+  outboardOANode.setName("(OA)");
+  outboardReliefNode.setName("(RE)");
+
+  CoilHeatingElectric oaCoilA(m);
+  oaCoilA.setName("OACoilA");
+  oaCoilA.setComment("OACoilA");
+  EXPECT_TRUE(oaCoilA.addToNode(outboardOANode));
+
+  CoilHeatingElectric reliefCoilA(m);
+  reliefCoilA.setName("RECoilA");
+  reliefCoilA.setComment("RECoilA");
+  EXPECT_TRUE(reliefCoilA.addToNode(outboardReliefNode));
+
+  HeatExchangerAirToAirSensibleAndLatent ervA(m);
+  ervA.setName("ERVA");
+  ervA.setComment("ERVA");
+  EXPECT_TRUE(ervA.addToNode(outboardOANode));
+
+  CoilHeatingElectric oaCoilB(m);
+  oaCoilB.setName("OACoilB");
+  oaCoilB.setComment("OACoilB");
+  EXPECT_TRUE(oaCoilB.addToNode(outboardOANode));
+
+  HeatExchangerAirToAirSensibleAndLatent ervB(m);
+  ervB.setName("ERVB");
+  ervB.setComment("ERVB");
+  EXPECT_TRUE(ervB.addToNode(outboardOANode));
+
+  CoilHeatingElectric reliefCoilB(m);
+  reliefCoilB.setName("RECoilB");
+  reliefCoilB.setComment("RECoilB");
+  EXPECT_TRUE(reliefCoilB.addToNode(outboardReliefNode));
+
+  EXPECT_EQ(18, oaSystem.components().size());       // ERV counted twice
+  EXPECT_EQ(9, oaSystem.reliefComponents().size());  //     o-----RECoilA-----o-----ERVA-----o-----ERVB-----o-----RECoilB-----(RE)
+  EXPECT_EQ(9, oaSystem.oaComponents().size());      //  (OA)-----ERVB-----o-----OACoilB-----o-----ERVA-----o-----OACoilA-----o
+                                                     //  <=>
+                                                     //  (RE)---- RECoilB-----o----ERVB-----o-----------------------ERVA-----o-----RECoilA-----o
+                                                     //                             X                                 X
+                                                     //  (OA)----------------------ERVB-----o-----OACoilB-----o-----ERVA-----o-----OACoilA-----o
+
+  EXPECT_EQ(1, m.getConcreteModelObjects<AirLoopHVACOutdoorAirSystem>().size());
+  EXPECT_EQ(1, m.getConcreteModelObjects<ControllerOutdoorAir>().size());
+  EXPECT_EQ(10, m.getConcreteModelObjects<Node>().size());
+  EXPECT_EQ(4, m.getConcreteModelObjects<CoilHeatingElectric>().size());
+  EXPECT_EQ(2, m.getConcreteModelObjects<HeatExchangerAirToAirSensibleAndLatent>().size());
+
+  auto getTypes = [](const auto& comps) {
+    std::vector<IddObjectType> compTypes;
+    compTypes.reserve(comps.size());
+    std::transform(comps.cbegin(), comps.cend(), std::back_inserter(compTypes), [](const auto& s) { return s.iddObjectType(); });
+    return compTypes;
+  };
+  auto getComments = [](const auto& comps) {
+    std::vector<std::string> compComments;
+    compComments.reserve(comps.size());
+    std::transform(comps.cbegin(), comps.cend(), std::back_inserter(compComments), [](const auto& s) { return s.comment(); });
+    return compComments;
+  };
+
+  auto testStreamLayout = [&getTypes, &getComments](const std::vector<ModelObject>& lhsComps, const std::vector<ModelObject>& rhsComps) {
+    ASSERT_EQ(lhsComps.size(), rhsComps.size());
+    auto lhsTypes = getTypes(lhsComps);
+    auto rhsTypes = getTypes(rhsComps);
+    EXPECT_EQ(lhsTypes, rhsTypes);
+    auto lhsComments = getComments(lhsComps);
+    auto rhsComments = getComments(rhsComps);
+    EXPECT_EQ(lhsComments, rhsComments);
+  };
+
+  auto testLayout = [&testStreamLayout](const AirLoopHVACOutdoorAirSystem& lhs, const AirLoopHVACOutdoorAirSystem& rhs) {
+    testStreamLayout(lhs.reliefComponents(), rhs.reliefComponents());
+    testStreamLayout(lhs.oaComponents(), rhs.oaComponents());
+  };
+
+  {
+    // Other model first
+    Model m2;
+    auto oaSystem2 = oaSystem.clone(m2).cast<AirLoopHVACOutdoorAirSystem>();
+
+    EXPECT_NE(controller, oaSystem2.getControllerOutdoorAir());
+
+    EXPECT_EQ(18, oaSystem2.components().size());  // ERV counted twice
+    EXPECT_EQ(9, oaSystem2.reliefComponents().size());
+    EXPECT_EQ(9, oaSystem2.oaComponents().size());  // (OA)-----Coil------o
+    EXPECT_EQ(1, m2.getConcreteModelObjects<AirLoopHVACOutdoorAirSystem>().size());
+    EXPECT_EQ(1, m2.getConcreteModelObjects<ControllerOutdoorAir>().size());
+    EXPECT_EQ(10, m2.getConcreteModelObjects<Node>().size());
+    EXPECT_EQ(4, m2.getConcreteModelObjects<CoilHeatingElectric>().size());
+    EXPECT_EQ(2, m2.getConcreteModelObjects<HeatExchangerAirToAirSensibleAndLatent>().size());
+
+    testLayout(oaSystem, oaSystem2);
+
+    if constexpr (debug) {
+      AirLoopHVAC a2(m2);
+      a2.setName("Cloned");
+      auto node = a2.supplyOutletNode();
+      oaSystem2.addToNode(node);
+      m2.save("AirLoopHVACOutdoorAirSystem_Clone_SeveralERVs_OtherModel.osm", true);
+    }
+  }
+
+  if constexpr (debug) {
+    AirLoopHVAC a(m);
+    a.setName("Ori");
+    auto node = a.supplyOutletNode();
+    oaSystem.addToNode(node);
+  }
+
+  {
+    // Same model
+    auto oaSystem2 = oaSystem.clone(m).cast<AirLoopHVACOutdoorAirSystem>();
+
+    EXPECT_NE(controller, oaSystem2.getControllerOutdoorAir());
+
+    EXPECT_EQ(18, oaSystem2.components().size());  // ERV counted twice
+    EXPECT_EQ(9, oaSystem2.reliefComponents().size());
+    EXPECT_EQ(9, oaSystem2.oaComponents().size());  // (OA)-----Coil------o
+    EXPECT_EQ(2, m.getConcreteModelObjects<AirLoopHVACOutdoorAirSystem>().size());
+    EXPECT_EQ(2, m.getConcreteModelObjects<ControllerOutdoorAir>().size());
+    if constexpr (debug) {
+      EXPECT_EQ(25, m.getConcreteModelObjects<Node>().size());
+    } else {
+      EXPECT_EQ(20, m.getConcreteModelObjects<Node>().size());
+    }
+    EXPECT_EQ(8, m.getConcreteModelObjects<CoilHeatingElectric>().size());
+    EXPECT_EQ(4, m.getConcreteModelObjects<HeatExchangerAirToAirSensibleAndLatent>().size());
+
+    testLayout(oaSystem, oaSystem2);
+
+    if constexpr (debug) {
+      AirLoopHVAC a2(m);
+      a2.setName("Cloned");
+      auto node = a2.supplyOutletNode();
+      oaSystem2.addToNode(node);
+      m.save("AirLoopHVACOutdoorAirSystem_Clone_SeveralERVs.osm", true);
+    }
+  }
+}
+
+TEST_F(ModelFixture, AirLoopHVACOutdoorAirSystem_HeatCoolFuelTypes) {
+  Model m;
+
+  ControllerOutdoorAir controller(m);
+  AirLoopHVACOutdoorAirSystem oaSystem(m, controller);
+  auto outboardReliefNode = oaSystem.outboardReliefNode().get();
+  auto outboardOANode = oaSystem.outboardOANode().get();
+
+  EXPECT_EQ(ComponentType(ComponentType::None), oaSystem.componentType());
+  testFuelTypeEquality({}, oaSystem.coolingFuelTypes());
+  testFuelTypeEquality({}, oaSystem.heatingFuelTypes());
+  testAppGFuelTypeEquality({}, oaSystem.appGHeatingFuelTypes());
+
+  CoilHeatingElectric oaCoil(m);
+  EXPECT_TRUE(oaCoil.addToNode(outboardOANode));
+
+  EXPECT_EQ(ComponentType(ComponentType::Heating), oaSystem.componentType());
+  testFuelTypeEquality({}, oaSystem.coolingFuelTypes());
+  testFuelTypeEquality({FuelType::Electricity}, oaSystem.heatingFuelTypes());
+  testAppGFuelTypeEquality({AppGFuelType::Electric}, oaSystem.appGHeatingFuelTypes());
+
+  CoilCoolingDXVariableSpeed reliefCoil(m);
+  EXPECT_TRUE(reliefCoil.addToNode(outboardReliefNode));
+
+  EXPECT_EQ(ComponentType(ComponentType::Both), oaSystem.componentType());
+  testFuelTypeEquality({FuelType::Electricity}, oaSystem.coolingFuelTypes());
+  testFuelTypeEquality({FuelType::Electricity}, oaSystem.heatingFuelTypes());
+  testAppGFuelTypeEquality({AppGFuelType::Electric}, oaSystem.appGHeatingFuelTypes());
+}
diff --git a/src/model/test/AirLoopHVAC_GTest.cpp b/src/model/test/AirLoopHVAC_GTest.cpp
index dfdf3d66ac2..4f90b410aea 100644
--- a/src/model/test/AirLoopHVAC_GTest.cpp
+++ b/src/model/test/AirLoopHVAC_GTest.cpp
@@ -44,8 +44,8 @@
 #include "../AirLoopHVACZoneMixer_Impl.hpp"
 #include "../AirLoopHVACZoneSplitter.hpp"
 #include "../AirLoopHVACZoneSplitter_Impl.hpp"
-#include "../AirTerminalSingleDuctConstantVolumeNoReheat.hpp"
-#include "../AirTerminalSingleDuctConstantVolumeNoReheat_Impl.hpp"
+#include "../HeatExchangerAirToAirSensibleAndLatent.hpp"
+#include "../HeatExchangerAirToAirSensibleAndLatent_Impl.hpp"
 
 #include "../PlantLoop.hpp"
 #include "../PlantLoop_Impl.hpp"
@@ -56,6 +56,8 @@
 #include "../CoilCoolingCooledBeam.hpp"
 #include "../CoilCoolingCooledBeam_Impl.hpp"
 
+#include "../AirTerminalSingleDuctConstantVolumeNoReheat.hpp"
+#include "../AirTerminalSingleDuctConstantVolumeNoReheat_Impl.hpp"
 #include "../AirTerminalSingleDuctConstantVolumeFourPipeInduction.hpp"
 #include "../AirTerminalSingleDuctConstantVolumeFourPipeInduction_Impl.hpp"
 #include "../AirTerminalSingleDuctVAVReheat.hpp"
@@ -1672,6 +1674,116 @@ TEST_F(ModelFixture, AirLoopHVAC_singleDuct_Clone_WithComponents) {
   }
 }
 
+TEST_F(ModelFixture, AirLoopHVAC_singleDuct_Clone_WithOutdoorAirSystem) {
+  // Test for #4869 - AirLoopHVAC::clone will duplicate components that are on the relief and intake air streams of an OutdoorAirSystem
+  Model m;
+
+  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(5, m.getConcreteModelObjects<Node>().size());
+  EXPECT_EQ(1, m.getModelObjects<Splitter>().size());
+  EXPECT_EQ(1, m.getModelObjects<Mixer>().size());
+
+  auto supplyOutletNode = a.supplyOutletNode();
+
+  ControllerOutdoorAir controller(m);
+  AirLoopHVACOutdoorAirSystem oaSystem(m, controller);
+
+  ASSERT_TRUE(oaSystem.addToNode(supplyOutletNode));
+  EXPECT_EQ(7, m.getConcreteModelObjects<Node>().size());
+  EXPECT_EQ(7, a.components(openstudio::IddObjectType::OS_Node).size());
+
+  EXPECT_EQ(1, a.supplyOutletNodes().size());
+  EXPECT_EQ(3, a.supplyComponents().size());  // o----OASystem----o
+  EXPECT_EQ(5, a.demandComponents().size());  // 3 nodes, one splitter, one mixer
+
+  EXPECT_FALSE(a.supplySplitter());
+
+  HeatExchangerAirToAirSensibleAndLatent erv(m);
+  Node oaNode = oaSystem.outboardOANode().get();
+  EXPECT_TRUE(erv.addToNode(oaNode));
+  EXPECT_EQ(9, m.getConcreteModelObjects<Node>().size());
+  EXPECT_EQ(1, m.getConcreteModelObjects<HeatExchangerAirToAirSensibleAndLatent>().size());
+  EXPECT_EQ(9, a.components(openstudio::IddObjectType::OS_Node).size());
+  EXPECT_EQ(1, a.supplyOutletNodes().size());
+  EXPECT_EQ(3, a.supplyComponents().size());  // o----OASystem----o
+  EXPECT_EQ(5, a.demandComponents().size());  // o----Splitter-----o----mixer----o
+
+  auto countType = [](const auto& vec, const openstudio::IddObjectType& iddObjType) {
+    return std::count_if(vec.cbegin(), vec.cend(), [&iddObjType](const auto& c) { return c.iddObjectType() == iddObjType; });
+  };
+
+  {
+    auto oaSystemComps = oaSystem.components();
+    EXPECT_EQ(6, oaSystemComps.size());  // ERV is present twice there
+    EXPECT_EQ(4, countType(oaSystemComps, openstudio::IddObjectType::OS_Node));
+    EXPECT_EQ(2, countType(oaSystemComps, openstudio::IddObjectType::OS_HeatExchanger_AirToAir_SensibleAndLatent));
+  }
+  {
+    auto oaSystemReliefComps = oaSystem.reliefComponents();
+    EXPECT_EQ(3, oaSystemReliefComps.size());
+    EXPECT_EQ(2, countType(oaSystemReliefComps, openstudio::IddObjectType::OS_Node));
+    EXPECT_EQ(1, countType(oaSystemReliefComps, openstudio::IddObjectType::OS_HeatExchanger_AirToAir_SensibleAndLatent));
+  }
+  {
+    auto oaSystemOAComps = oaSystem.oaComponents();
+    EXPECT_EQ(3, oaSystemOAComps.size());
+    EXPECT_EQ(2, countType(oaSystemOAComps, openstudio::IddObjectType::OS_Node));
+    EXPECT_EQ(1, countType(oaSystemOAComps, openstudio::IddObjectType::OS_HeatExchanger_AirToAir_SensibleAndLatent));
+  }
+
+  {
+    Model m2;
+    auto a2 = a.clone(m2).cast<AirLoopHVAC>();
+    ASSERT_TRUE(a2.airLoopHVACOutdoorAirSystem());
+    auto oaSystem2 = a2.airLoopHVACOutdoorAirSystem().get();
+
+    EXPECT_EQ(1, m2.getConcreteModelObjects<AirLoopHVAC>().size());
+    EXPECT_EQ(1, m2.getConcreteModelObjects<HeatExchangerAirToAirSensibleAndLatent>().size());
+    EXPECT_EQ(9, m2.getConcreteModelObjects<Node>().size());
+    EXPECT_EQ(9, a2.components(openstudio::IddObjectType::OS_Node).size());
+    EXPECT_EQ(1, a2.supplyOutletNodes().size());
+    EXPECT_EQ(3, a2.supplyComponents().size());  // o----OASystem----o
+    EXPECT_EQ(5, a2.demandComponents().size());  // o----Splitter-----o----mixer----o
+
+    {
+      auto oaSystemComps = oaSystem2.components();
+      EXPECT_EQ(6, oaSystemComps.size());  // ERV is present twice there
+      EXPECT_EQ(4, countType(oaSystemComps, openstudio::IddObjectType::OS_Node));
+      EXPECT_EQ(2, countType(oaSystemComps, openstudio::IddObjectType::OS_HeatExchanger_AirToAir_SensibleAndLatent));
+    }
+    {
+      auto oaSystemReliefComps = oaSystem2.reliefComponents();
+      EXPECT_EQ(3, oaSystemReliefComps.size());
+      EXPECT_EQ(2, countType(oaSystemReliefComps, openstudio::IddObjectType::OS_Node));
+      EXPECT_EQ(1, countType(oaSystemReliefComps, openstudio::IddObjectType::OS_HeatExchanger_AirToAir_SensibleAndLatent));
+    }
+    {
+      auto oaSystemOAComps = oaSystem2.oaComponents();
+      EXPECT_EQ(3, oaSystemOAComps.size());
+      EXPECT_EQ(2, countType(oaSystemOAComps, openstudio::IddObjectType::OS_Node));
+      EXPECT_EQ(1, countType(oaSystemOAComps, openstudio::IddObjectType::OS_HeatExchanger_AirToAir_SensibleAndLatent));
+    }
+  }
+
+  {
+    // Same model
+    auto a2 = a.clone(m).cast<AirLoopHVAC>();
+
+    EXPECT_EQ(2, m.getConcreteModelObjects<AirLoopHVAC>().size());
+    EXPECT_EQ(2, m.getConcreteModelObjects<HeatExchangerAirToAirSensibleAndLatent>().size());
+  }
+}
+
 TEST_F(ModelFixture, AirLoopHVAC_dualDuct_Clone) {
   // Test for #4060
   Model m;