Repository: NREL/OpenStudio (language: python)

Issue:
add Coil:UserDefined
issue https://github.com/NREL/OpenStudio/issues/4823
issue https://github.com/NREL/OpenStudio/issues/4449

Pull request overview
---------------------

 - Fixes #4825

### Pull Request Author

<!--- Add to this list or remove from it as applicable.  This is a simple templated set of guidelines. -->

 - [x] Model API Changes / Additions
 - [x] Any new or modified fields have been implemented in the EnergyPlus ForwardTranslator (and ReverseTranslator as appropriate)
 - [x] Model API methods are tested (in `src/model/test`)
 - [x] EnergyPlus ForwardTranslator Tests (in `src/energyplus/Test`)
 - [x] If a new object or method, added a test in NREL/OpenStudio-resources: Add Link (https://github.com/NREL/OpenStudio-resources/pull/193)
 - [x] If needed, added VersionTranslation rules for the objects (`src/osversion/VersionTranslator.cpp`)
 - [ ] Verified that C# bindings built fine on Windows, partial classes used as needed, etc.
 - [x] All new and existing tests passes
 - [ ] If methods have been deprecated, update rest of code to use the new methods

**Labels:**

 - [ ] If change to an IDD file, add the label `IDDChange`
 - [ ] If breaking existing API, add the label `APIChange`
 - [x] If deemed ready, add label `Pull Request - Ready for CI` so that CI builds your PR

### Review Checklist

This will not be exhaustively relevant to every PR.
 - [x] Perform a Code Review on GitHub
 - [ ] Code Style, strip trailing whitespace, etc.
 - [x] All related changes have been implemented: model changes, model tests, FT changes, FT tests, VersionTranslation, OS App
 - [x] Labeling is ok
 - [ ] If defect, verify by running develop branch and reproducing defect, then running PR and reproducing fix
 - [x] If feature, test running new feature, try creative ways to break it
 - [ ] CI status: all green or justified

Wrap OutputControl:Timestamp
Pull request overview
---------------------

 - Fixes #4803

### Pull Request Author

<!--- Add to this list or remove from it as applicable.  This is a simple templated set of guidelines. -->

 - [ ] Model API Changes / Additions
 - [ ] Any new or modified fields have been implemented in the EnergyPlus ForwardTranslator (and ReverseTranslator as appropriate)
 - [ ] Model API methods are tested (in `src/model/test`)
 - [ ] EnergyPlus ForwardTranslator Tests (in `src/energyplus/Test`)
 - [ ] If a new object or method, added a test in NREL/OpenStudio-resources:
   - https://github.com/NREL/OpenStudio-resources/pull/191
 - [ ] If needed, added VersionTranslation rules for the objects (`src/osversion/VersionTranslator.cpp`)
 - [ ] Verified that C# bindings built fine on Windows, partial classes used as needed, etc.
 - [ ] All new and existing tests passes
 - [ ] If methods have been deprecated, update rest of code to use the new methods

**Labels:**

 - [ ] If change to an IDD file, add the label `IDDChange`
 - [ ] If breaking existing API, add the label `APIChange`
 - [ ] If deemed ready, add label `Pull Request - Ready for CI` so that CI builds your PR

### Review Checklist

This will not be exhaustively relevant to every PR.
 - [ ] Perform a Code Review on GitHub
 - [ ] Code Style, strip trailing whitespace, etc.
 - [ ] All related changes have been implemented: model changes, model tests, FT changes, FT tests, VersionTranslation, OS App
 - [ ] Labeling is ok
 - [ ] If defect, verify by running develop branch and reproducing defect, then running PR and reproducing fix
 - [ ] If feature, test running new feature, try creative ways to break it
 - [ ] CI status: all green or justified



Discussion / hints:

Fix patch:
diff --git a/resources/model/OpenStudio.idd b/resources/model/OpenStudio.idd
index 829295ac8eb..025f7314306 100644
--- a/resources/model/OpenStudio.idd
+++ b/resources/model/OpenStudio.idd
@@ -11835,6 +11835,7 @@ OS:AirLoopHVAC:UnitarySystem,
        \object-list HeatingCoilsElectricMultiStage
        \object-list HeatingCoilsGasMultiStage
        \object-list HeatingCoilsDesuperheater
+       \object-list UserDefinedCoil
        \note Enter the name of the heating coil if included in the unitary system.
   N1,  \field DX Heating Coil Sizing Ratio
        \type real
@@ -11851,6 +11852,7 @@ OS:AirLoopHVAC:UnitarySystem,
        \object-list CoolingCoilsWaterToAirHP
        \object-list CoolingCoilsWaterToAirVSHP
        \object-list CoolingCoilsWater
+       \object-list UserDefinedCoil
        \note Enter the name of the cooling coil if included in the unitary system.
   A14, \field Use DOAS DX Cooling Coil
        \type choice
@@ -38539,6 +38541,85 @@ OS:ExternalInterface:FunctionalMockupUnitExport:To:Variable,
 
 \group User Defined HVAC and Plant Component Models
 
+OS:Coil:UserDefined,
+       \memo Defines a generic air system component for custom modeling
+       \memo using Energy Management System or External Interface
+       \min-fields 5
+  A1,  \field Handle
+       \type handle
+       \required-field
+  A2,  \field Name
+       \required-field
+       \type alpha
+       \reference UserDefinedCoil
+       \reference ConnectionObject
+  A3,  \field Overall Model Simulation Program Calling Manager Name
+       \type object-list
+       \object-list ErlProgramCallingManagerNames
+       \required-field
+  A4,  \field Model Setup and Sizing Program Calling Manager Name
+       \type object-list
+       \object-list ErlProgramCallingManagerNames
+       \required-field
+  A5,  \field Air Connection 1 Inlet Node Name
+       \type object-list
+       \object-list ConnectionNames
+       \required-field
+  A6,  \field Air Connection 1 Outlet Node Name
+       \type object-list
+       \object-list ConnectionNames
+       \required-field
+  A8,  \field Plant Connection Inlet Node Name
+       \type object-list
+       \object-list ConnectionNames
+  A9,  \field Plant Connection Outlet Node Name
+       \type object-list
+       \object-list ConnectionNames
+  A10, \field Ambient Zone Name
+       \type object-list
+       \object-list ThermalZoneNames
+       \note Used for modeling device losses to surrounding zone
+  A11, \field Overall Simulation Program Name
+       \type object-list
+       \object-list ErlProgramNames
+       \required-field
+  A12, \field Initialization Simulation Program Name
+       \type object-list
+       \object-list ErlProgramNames
+       \required-field
+  A13, \field Air Outlet Temperature Actuator
+       \type object-list
+       \object-list ErlActuatorNames
+       \required-field
+  A14, \field Air Outlet Humidity Ratio Actuator
+       \type object-list
+       \object-list ErlActuatorNames
+       \required-field
+  A15, \field Air Mass Flow Rate Actuator
+       \type object-list
+       \object-list ErlActuatorNames
+       \required-field
+  A16, \field Plant Minimum Mass Flow Rate Actuator
+       \type object-list
+       \object-list ErlActuatorNames
+       \required-field
+  A17, \field Plant Maximum Mass Flow Rate Actuator
+       \type object-list
+       \object-list ErlActuatorNames
+       \required-field
+  A18, \field Plant Design Volume Flow Rate Actuator
+       \type object-list
+       \object-list ErlActuatorNames
+       \required-field
+  A19, \field Plant Outlet Temperature Actuator
+       \type object-list
+       \object-list ErlActuatorNames
+       \required-field
+  A20; \field Plant Mass Flow Rate Actuator
+       \type object-list
+       \object-list ErlActuatorNames
+       \required-field
+
 OS:PlantComponent:UserDefined,
       \memo Defines a generic plant component for custom modeling
       \memo using Energy Management System or External Interface
diff --git a/src/energyplus/CMakeLists.txt b/src/energyplus/CMakeLists.txt
index 57217c4eb42..b8d29c9202d 100644
--- a/src/energyplus/CMakeLists.txt
+++ b/src/energyplus/CMakeLists.txt
@@ -99,6 +99,7 @@ set(${target_name}_src
   ForwardTranslator/ForwardTranslateCoilSystemCoolingDXHeatExchangerAssisted.cpp
   ForwardTranslator/ForwardTranslateCoilSystemCoolingWaterHeatExchangerAssisted.cpp
   ForwardTranslator/ForwardTranslateCoilSystemIntegratedHeatPumpAirSource.cpp
+  ForwardTranslator/ForwardTranslateCoilUserDefined.cpp
   ForwardTranslator/ForwardTranslateCoilWaterHeatingDesuperheater.cpp
   ForwardTranslator/ForwardTranslateCoilWaterHeatingAirToWaterHeatPump.cpp
   ForwardTranslator/ForwardTranslateCoilWaterHeatingAirToWaterHeatPumpVariableSpeed.cpp
@@ -696,6 +697,7 @@ set(${target_name}_test_src
   Test/CoilHeatingGas_GTest.cpp
   Test/CoilHeatingGasMultiStage_GTest.cpp
   Test/CoilSystemIntegratedHeatPumpAirSource_GTest.cpp
+  Test/CoilUserDefined_GTest.cpp
   Test/CoilWaterHeatingAirToWaterHeatPumpVariableSpeed_GTest.cpp
   Test/CoilWaterHeatingDesuperheater_GTest.cpp
   Test/Construction_GTest.cpp
diff --git a/src/energyplus/ForwardTranslator.cpp b/src/energyplus/ForwardTranslator.cpp
index 263537436d1..73eab131175 100644
--- a/src/energyplus/ForwardTranslator.cpp
+++ b/src/energyplus/ForwardTranslator.cpp
@@ -1231,6 +1231,11 @@ namespace energyplus {
         retVal = translateCoilSystemIntegratedHeatPumpAirSource(mo);
         break;
       }
+      case openstudio::IddObjectType::OS_Coil_UserDefined: {
+        auto coil = modelObject.cast<CoilUserDefined>();
+        retVal = translateCoilUserDefined(coil);
+        break;
+      }
       case openstudio::IddObjectType::OS_Coil_WaterHeating_Desuperheater: {
         auto coil = modelObject.cast<CoilWaterHeatingDesuperheater>();
         retVal = translateCoilWaterHeatingDesuperheater(coil);
@@ -3463,6 +3468,7 @@ namespace energyplus {
       IddObjectType::OS_Coil_Heating_Water,
       IddObjectType::OS_Coil_Heating_WaterToAirHeatPump_EquationFit,
       IddObjectType::OS_Coil_WaterHeating_Desuperheater,
+      // IddObjectType::OS_Coil_UserDefined,
 
       // If using a plantLoop, this will be translated by the PlantLoop. But WaterHeaters can also be used stand-alone, so always translate them
       // We'll check in their FT if the "Peak Use Flow Rate" is actually initialized as it's an indication that the WH was
diff --git a/src/energyplus/ForwardTranslator.hpp b/src/energyplus/ForwardTranslator.hpp
index d8f1792cd0f..6c119227043 100644
--- a/src/energyplus/ForwardTranslator.hpp
+++ b/src/energyplus/ForwardTranslator.hpp
@@ -159,6 +159,7 @@ namespace model {
   class CoilSystemCoolingDXHeatExchangerAssisted;
   class CoilSystemCoolingWaterHeatExchangerAssisted;
   class CoilSystemIntegratedHeatPumpAirSource;
+  class CoilUserDefined;
   class CoilWaterHeatingDesuperheater;
   class CoilWaterHeatingAirToWaterHeatPump;
   class CoilWaterHeatingAirToWaterHeatPumpVariableSpeed;
@@ -857,6 +858,8 @@ namespace energyplus {
 
     boost::optional<IdfObject> translateCoilSystemIntegratedHeatPumpAirSource(model::CoilSystemIntegratedHeatPumpAirSource& modelObject);
 
+    boost::optional<IdfObject> translateCoilUserDefined(model::CoilUserDefined& modelObject);
+
     boost::optional<IdfObject> translateCoilWaterHeatingDesuperheater(model::CoilWaterHeatingDesuperheater& modelObject);
 
     boost::optional<IdfObject> translateCoilWaterHeatingAirToWaterHeatPump(model::CoilWaterHeatingAirToWaterHeatPump& modelObject);
diff --git a/src/energyplus/ForwardTranslator/ForwardTranslateCoilUserDefined.cpp b/src/energyplus/ForwardTranslator/ForwardTranslateCoilUserDefined.cpp
new file mode 100644
index 00000000000..d37b1682135
--- /dev/null
+++ b/src/energyplus/ForwardTranslator/ForwardTranslateCoilUserDefined.cpp
@@ -0,0 +1,137 @@
+/***********************************************************************************************************************
+*  OpenStudio(R), Copyright (c) 2008-2022, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
+*
+*  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+*  following conditions are met:
+*
+*  (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+*  disclaimer.
+*
+*  (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+*  disclaimer in the documentation and/or other materials provided with the distribution.
+*
+*  (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
+*  derived from this software without specific prior written permission from the respective party.
+*
+*  (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
+*  may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
+*  written permission from Alliance for Sustainable Energy, LLC.
+*
+*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+*  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+*  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
+*  STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+*  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+*  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+*  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+***********************************************************************************************************************/
+
+#include "../ForwardTranslator.hpp"
+
+#include "../../model/CoilUserDefined.hpp"
+#include "../../model/CoilUserDefined_Impl.hpp"
+#include "../../model/Model.hpp"
+#include "../../model/Node.hpp"
+#include "../../model/Node_Impl.hpp"
+#include "../../model/ThermalZone.hpp"
+#include "../../model/ThermalZone_Impl.hpp"
+#include "../../model/EnergyManagementSystemProgramCallingManager.hpp"
+#include "../../model/EnergyManagementSystemProgram.hpp"
+#include "../../model/EnergyManagementSystemActuator.hpp"
+
+#include "../../utilities/core/Logger.hpp"
+#include "../../utilities/core/Assert.hpp"
+
+#include <utilities/idd/Coil_UserDefined_FieldEnums.hxx>
+#include <utilities/idd/IddEnums.hxx>
+
+using namespace openstudio::model;
+
+using namespace std;
+
+namespace openstudio {
+
+namespace energyplus {
+
+  boost::optional<IdfObject> ForwardTranslator::translateCoilUserDefined(CoilUserDefined& modelObject) {
+
+    if (modelObject.numberofAirConnections() == 0) {
+      LOG(Warn, modelObject.briefDescription() << "will not be translated, cannot find air connection.");
+      return boost::none;
+    }
+
+    IdfObject idfObject = createRegisterAndNameIdfObject(openstudio::IddObjectType::Coil_UserDefined, modelObject);
+
+    //PlantConnectionisUsed default value
+    idfObject.setString(Coil_UserDefinedFields::PlantConnectionisUsed, "No");
+
+    // PlantConnectionInletNodeName
+    bool plantUsed = false;
+    if (boost::optional<ModelObject> mo = modelObject.waterInletModelObject()) {
+      if (boost::optional<Node> node = mo->optionalCast<Node>()) {
+        idfObject.setString(Coil_UserDefinedFields::PlantConnectionInletNodeName, node->nameString());
+        //PlantConnectionisUsed to Yes since Node is connected
+        idfObject.setString(Coil_UserDefinedFields::PlantConnectionisUsed, "Yes");
+        plantUsed = true;
+      }
+    }
+
+    // PlantConnectionOutletNodeName
+
+    if (boost::optional<ModelObject> mo = modelObject.waterOutletModelObject()) {
+      if (boost::optional<Node> node = mo->optionalCast<Node>()) {
+        idfObject.setString(Coil_UserDefinedFields::PlantConnectionOutletNodeName, node->nameString());
+      }
+    }
+    // remove plant actuators if no plant connections
+    if (!plantUsed) {
+      modelObject.plantMinimumMassFlowRateActuator().remove();
+      modelObject.plantMaximumMassFlowRateActuator().remove();
+      modelObject.plantDesignVolumeFlowRateActuator().remove();
+      modelObject.plantMassFlowRateActuator().remove();
+      modelObject.plantOutletTemperatureActuator().remove();
+    }
+    // AirConnection1InletNodeName
+
+    if (boost::optional<ModelObject> mo = modelObject.airInletModelObject()) {
+      if (boost::optional<Node> node = mo->optionalCast<Node>()) {
+        idfObject.setString(Coil_UserDefinedFields::AirConnection1InletNodeName, node->nameString());
+      }
+    }
+
+    // AirConnection1OutletNodeName
+
+    if (boost::optional<ModelObject> mo = modelObject.airOutletModelObject()) {
+      if (boost::optional<Node> node = mo->optionalCast<Node>()) {
+        idfObject.setString(Coil_UserDefinedFields::AirConnection1OutletNodeName, node->nameString());
+      }
+    }
+
+    // NumberofAirConnections
+    idfObject.setInt(Coil_UserDefinedFields::NumberofAirConnections, modelObject.numberofAirConnections());
+
+    // OverallModelSimulationProgramCallingManagerName
+
+    if (boost::optional<EnergyManagementSystemProgramCallingManager> pcm = modelObject.overallModelSimulationProgramCallingManager()) {
+      idfObject.setString(Coil_UserDefinedFields::OverallModelSimulationProgramCallingManagerName, pcm->nameString());
+    }
+
+    // ModelSetupandSizingProgramCallingManagerName
+
+    if (boost::optional<EnergyManagementSystemProgramCallingManager> pcm = modelObject.modelSetupandSizingProgramCallingManager()) {
+      idfObject.setString(Coil_UserDefinedFields::ModelSetupandSizingProgramCallingManagerName, pcm->nameString());
+    }
+
+    // AmbientZoneName
+
+    if (boost::optional<ThermalZone> zone_ = modelObject.ambientZone()) {
+      idfObject.setString(Coil_UserDefinedFields::AmbientZoneName, zone_->nameString());
+    }
+
+    return idfObject;
+  }
+
+}  // namespace energyplus
+
+}  // namespace openstudio
diff --git a/src/energyplus/Test/CoilUserDefined_GTest.cpp b/src/energyplus/Test/CoilUserDefined_GTest.cpp
new file mode 100644
index 00000000000..e49a9004efa
--- /dev/null
+++ b/src/energyplus/Test/CoilUserDefined_GTest.cpp
@@ -0,0 +1,343 @@
+/***********************************************************************************************************************
+*  OpenStudio(R), Copyright (c) 2008-2022, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
+*
+*  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+*  following conditions are met:
+*
+*  (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+*  disclaimer.
+*
+*  (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+*  disclaimer in the documentation and/or other materials provided with the distribution.
+*
+*  (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
+*  derived from this software without specific prior written permission from the respective party.
+*
+*  (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
+*  may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
+*  written permission from Alliance for Sustainable Energy, LLC.
+*
+*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+*  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+*  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
+*  STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+*  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+*  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+*  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+***********************************************************************************************************************/
+
+#include <gtest/gtest.h>
+#include "EnergyPlusFixture.hpp"
+
+#include "../ForwardTranslator.hpp"
+
+#include "../../model/AirLoopHVAC.hpp"
+#include "../../model/AirLoopHVAC_Impl.hpp"
+#include "../../model/Model.hpp"
+#include "../../model/CoilUserDefined.hpp"
+#include "../../model/CoilUserDefined_Impl.hpp"
+#include "../../model/EnergyManagementSystemProgramCallingManager.hpp"
+#include "../../model/EnergyManagementSystemProgram.hpp"
+#include "../../model/EnergyManagementSystemActuator.hpp"
+#include "../../model/Node.hpp"
+#include "../../model/ThermalZone.hpp"
+#include "../../model/ThermalZone_Impl.hpp"
+
+#include <utilities/idd/Coil_UserDefined_FieldEnums.hxx>
+#include <utilities/idd/IddEnums.hxx>
+#include <utilities/idd/OS_EnergyManagementSystem_ProgramCallingManager_FieldEnums.hxx>
+#include <utilities/idd/EnergyManagementSystem_ProgramCallingManager_FieldEnums.hxx>
+#include <utilities/idd/OS_EnergyManagementSystem_Actuator_FieldEnums.hxx>
+#include <utilities/idd/EnergyManagementSystem_Actuator_FieldEnums.hxx>
+
+#include <array>
+#include <string_view>
+#include <utility>
+
+using namespace openstudio::energyplus;
+using namespace openstudio::model;
+using namespace openstudio;
+
+TEST_F(EnergyPlusFixture, ForwardTranslator_CoilUserDefined_model) {
+  Model model;
+
+  CoilUserDefined coil(model);
+
+  AirLoopHVAC airLoop(model);
+  Node supplyOutletNode = airLoop.supplyOutletNode();
+
+  coil.addToNode(supplyOutletNode);
+
+  PlantLoop plant(model);
+  plant.addDemandBranchForComponent(coil);
+
+  std::string air_inname = coil.airInletModelObject().get().nameString();
+  std::string air_outname = coil.airOutletModelObject().get().nameString();
+  std::string plant_inname = coil.waterInletModelObject().get().nameString();
+  std::string plant_outname = coil.waterOutletModelObject().get().nameString();
+
+  ForwardTranslator forwardTranslator;
+  Workspace workspace = forwardTranslator.translateModel(model);
+
+  EXPECT_EQ(0u, forwardTranslator.errors().size());
+  // check objects and children are translated
+  EXPECT_EQ(1u, workspace.getObjectsByType(IddObjectType::Coil_UserDefined).size());
+  EXPECT_EQ(2u, workspace.getObjectsByType(IddObjectType::EnergyManagementSystem_ProgramCallingManager).size());
+  EXPECT_EQ(2u, workspace.getObjectsByType(IddObjectType::EnergyManagementSystem_Program).size());
+  EXPECT_EQ(8u, workspace.getObjectsByType(IddObjectType::EnergyManagementSystem_Actuator).size());
+  // check actuators are setup
+  WorkspaceObjectVector actuators = workspace.getObjectsByType(IddObjectType::EnergyManagementSystem_Actuator);
+  EXPECT_EQ(8u, actuators.size());
+  for (const auto& actuator : actuators) {
+    EXPECT_EQ("Coil User Defined 1", actuator.getString(EnergyManagementSystem_ActuatorFields::ActuatedComponentUniqueName, false).get());
+    EXPECT_TRUE(actuator.getString(EnergyManagementSystem_ActuatorFields::ActuatedComponentType, false).get() == "Air Connection 1"
+                || actuator.getString(EnergyManagementSystem_ActuatorFields::ActuatedComponentType, false).get() == "Plant Connection");
+  }
+
+  WorkspaceObjectVector idf_coil(workspace.getObjectsByType(IddObjectType::Coil_UserDefined));
+  EXPECT_EQ(1u, idf_coil.size());
+  WorkspaceObject ws_coil(idf_coil[0]);
+  EXPECT_EQ("overallModelSimulationProgramCallingManager",
+            ws_coil.getString(Coil_UserDefinedFields::OverallModelSimulationProgramCallingManagerName, false).get());
+  EXPECT_EQ("Yes", ws_coil.getString(Coil_UserDefinedFields::PlantConnectionisUsed, false).get());
+  // check air connections
+  ASSERT_TRUE(ws_coil.getString(Coil_UserDefinedFields::AirConnection1InletNodeName, false));
+  EXPECT_EQ(air_inname, ws_coil.getString(Coil_UserDefinedFields::AirConnection1InletNodeName, false).get());
+  ASSERT_TRUE(ws_coil.getString(Coil_UserDefinedFields::AirConnection1OutletNodeName, false));
+  EXPECT_EQ(air_outname, ws_coil.getString(Coil_UserDefinedFields::AirConnection1OutletNodeName, false).get());
+
+  // check plant connections
+  ASSERT_TRUE(ws_coil.getString(Coil_UserDefinedFields::PlantConnectionInletNodeName, false));
+  EXPECT_EQ(plant_inname, ws_coil.getString(Coil_UserDefinedFields::PlantConnectionInletNodeName, false).get());
+  ASSERT_TRUE(ws_coil.getString(Coil_UserDefinedFields::PlantConnectionOutletNodeName, false));
+  EXPECT_EQ(plant_outname, ws_coil.getString(Coil_UserDefinedFields::PlantConnectionOutletNodeName, false).get());
+}
+
+TEST_F(EnergyPlusFixture, ForwardTranslator_CoilUserDefined_examplemodel) {
+  // Generate the example Model
+  Model model = openstudio::model::exampleModel();
+  // Get the single thermal Zone in the model
+  AirLoopHVAC airLoop = model.getConcreteModelObjects<AirLoopHVAC>()[0];
+  //OS:Coil:Cooling:DX:SingleSpeed
+  Node supplyOutletNode = airLoop.supplyOutletNode();
+
+  // Get the single thermal Zone in the model
+  ThermalZone z = model.getConcreteModelObjects<ThermalZone>()[0];
+  std::string tz_name = z.nameString();
+  CoilUserDefined coil(model);
+  coil.setAmbientZone(z);
+  coil.addToNode(supplyOutletNode);
+
+  PlantLoop plant(model);
+  plant.addDemandBranchForComponent(coil);
+
+  std::string air_inname = coil.airInletModelObject().get().nameString();
+  std::string air_outname = coil.airOutletModelObject().get().nameString();
+  std::string plant_inname = coil.waterInletModelObject().get().nameString();
+  std::string plant_outname = coil.waterOutletModelObject().get().nameString();
+
+  ForwardTranslator forwardTranslator;
+  Workspace workspace = forwardTranslator.translateModel(model);
+
+  EXPECT_EQ(0u, forwardTranslator.errors().size());
+  // check objects and children are translated
+  EXPECT_EQ(1u, workspace.getObjectsByType(IddObjectType::Coil_UserDefined).size());
+  EXPECT_EQ(2u, workspace.getObjectsByType(IddObjectType::EnergyManagementSystem_ProgramCallingManager).size());
+  EXPECT_EQ(2u, workspace.getObjectsByType(IddObjectType::EnergyManagementSystem_Program).size());
+  EXPECT_EQ(8u, workspace.getObjectsByType(IddObjectType::EnergyManagementSystem_Actuator).size());
+  // check actuators are setup
+  WorkspaceObjectVector actuators = workspace.getObjectsByType(IddObjectType::EnergyManagementSystem_Actuator);
+  EXPECT_EQ(8u, actuators.size());
+  for (const auto& actuator : actuators) {
+    EXPECT_EQ("Coil User Defined 1", actuator.getString(EnergyManagementSystem_ActuatorFields::ActuatedComponentUniqueName, false).get());
+    EXPECT_TRUE(actuator.getString(EnergyManagementSystem_ActuatorFields::ActuatedComponentType, false).get() == "Air Connection 1"
+                || actuator.getString(EnergyManagementSystem_ActuatorFields::ActuatedComponentType, false).get() == "Plant Connection");
+  }
+
+  WorkspaceObjectVector idf_coil(workspace.getObjectsByType(IddObjectType::Coil_UserDefined));
+  EXPECT_EQ(1u, idf_coil.size());
+  WorkspaceObject ws_coil(idf_coil[0]);
+  EXPECT_EQ("overallModelSimulationProgramCallingManager",
+            ws_coil.getString(Coil_UserDefinedFields::OverallModelSimulationProgramCallingManagerName, false).get());
+  EXPECT_EQ("Yes", ws_coil.getString(Coil_UserDefinedFields::PlantConnectionisUsed, false).get());
+  // check air connections
+  ASSERT_TRUE(ws_coil.getString(Coil_UserDefinedFields::AirConnection1InletNodeName, false));
+  EXPECT_EQ(air_inname, ws_coil.getString(Coil_UserDefinedFields::AirConnection1InletNodeName, false).get());
+  ASSERT_TRUE(ws_coil.getString(Coil_UserDefinedFields::AirConnection1OutletNodeName, false));
+  EXPECT_EQ(air_outname, ws_coil.getString(Coil_UserDefinedFields::AirConnection1OutletNodeName, false).get());
+
+  // check plant connections
+  ASSERT_TRUE(ws_coil.getString(Coil_UserDefinedFields::PlantConnectionInletNodeName, false));
+  EXPECT_EQ(plant_inname, ws_coil.getString(Coil_UserDefinedFields::PlantConnectionInletNodeName, false).get());
+  ASSERT_TRUE(ws_coil.getString(Coil_UserDefinedFields::PlantConnectionOutletNodeName, false));
+  EXPECT_EQ(plant_outname, ws_coil.getString(Coil_UserDefinedFields::PlantConnectionOutletNodeName, false).get());
+
+  // check ambient zone
+  ASSERT_TRUE(ws_coil.getString(Coil_UserDefinedFields::AmbientZoneName, false));
+  EXPECT_EQ(tz_name, ws_coil.getString(Coil_UserDefinedFields::AmbientZoneName, false).get());
+}
+
+TEST_F(EnergyPlusFixture, ForwardTranslator_CoilUserDefined_NoPlant) {
+  // Generate the example Model
+  Model model = openstudio::model::exampleModel();
+  // Get the single thermal Zone in the model
+  AirLoopHVAC airLoop = model.getConcreteModelObjects<AirLoopHVAC>()[0];
+  //OS:Coil:Cooling:DX:SingleSpeed
+  Node supplyOutletNode = airLoop.supplyOutletNode();
+
+  // Get the single thermal Zone in the model
+  ThermalZone z = model.getConcreteModelObjects<ThermalZone>()[0];
+  std::string tz_name = z.nameString();
+  CoilUserDefined coil(model);
+  coil.setAmbientZone(z);
+  coil.addToNode(supplyOutletNode);
+
+  std::string air_inname = coil.airInletModelObject().get().nameString();
+  std::string air_outname = coil.airOutletModelObject().get().nameString();
+
+  ForwardTranslator forwardTranslator;
+  Workspace workspace = forwardTranslator.translateModel(model);
+
+  EXPECT_EQ(0u, forwardTranslator.errors().size());
+  // check objects and children are translated
+  EXPECT_EQ(1u, workspace.getObjectsByType(IddObjectType::Coil_UserDefined).size());
+  EXPECT_EQ(2u, workspace.getObjectsByType(IddObjectType::EnergyManagementSystem_ProgramCallingManager).size());
+  EXPECT_EQ(2u, workspace.getObjectsByType(IddObjectType::EnergyManagementSystem_Program).size());
+  EXPECT_EQ(3u, workspace.getObjectsByType(IddObjectType::EnergyManagementSystem_Actuator).size());
+  // check actuators are setup
+  WorkspaceObjectVector actuators = workspace.getObjectsByType(IddObjectType::EnergyManagementSystem_Actuator);
+  EXPECT_EQ(3u, actuators.size());
+  for (const auto& actuator : actuators) {
+    EXPECT_EQ("Coil User Defined 1", actuator.getString(EnergyManagementSystem_ActuatorFields::ActuatedComponentUniqueName, false).get());
+    EXPECT_TRUE(actuator.getString(EnergyManagementSystem_ActuatorFields::ActuatedComponentType, false).get() == "Air Connection 1");
+  }
+  // model.save("CoilUserDefined_constructor.osm", true);
+  // workspace.save("CoilUserDefined_constructor.idf", true);
+}
+
+TEST_F(EnergyPlusFixture, ForwardTranslator_CoilUserDefined_setters) {
+  Model model;
+
+  CoilUserDefined coil(model);
+
+  AirLoopHVAC airLoop(model);
+  Node supplyOutletNode = airLoop.supplyOutletNode();
+
+  coil.addToNode(supplyOutletNode);
+
+  PlantLoop plant(model);
+  plant.addDemandBranchForComponent(coil);
+
+  // make new actuators to test setters and remove the old ones
+  coil.airOutletTemperatureActuator().remove();
+  EnergyManagementSystemActuator aota(coil, "Air Connection 1", "Outlet Temperature");
+  aota.setName("airOutletTemperature new");
+  EXPECT_TRUE(coil.setAirOutletTemperatureActuator(aota));
+
+  coil.airOutletHumidityRatioActuator().remove();
+  EnergyManagementSystemActuator aohra(coil, "Air Connection 1", "Outlet Humidity Ratio");
+  aohra.setName("airOutletHumidityRatio new");
+  EXPECT_TRUE(coil.setAirOutletHumidityRatioActuator(aohra));
+
+  coil.airMassFlowRateActuator().remove();
+  EnergyManagementSystemActuator amfra(coil, "Air Connection 1", "Mass Flow Rate");
+  amfra.setName("airMassFlowRate new");
+  EXPECT_TRUE(coil.setAirMassFlowRateActuator(amfra));
+
+  coil.plantMinimumMassFlowRateActuator().remove();
+  EnergyManagementSystemActuator pminmfra(coil, "Plant Connection", "Minimum Mass Flow Rate");
+  pminmfra.setName("plantMinimumMassFlowRate new");
+  EXPECT_TRUE(coil.setPlantMinimumMassFlowRateActuator(pminmfra));
+
+  coil.plantMaximumMassFlowRateActuator().remove();
+  EnergyManagementSystemActuator pmaxmfra(coil, "Plant Connection", "Maximum Mass Flow Rate");
+  pmaxmfra.setName("plantMaximumMassFlowRate new");
+  EXPECT_TRUE(coil.setPlantMaximumMassFlowRateActuator(pmaxmfra));
+
+  coil.plantDesignVolumeFlowRateActuator().remove();
+  EnergyManagementSystemActuator pdvfra(coil, "Plant Connection", "Design Volume Flow Rate");
+  pdvfra.setName("plantDesignVolumeFlowRate new");
+  EXPECT_TRUE(coil.setPlantDesignVolumeFlowRateActuator(pdvfra));
+
+  coil.plantMassFlowRateActuator().remove();
+  EnergyManagementSystemActuator pota(coil, "Plant Connection", "Mass Flow Rate");
+  pota.setName("plantMassFlowRate new");
+  EXPECT_TRUE(coil.setPlantMassFlowRateActuator(pota));
+
+  coil.plantOutletTemperatureActuator().remove();
+  EnergyManagementSystemActuator pmmfra(coil, "Plant Connection", "Outlet Temperature");
+  pmmfra.setName("plantOutletTemperature new");
+  EXPECT_TRUE(coil.setPlantOutletTemperatureActuator(pmmfra));
+
+  //use setters for program and programmanager
+  coil.overallModelSimulationProgramCallingManager().remove();
+  coil.overallSimulationProgram().remove();
+  EnergyManagementSystemProgram overAll(model);
+  overAll.setName("overAllNew");
+  EnergyManagementSystemProgramCallingManager overAllmgr(model);
+  overAllmgr.setName("overAllmgrNew");
+  overAllmgr.addProgram(overAll);
+  EXPECT_TRUE(coil.setOverallModelSimulationProgramCallingManager(overAllmgr));
+  EXPECT_TRUE(coil.setOverallSimulationProgram(overAll));
+
+  coil.modelSetupandSizingProgramCallingManager().remove();
+  coil.initializationSimulationProgram().remove();
+  EnergyManagementSystemProgram init(model);
+  init.setName("initNew");
+  EnergyManagementSystemProgramCallingManager initmgr(model);
+  initmgr.setName("initmgrNew");
+  initmgr.addProgram(init);
+  EXPECT_TRUE(coil.setModelSetupandSizingProgramCallingManager(initmgr));
+  EXPECT_TRUE(coil.setInitializationSimulationProgram(init));
+
+  ForwardTranslator forwardTranslator;
+  Workspace workspace = forwardTranslator.translateModel(model);
+
+  EXPECT_EQ(0u, forwardTranslator.errors().size());
+  // check objects and children are translated
+  EXPECT_EQ(1u, workspace.getObjectsByType(IddObjectType::Coil_UserDefined).size());
+  EXPECT_EQ(2u, workspace.getObjectsByType(IddObjectType::EnergyManagementSystem_ProgramCallingManager).size());
+  EXPECT_EQ(2u, workspace.getObjectsByType(IddObjectType::EnergyManagementSystem_Program).size());
+  EXPECT_EQ(8u, workspace.getObjectsByType(IddObjectType::EnergyManagementSystem_Actuator).size());
+  // check actuators are setup
+  WorkspaceObjectVector actuators = workspace.getObjectsByType(IddObjectType::EnergyManagementSystem_Actuator);
+  EXPECT_EQ(8u, actuators.size());
+
+  constexpr std::array<std::pair<std::string_view, std::string_view>, 8> expectedValues{{
+    {"airOutletTemperature_new", "Air Connection 1"},
+    {"airOutletHumidityRatio_new", "Air Connection 1"},
+    {"airMassFlowRate_new", "Air Connection 1"},
+    {"plantMinimumMassFlowRate_new", "Plant Connection"},
+    {"plantMaximumMassFlowRate_new", "Plant Connection"},
+    {"plantDesignVolumeFlowRate_new", "Plant Connection"},
+    {"plantMassFlowRate_new", "Plant Connection"},
+    {"plantOutletTemperature_new", "Plant Connection"},
+  }};
+
+  std::vector<std::string> actuatedNames;
+  std::transform(actuators.cbegin(), actuators.cend(), std::back_inserter(actuatedNames),
+                 [](const auto& wo) { return wo.getString(EnergyManagementSystem_ActuatorFields::ActuatedComponentUniqueName).get(); });
+  std::sort(actuatedNames.begin(), actuatedNames.end());
+
+  for (const auto& actuator : actuators) {
+    EXPECT_EQ("Coil User Defined 1", actuator.getString(EnergyManagementSystem_ActuatorFields::ActuatedComponentUniqueName, false).get());
+
+    auto actuatorName = actuator.getString(EnergyManagementSystem_ActuatorFields::Name, false).get();
+
+    auto it = std::find_if(expectedValues.cbegin(), expectedValues.cend(), [&actuatorName](const auto& k) { return k.first == actuatorName; });
+    ASSERT_FALSE(it == expectedValues.end()) << actuatorName << " not found";
+
+    auto actuatedCompType = actuator.getString(EnergyManagementSystem_ActuatorFields::ActuatedComponentType, false).get();
+    EXPECT_EQ(it->second, actuatedCompType);
+  }
+
+  WorkspaceObjectVector idf_coil(workspace.getObjectsByType(IddObjectType::Coil_UserDefined));
+  EXPECT_EQ(1u, idf_coil.size());
+  WorkspaceObject ws_coil(idf_coil[0]);
+  EXPECT_EQ("overAllmgrNew", ws_coil.getString(Coil_UserDefinedFields::OverallModelSimulationProgramCallingManagerName, false).get());
+  EXPECT_EQ("initmgrNew", ws_coil.getString(Coil_UserDefinedFields::ModelSetupandSizingProgramCallingManagerName, false).get());
+
+  // model.save("CoilUserDefined_constructor.osm", true);
+  // workspace.save("CoilUserDefined_constructor.idf", true);
+}
diff --git a/src/model/CMakeLists.txt b/src/model/CMakeLists.txt
index 2eb20890f63..92c864581a6 100644
--- a/src/model/CMakeLists.txt
+++ b/src/model/CMakeLists.txt
@@ -449,6 +449,9 @@ set(${target_name}_src
   CoilSystemIntegratedHeatPumpAirSource.hpp
   CoilSystemIntegratedHeatPumpAirSource_Impl.hpp
   CoilSystemIntegratedHeatPumpAirSource.cpp
+  CoilUserDefined.hpp
+  CoilUserDefined_Impl.hpp
+  CoilUserDefined.cpp
   CoilWaterHeatingAirToWaterHeatPump.hpp
   CoilWaterHeatingAirToWaterHeatPump_Impl.hpp
   CoilWaterHeatingAirToWaterHeatPump.cpp
@@ -1981,10 +1984,11 @@ set(${target_name}_test_src
   test/CoilHeatingWaterToAirHeatPumpVariableSpeedEquationFitSpeedData_GTest.cpp
   test/CoilHeatingWaterBaseboard_GTest.cpp
   test/CoilHeatingWaterBaseboardRadiant_GTest.cpp
+  test/CoilPerformanceDXCooling_GTest.cpp
   test/CoilSystemCoolingDXHeatExchangerAssisted_GTest.cpp
   test/CoilSystemCoolingWaterHeatExchangerAssisted_GTest.cpp
   test/CoilSystemIntegratedHeatPumpAirSource_GTest.cpp
-  test/CoilPerformanceDXCooling_GTest.cpp
+  test/CoilUserDefined_GTest.cpp
   test/CoilWaterHeatingAirToWaterHeatPump_GTest.cpp
   test/CoilWaterHeatingAirToWaterHeatPumpVariableSpeed_GTest.cpp
   test/CoilWaterHeatingAirToWaterHeatPumpVariableSpeedSpeedData_GTest.cpp
diff --git a/src/model/CoilUserDefined.cpp b/src/model/CoilUserDefined.cpp
new file mode 100644
index 00000000000..24d9dad2394
--- /dev/null
+++ b/src/model/CoilUserDefined.cpp
@@ -0,0 +1,700 @@
+/***********************************************************************************************************************
+*  OpenStudio(R), Copyright (c) 2008-2022, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
+*
+*  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+*  following conditions are met:
+*
+*  (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+*  disclaimer.
+*
+*  (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+*  disclaimer in the documentation and/or other materials provided with the distribution.
+*
+*  (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
+*  derived from this software without specific prior written permission from the respective party.
+*
+*  (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
+*  may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
+*  written permission from Alliance for Sustainable Energy, LLC.
+*
+*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+*  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+*  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
+*  STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+*  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+*  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+*  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+***********************************************************************************************************************/
+
+#include "CoilUserDefined.hpp"
+#include "CoilUserDefined_Impl.hpp"
+
+#include "EnergyManagementSystemActuator.hpp"
+#include "EnergyManagementSystemActuator_Impl.hpp"
+#include "EnergyManagementSystemProgram.hpp"
+#include "EnergyManagementSystemProgram_Impl.hpp"
+#include "EnergyManagementSystemProgramCallingManager.hpp"
+#include "EnergyManagementSystemProgramCallingManager_Impl.hpp"
+#include "ThermalZone.hpp"
+#include "ThermalZone_Impl.hpp"
+#include "AirLoopHVACUnitarySystem.hpp"
+#include "AirLoopHVACUnitarySystem_Impl.hpp"
+#include "Model.hpp"
+#include "Model_Impl.hpp"
+
+#include "../utilities/core/Assert.hpp"
+
+#include <algorithm>
+#include <utilities/idd/IddEnums.hxx>
+#include <utilities/idd/OS_Coil_UserDefined_FieldEnums.hxx>
+
+namespace openstudio {
+namespace model {
+
+  namespace detail {
+
+    CoilUserDefined_Impl::CoilUserDefined_Impl(const IdfObject& idfObject, Model_Impl* model, bool keepHandle)
+      : WaterToAirComponent_Impl(idfObject, model, keepHandle) {
+      OS_ASSERT(idfObject.iddObject().type() == CoilUserDefined::iddObjectType());
+    }
+
+    CoilUserDefined_Impl::CoilUserDefined_Impl(const openstudio::detail::WorkspaceObject_Impl& other, Model_Impl* model, bool keepHandle)
+      : WaterToAirComponent_Impl(other, model, keepHandle) {
+      OS_ASSERT(other.iddObject().type() == CoilUserDefined::iddObjectType());
+    }
+
+    CoilUserDefined_Impl::CoilUserDefined_Impl(const CoilUserDefined_Impl& other, Model_Impl* model, bool keepHandle)
+      : WaterToAirComponent_Impl(other, model, keepHandle) {}
+
+    const std::vector<std::string>& CoilUserDefined_Impl::outputVariableNames() const {
+      static std::vector<std::string> result;
+      return result;
+    }
+
+    boost::optional<HVACComponent> CoilUserDefined_Impl::containingHVACComponent() const {
+      // AirLoopHVACUnitarySystem is the only accepted type, but the coil can be used as any of the three HC, CC or suppHC
+      for (const auto& airLoopHVACUnitarySystem : this->model().getConcreteModelObjects<AirLoopHVACUnitarySystem>()) {
+        if (boost::optional<HVACComponent> coolingCoil = airLoopHVACUnitarySystem.coolingCoil()) {
+          if (coolingCoil->handle() == this->handle()) {
+            return airLoopHVACUnitarySystem;
+          }
+        } else if (boost::optional<HVACComponent> coolingCoil = airLoopHVACUnitarySystem.heatingCoil()) {
+          if (coolingCoil->handle() == this->handle()) {
+            return airLoopHVACUnitarySystem;
+          }
+        } else if (boost::optional<HVACComponent> coolingCoil = airLoopHVACUnitarySystem.supplementalHeatingCoil()) {
+          if (coolingCoil->handle() == this->handle()) {
+            return airLoopHVACUnitarySystem;
+          }
+        }
+      }
+      return boost::none;
+    }
+
+    IddObjectType CoilUserDefined_Impl::iddObjectType() const {
+      return CoilUserDefined::iddObjectType();
+    }
+
+    unsigned CoilUserDefined_Impl::airInletPort() const {
+      return OS_Coil_UserDefinedFields::AirConnection1InletNodeName;
+    }
+
+    unsigned CoilUserDefined_Impl::airOutletPort() const {
+      return OS_Coil_UserDefinedFields::AirConnection1OutletNodeName;
+    }
+
+    unsigned CoilUserDefined_Impl::waterInletPort() const {
+      return OS_Coil_UserDefinedFields::PlantConnectionInletNodeName;
+    }
+
+    unsigned CoilUserDefined_Impl::waterOutletPort() const {
+      return OS_Coil_UserDefinedFields::PlantConnectionOutletNodeName;
+    }
+
+    std::vector<IddObjectType> CoilUserDefined_Impl::allowableChildTypes() const {
+      std::vector<IddObjectType> result{
+        IddObjectType::OS_EnergyManagementSystem_ProgramCallingManager,
+        IddObjectType::OS_EnergyManagementSystem_Program,
+        IddObjectType::OS_EnergyManagementSystem_Actuator,
+        IddObjectType::OS_Coil_UserDefined,
+      };
+      return result;
+    }
+
+    // Returns the children object
+    std::vector<ModelObject> CoilUserDefined_Impl::children() const {
+      std::vector<ModelObject> result;
+      if (boost::optional<EnergyManagementSystemProgramCallingManager> omsmpcm = optionalOverallModelSimulationProgramCallingManager()) {
+        result.push_back(omsmpcm.get());
+      }
+      if (boost::optional<EnergyManagementSystemProgramCallingManager> msspcm = optionalModelSetupandSizingProgramCallingManager()) {
+        result.push_back(msspcm.get());
+      }
+      if (boost::optional<EnergyManagementSystemProgram> osp = optionalOverallSimulationProgram()) {
+        result.push_back(osp.get());
+      }
+      if (boost::optional<EnergyManagementSystemProgram> isp = optionalInitializationSimulationProgram()) {
+        result.push_back(isp.get());
+      }
+      if (boost::optional<EnergyManagementSystemActuator> aota = optionalAirOutletTemperatureActuator()) {
+        result.push_back(aota.get());
+      }
+      if (boost::optional<EnergyManagementSystemActuator> aohra = optionalAirOutletHumidityRatioActuator()) {
+        result.push_back(aohra.get());
+      }
+      if (boost::optional<EnergyManagementSystemActuator> amfra = optionalAirMassFlowRateActuator()) {
+        result.push_back(amfra.get());
+      }
+      if (boost::optional<EnergyManagementSystemActuator> pminmfra = optionalPlantMinimumMassFlowRateActuator()) {
+        result.push_back(pminmfra.get());
+      }
+      if (boost::optional<EnergyManagementSystemActuator> pmaxmfra = optionalPlantMaximumMassFlowRateActuator()) {
+        result.push_back(pmaxmfra.get());
+      }
+      if (boost::optional<EnergyManagementSystemActuator> pdvfra = optionalPlantDesignVolumeFlowRateActuator()) {
+        result.push_back(pdvfra.get());
+      }
+      if (boost::optional<EnergyManagementSystemActuator> pmmfra = optionalPlantMassFlowRateActuator()) {
+        result.push_back(pmmfra.get());
+      }
+      if (boost::optional<EnergyManagementSystemActuator> pota = optionalPlantOutletTemperatureActuator()) {
+        result.push_back(pota.get());
+      }
+      return result;
+    }
+
+    ModelObject CoilUserDefined_Impl::clone(Model model) const {
+      auto newCoilUserDefined = ModelObject_Impl::clone(model).cast<CoilUserDefined>();  // NOLINT(bugprone-parent-virtual-call
+      //airOutletTemperatureActuator
+      if (boost::optional<EnergyManagementSystemActuator> object = optionalAirOutletTemperatureActuator()) {
+        auto objectClone = object.get().clone(model).cast<EnergyManagementSystemActuator>();
+        objectClone.setActuatedComponent(newCoilUserDefined);
+        newCoilUserDefined.setAirOutletTemperatureActuator(objectClone);
+      }
+      //airOutletHumidityRatioActuator
+      if (boost::optional<EnergyManagementSystemActuator> object = optionalAirOutletHumidityRatioActuator()) {
+        auto objectClone = object.get().clone(model).cast<EnergyManagementSystemActuator>();
+        objectClone.setActuatedComponent(newCoilUserDefined);
+        newCoilUserDefined.setAirOutletHumidityRatioActuator(objectClone);
+      }
+      //airMassFlowRateActuator
+      if (boost::optional<EnergyManagementSystemActuator> object = optionalAirMassFlowRateActuator()) {
+        auto objectClone = object.get().clone(model).cast<EnergyManagementSystemActuator>();
+        objectClone.setActuatedComponent(newCoilUserDefined);
+        newCoilUserDefined.setAirMassFlowRateActuator(objectClone);
+      }
+      //plantMinimumMassFlowRateActuator
+      if (boost::optional<EnergyManagementSystemActuator> object = optionalPlantMinimumMassFlowRateActuator()) {
+        auto objectClone = object.get().clone(model).cast<EnergyManagementSystemActuator>();
+        objectClone.setActuatedComponent(newCoilUserDefined);
+        newCoilUserDefined.setPlantMinimumMassFlowRateActuator(objectClone);
+      }
+      //plantMaximumMassFlowRateActuator
+      if (boost::optional<EnergyManagementSystemActuator> object = optionalPlantMaximumMassFlowRateActuator()) {
+        auto objectClone = object.get().clone(model).cast<EnergyManagementSystemActuator>();
+        objectClone.setActuatedComponent(newCoilUserDefined);
+        newCoilUserDefined.setPlantMaximumMassFlowRateActuator(objectClone);
+      }
+      //plantDesignVolumeFlowRateActuator
+      if (boost::optional<EnergyManagementSystemActuator> object = optionalPlantDesignVolumeFlowRateActuator()) {
+        auto objectClone = object.get().clone(model).cast<EnergyManagementSystemActuator>();
+        objectClone.setActuatedComponent(newCoilUserDefined);
+        newCoilUserDefined.setPlantDesignVolumeFlowRateActuator(objectClone);
+      }
+      //plantMassFlowRateActuator
+      if (boost::optional<EnergyManagementSystemActuator> object = optionalPlantMassFlowRateActuator()) {
+        auto objectClone = object.get().clone(model).cast<EnergyManagementSystemActuator>();
+        objectClone.setActuatedComponent(newCoilUserDefined);
+        newCoilUserDefined.setPlantMassFlowRateActuator(objectClone);
+      }
+      //plantOutletTemperatureActuator
+      if (boost::optional<EnergyManagementSystemActuator> object = optionalPlantOutletTemperatureActuator()) {
+        auto objectClone = object.get().clone(model).cast<EnergyManagementSystemActuator>();
+        objectClone.setActuatedComponent(newCoilUserDefined);
+        newCoilUserDefined.setPlantOutletTemperatureActuator(objectClone);
+      }
+      //overallModelSimulationProgramCallingManager
+      if (boost::optional<EnergyManagementSystemProgramCallingManager> object = optionalOverallModelSimulationProgramCallingManager()) {
+        auto objectClone = object.get().clone(model).cast<EnergyManagementSystemProgramCallingManager>();
+        //clone the program referenced in the callingManager
+        if (boost::optional<EnergyManagementSystemProgram> program = optionalOverallSimulationProgram()) {
+          auto cloneProgram = program.get().clone(model).cast<EnergyManagementSystemProgram>();
+          //add cloned program to cloned PCUD
+          newCoilUserDefined.setOverallSimulationProgram(cloneProgram);
+          //add cloned program to cloned programCallingManager
+          objectClone.erasePrograms();
+          objectClone.addProgram(cloneProgram);
+        }
+        newCoilUserDefined.setOverallModelSimulationProgramCallingManager(objectClone);
+      }
+      //modelSetupandSizingProgramCallingManager
+      if (boost::optional<EnergyManagementSystemProgramCallingManager> object = optionalModelSetupandSizingProgramCallingManager()) {
+        auto objectClone = object.get().clone(model).cast<EnergyManagementSystemProgramCallingManager>();
+        //clone the program referenced in the callingManager
+        if (boost::optional<EnergyManagementSystemProgram> program = optionalInitializationSimulationProgram()) {
+          auto cloneProgram = program.get().clone(model).cast<EnergyManagementSystemProgram>();
+          //add cloned program to cloned PCUD
+          newCoilUserDefined.setInitializationSimulationProgram(cloneProgram);
+          //add cloned program to cloned programCallingManager
+          objectClone.erasePrograms();
+          objectClone.addProgram(cloneProgram);
+        }
+        newCoilUserDefined.setModelSetupandSizingProgramCallingManager(objectClone);
+      }
+      // ambientZone
+      newCoilUserDefined.resetAmbientZone();
+
+      return std::move(newCoilUserDefined);
+    }
+
+    EnergyManagementSystemProgramCallingManager CoilUserDefined_Impl::overallModelSimulationProgramCallingManager() const {
+      boost::optional<EnergyManagementSystemProgramCallingManager> value = optionalOverallModelSimulationProgramCallingManager();
+      if (!value) {
+        LOG_AND_THROW(briefDescription() << " does not have an Overall Model Simulation Program Calling Manager attached.");
+      }
+      return value.get();
+    }
+
+    EnergyManagementSystemProgramCallingManager CoilUserDefined_Impl::modelSetupandSizingProgramCallingManager() const {
+      boost::optional<EnergyManagementSystemProgramCallingManager> value = optionalModelSetupandSizingProgramCallingManager();
+      if (!value) {
+        LOG_AND_THROW(briefDescription() << " does not have an Model Setupand Sizing Program Calling Manager attached.");
+      }
+      return value.get();
+    }
+
+    int CoilUserDefined_Impl::numberofAirConnections() const {
+      if (airLoopHVAC() || containingHVACComponent()) {
+        return 1;
+      }
+      return 0;
+    }
+
+    boost::optional<ThermalZone> CoilUserDefined_Impl::ambientZone() const {
+      return getObject<ModelObject>().getModelObjectTarget<ThermalZone>(OS_Coil_UserDefinedFields::AmbientZoneName);
+    }
+
+    EnergyManagementSystemProgram CoilUserDefined_Impl::overallSimulationProgram() const {
+      boost::optional<EnergyManagementSystemProgram> value = optionalOverallSimulationProgram();
+      if (!value) {
+        LOG_AND_THROW(briefDescription() << " does not have an Overall Simulation Program attached.");
+      }
+      return value.get();
+    }
+
+    EnergyManagementSystemProgram CoilUserDefined_Impl::initializationSimulationProgram() const {
+      boost::optional<EnergyManagementSystemProgram> value = optionalInitializationSimulationProgram();
+      if (!value) {
+        LOG_AND_THROW(briefDescription() << " does not have an Initialization Simulation Program attached.");
+      }
+      return value.get();
+    }
+
+    EnergyManagementSystemActuator CoilUserDefined_Impl::airOutletTemperatureActuator() const {
+      boost::optional<EnergyManagementSystemActuator> value = optionalAirOutletTemperatureActuator();
+      if (!value) {
+        LOG_AND_THROW(briefDescription() << " does not have an Air Outlet Temperature Actuator attached.");
+      }
+      return value.get();
+    }
+
+    EnergyManagementSystemActuator CoilUserDefined_Impl::airOutletHumidityRatioActuator() const {
+      boost::optional<EnergyManagementSystemActuator> value = optionalAirOutletHumidityRatioActuator();
+      if (!value) {
+        LOG_AND_THROW(briefDescription() << " does not have an Air Outlet Humidity Ratio Actuator attached.");
+      }
+      return value.get();
+    }
+
+    EnergyManagementSystemActuator CoilUserDefined_Impl::airMassFlowRateActuator() const {
+      boost::optional<EnergyManagementSystemActuator> value = optionalAirMassFlowRateActuator();
+      if (!value) {
+        LOG_AND_THROW(briefDescription() << " does not have an Air Mass Flow Rate Actuator attached.");
+      }
+      return value.get();
+    }
+
+    EnergyManagementSystemActuator CoilUserDefined_Impl::plantMinimumMassFlowRateActuator() const {
+      boost::optional<EnergyManagementSystemActuator> value = optionalPlantMinimumMassFlowRateActuator();
+      if (!value) {
+        LOG_AND_THROW(briefDescription() << " does not have an Plant Minimum Mass Flow Rate Actuator attached.");
+      }
+      return value.get();
+    }
+
+    EnergyManagementSystemActuator CoilUserDefined_Impl::plantMaximumMassFlowRateActuator() const {
+      boost::optional<EnergyManagementSystemActuator> value = optionalPlantMaximumMassFlowRateActuator();
+      if (!value) {
+        LOG_AND_THROW(briefDescription() << " does not have an Plant Maximum Mass Flow Rate Actuator attached.");
+      }
+      return value.get();
+    }
+
+    EnergyManagementSystemActuator CoilUserDefined_Impl::plantDesignVolumeFlowRateActuator() const {
+      boost::optional<EnergyManagementSystemActuator> value = optionalPlantDesignVolumeFlowRateActuator();
+      if (!value) {
+        LOG_AND_THROW(briefDescription() << " does not have an Plant Design Volume Flow Rate Actuator attached.");
+      }
+      return value.get();
+    }
+
+    EnergyManagementSystemActuator CoilUserDefined_Impl::plantOutletTemperatureActuator() const {
+      boost::optional<EnergyManagementSystemActuator> value = optionalPlantOutletTemperatureActuator();
+      if (!value) {
+        LOG_AND_THROW(briefDescription() << " does not have an Plant Outlet Temperature Actuator attached.");
+      }
+      return value.get();
+    }
+
+    EnergyManagementSystemActuator CoilUserDefined_Impl::plantMassFlowRateActuator() const {
+      boost::optional<EnergyManagementSystemActuator> value = optionalPlantMassFlowRateActuator();
+      if (!value) {
+        LOG_AND_THROW(briefDescription() << " does not have an Plant Mass Flow Rate Actuator attached.");
+      }
+      return value.get();
+    }
+
+    bool CoilUserDefined_Impl::setOverallModelSimulationProgramCallingManager(
+      const EnergyManagementSystemProgramCallingManager& emsProgramCallingManager) {
+      const bool result = setPointer(OS_Coil_UserDefinedFields::OverallModelSimulationProgramCallingManagerName, emsProgramCallingManager.handle());
+      return result;
+    }
+
+    bool
+      CoilUserDefined_Impl::setModelSetupandSizingProgramCallingManager(const EnergyManagementSystemProgramCallingManager& emsProgramCallingManager) {
+      const bool result = setPointer(OS_Coil_UserDefinedFields::ModelSetupandSizingProgramCallingManagerName, emsProgramCallingManager.handle());
+      return result;
+    }
+
+    bool CoilUserDefined_Impl::setAmbientZone(const ThermalZone& thermalZone) {
+      const bool result = setPointer(OS_Coil_UserDefinedFields::AmbientZoneName, thermalZone.handle());
+      return result;
+    }
+
+    void CoilUserDefined_Impl::resetAmbientZone() {
+      const bool result = setString(OS_Coil_UserDefinedFields::AmbientZoneName, "");
+      OS_ASSERT(result);
+    }
+
+    bool CoilUserDefined_Impl::setOverallSimulationProgram(const EnergyManagementSystemProgram& emsProgram) {
+      const bool result = setPointer(OS_Coil_UserDefinedFields::OverallSimulationProgramName, emsProgram.handle());
+      return result;
+    }
+
+    bool CoilUserDefined_Impl::setInitializationSimulationProgram(const EnergyManagementSystemProgram& emsProgram) {
+      const bool result = setPointer(OS_Coil_UserDefinedFields::InitializationSimulationProgramName, emsProgram.handle());
+      return result;
+    }
+
+    bool CoilUserDefined_Impl::setAirOutletTemperatureActuator(const EnergyManagementSystemActuator& emsActuator) {
+      const bool result = setPointer(OS_Coil_UserDefinedFields::AirOutletTemperatureActuator, emsActuator.handle());
+      return result;
+    }
+
+    bool CoilUserDefined_Impl::setAirOutletHumidityRatioActuator(const EnergyManagementSystemActuator& emsActuator) {
+      const bool result = setPointer(OS_Coil_UserDefinedFields::AirOutletHumidityRatioActuator, emsActuator.handle());
+      return result;
+    }
+
+    bool CoilUserDefined_Impl::setAirMassFlowRateActuator(const EnergyManagementSystemActuator& emsActuator) {
+      const bool result = setPointer(OS_Coil_UserDefinedFields::AirMassFlowRateActuator, emsActuator.handle());
+      return result;
+    }
+
+    bool CoilUserDefined_Impl::setPlantMinimumMassFlowRateActuator(const EnergyManagementSystemActuator& emsActuator) {
+      const bool result = setPointer(OS_Coil_UserDefinedFields::PlantMinimumMassFlowRateActuator, emsActuator.handle());
+      return result;
+    }
+
+    bool CoilUserDefined_Impl::setPlantMaximumMassFlowRateActuator(const EnergyManagementSystemActuator& emsActuator) {
+      const bool result = setPointer(OS_Coil_UserDefinedFields::PlantMaximumMassFlowRateActuator, emsActuator.handle());
+      return result;
+    }
+
+    bool CoilUserDefined_Impl::setPlantDesignVolumeFlowRateActuator(const EnergyManagementSystemActuator& emsActuator) {
+      const bool result = setPointer(OS_Coil_UserDefinedFields::PlantDesignVolumeFlowRateActuator, emsActuator.handle());
+      return result;
+    }
+
+    bool CoilUserDefined_Impl::setPlantOutletTemperatureActuator(const EnergyManagementSystemActuator& emsActuator) {
+      const bool result = setPointer(OS_Coil_UserDefinedFields::PlantOutletTemperatureActuator, emsActuator.handle());
+      return result;
+    }
+
+    bool CoilUserDefined_Impl::setPlantMassFlowRateActuator(const EnergyManagementSystemActuator& emsActuator) {
+      const bool result = setPointer(OS_Coil_UserDefinedFields::PlantMassFlowRateActuator, emsActuator.handle());
+      return result;
+    }
+
+    boost::optional<EnergyManagementSystemProgramCallingManager> CoilUserDefined_Impl::optionalOverallModelSimulationProgramCallingManager() const {
+      return getObject<ModelObject>().getModelObjectTarget<EnergyManagementSystemProgramCallingManager>(
+        OS_Coil_UserDefinedFields::OverallModelSimulationProgramCallingManagerName);
+    }
+
+    boost::optional<EnergyManagementSystemProgramCallingManager> CoilUserDefined_Impl::optionalModelSetupandSizingProgramCallingManager() const {
+      return getObject<ModelObject>().getModelObjectTarget<EnergyManagementSystemProgramCallingManager>(
+        OS_Coil_UserDefinedFields::ModelSetupandSizingProgramCallingManagerName);
+    }
+
+    boost::optional<EnergyManagementSystemProgram> CoilUserDefined_Impl::optionalOverallSimulationProgram() const {
+      return getObject<ModelObject>().getModelObjectTarget<EnergyManagementSystemProgram>(OS_Coil_UserDefinedFields::OverallSimulationProgramName);
+    }
+
+    boost::optional<EnergyManagementSystemProgram> CoilUserDefined_Impl::optionalInitializationSimulationProgram() const {
+      return getObject<ModelObject>().getModelObjectTarget<EnergyManagementSystemProgram>(
+        OS_Coil_UserDefinedFields::InitializationSimulationProgramName);
+    }
+
+    boost::optional<EnergyManagementSystemActuator> CoilUserDefined_Impl::optionalAirOutletTemperatureActuator() const {
+      return getObject<ModelObject>().getModelObjectTarget<EnergyManagementSystemActuator>(OS_Coil_UserDefinedFields::AirOutletTemperatureActuator);
+    }
+
+    boost::optional<EnergyManagementSystemActuator> CoilUserDefined_Impl::optionalAirOutletHumidityRatioActuator() const {
+      return getObject<ModelObject>().getModelObjectTarget<EnergyManagementSystemActuator>(OS_Coil_UserDefinedFields::AirOutletHumidityRatioActuator);
+    }
+
+    boost::optional<EnergyManagementSystemActuator> CoilUserDefined_Impl::optionalAirMassFlowRateActuator() const {
+      return getObject<ModelObject>().getModelObjectTarget<EnergyManagementSystemActuator>(OS_Coil_UserDefinedFields::AirMassFlowRateActuator);
+    }
+
+    boost::optional<EnergyManagementSystemActuator> CoilUserDefined_Impl::optionalPlantMinimumMassFlowRateActuator() const {
+      return getObject<ModelObject>().getModelObjectTarget<EnergyManagementSystemActuator>(
+        OS_Coil_UserDefinedFields::PlantMinimumMassFlowRateActuator);
+    }
+
+    boost::optional<EnergyManagementSystemActuator> CoilUserDefined_Impl::optionalPlantMaximumMassFlowRateActuator() const {
+      return getObject<ModelObject>().getModelObjectTarget<EnergyManagementSystemActuator>(
+        OS_Coil_UserDefinedFields::PlantMaximumMassFlowRateActuator);
+    }
+
+    boost::optional<EnergyManagementSystemActuator> CoilUserDefined_Impl::optionalPlantDesignVolumeFlowRateActuator() const {
+      return getObject<ModelObject>().getModelObjectTarget<EnergyManagementSystemActuator>(
+        OS_Coil_UserDefinedFields::PlantDesignVolumeFlowRateActuator);
+    }
+
+    boost::optional<EnergyManagementSystemActuator> CoilUserDefined_Impl::optionalPlantOutletTemperatureActuator() const {
+      return getObject<ModelObject>().getModelObjectTarget<EnergyManagementSystemActuator>(OS_Coil_UserDefinedFields::PlantOutletTemperatureActuator);
+    }
+
+    boost::optional<EnergyManagementSystemActuator> CoilUserDefined_Impl::optionalPlantMassFlowRateActuator() const {
+      return getObject<ModelObject>().getModelObjectTarget<EnergyManagementSystemActuator>(OS_Coil_UserDefinedFields::PlantMassFlowRateActuator);
+    }
+
+    void CoilUserDefined_Impl::renameEMSSubComponents() {
+      auto newName = nameString();
+      std::replace(newName.begin(), newName.end(), ' ', '_');
+      newName += "_";
+      overallModelSimulationProgramCallingManager().setName(newName + "overallModelSimulationProgramCallingManager");
+      modelSetupandSizingProgramCallingManager().setName(newName + "modelSetupandSizingProgramCallingManager");
+      overallSimulationProgram().setName(newName + "overallSimulationProgram");
+      initializationSimulationProgram().setName(newName + "initializationSimulationProgram");
+      airOutletTemperatureActuator().setName(newName + "airOutletTemperatureActuator");
+      airOutletHumidityRatioActuator().setName(newName + "airOutletHumidityRatioActuator");
+      airMassFlowRateActuator().setName(newName + "airMassFlowRateActuator");
+      plantMinimumMassFlowRateActuator().setName(newName + "plantMinimumMassFlowRateActuator");
+      plantMaximumMassFlowRateActuator().setName(newName + "plantMaximumMassFlowRateActuator");
+      plantDesignVolumeFlowRateActuator().setName(newName + "plantDesignVolumeFlowRateActuator");
+      plantMassFlowRateActuator().setName(newName + "plantMassFlowRateActuator");
+      plantOutletTemperatureActuator().setName(newName + "plantOutletTemperatureActuator");
+    }
+
+  }  // namespace detail
+
+  CoilUserDefined::CoilUserDefined(const Model& model) : WaterToAirComponent(CoilUserDefined::iddObjectType(), model) {
+    OS_ASSERT(getImpl<detail::CoilUserDefined_Impl>());
+
+    bool ok = true;
+    // setup required Actuators
+    EnergyManagementSystemActuator aota(this->cast<ModelObject>(), "Air Connection 1", "Outlet Temperature");
+    aota.setName("airOutletTemperature");
+    ok = setAirOutletTemperatureActuator(aota);
+    OS_ASSERT(ok);
+    EnergyManagementSystemActuator aohra(this->cast<ModelObject>(), "Air Connection 1", "Outlet Humidity Ratio");
+    aohra.setName("airOutletHumidityRatio");
+    ok = setAirOutletHumidityRatioActuator(aohra);
+    OS_ASSERT(ok);
+    EnergyManagementSystemActuator amfra(this->cast<ModelObject>(), "Air Connection 1", "Mass Flow Rate");
+    amfra.setName("airMassFlowRate");
+    ok = setAirMassFlowRateActuator(amfra);
+    OS_ASSERT(ok);
+    EnergyManagementSystemActuator pminmfra(this->cast<ModelObject>(), "Plant Connection", "Minimum Mass Flow Rate");
+    pminmfra.setName("plantMinimumMassFlowRate");
+    ok = setPlantMinimumMassFlowRateActuator(pminmfra);
+    OS_ASSERT(ok);
+    EnergyManagementSystemActuator pmaxmfra(this->cast<ModelObject>(), "Plant Connection", "Maximum Mass Flow Rate");
+    pmaxmfra.setName("plantMaximumMassFlowRate");
+    ok = setPlantMaximumMassFlowRateActuator(pmaxmfra);
+    OS_ASSERT(ok);
+    EnergyManagementSystemActuator pdvfra(this->cast<ModelObject>(), "Plant Connection", "Design Volume Flow Rate");
+    pdvfra.setName("plantDesignVolumeFlowRate");
+    ok = setPlantDesignVolumeFlowRateActuator(pdvfra);
+    OS_ASSERT(ok);
+    EnergyManagementSystemActuator pota(this->cast<ModelObject>(), "Plant Connection", "Mass Flow Rate");
+    pota.setName("plantMassFlowRate");
+    ok = setPlantMassFlowRateActuator(pota);
+    OS_ASSERT(ok);
+    EnergyManagementSystemActuator pmmfra(this->cast<ModelObject>(), "Plant Connection", "Outlet Temperature");
+    pmmfra.setName("plantOutletTemperature");
+    ok = setPlantOutletTemperatureActuator(pmmfra);
+    OS_ASSERT(ok);
+
+    // setup Overall Simulation Program and Manager
+    EnergyManagementSystemProgram mainProgram(model);
+    mainProgram.setName("overallSimulationProgram");
+
+    EnergyManagementSystemProgramCallingManager mainProgramCallingManager(model);
+    mainProgramCallingManager.setName("overallModelSimulationProgramCallingManager");
+    mainProgramCallingManager.setCallingPoint("UserDefinedComponentModel");
+    mainProgramCallingManager.addProgram(mainProgram);
+
+    // add as children so they delete if object gets removed
+    ok = setOverallModelSimulationProgramCallingManager(mainProgramCallingManager);
+    OS_ASSERT(ok);
+    ok = setOverallSimulationProgram(mainProgram);
+    OS_ASSERT(ok);
+
+    // setup Overall Simulation Program and Manager
+    EnergyManagementSystemProgram initProgram(model);
+    initProgram.setName("initializationSimulationProgram");
+
+    EnergyManagementSystemProgramCallingManager modelSetupandSizingPCM(model);
+    modelSetupandSizingPCM.setName("modelSetupandSizingProgramCallingManager");
+    modelSetupandSizingPCM.setCallingPoint("UserDefinedComponentModel");
+    modelSetupandSizingPCM.addProgram(initProgram);
+
+    // add as children so they delete if object gets removed
+    ok = setModelSetupandSizingProgramCallingManager(modelSetupandSizingPCM);
+    OS_ASSERT(ok);
+    ok = setInitializationSimulationProgram(initProgram);
+    OS_ASSERT(ok);
+
+    // getImpl<detail::CoilUserDefined_Impl>()->renameEMSSubComponents();
+  }
+
+  IddObjectType CoilUserDefined::iddObjectType() {
+    return {IddObjectType::OS_Coil_UserDefined};
+  }
+
+  EnergyManagementSystemProgramCallingManager CoilUserDefined::overallModelSimulationProgramCallingManager() const {
+    return getImpl<detail::CoilUserDefined_Impl>()->overallModelSimulationProgramCallingManager();
+  }
+
+  EnergyManagementSystemProgramCallingManager CoilUserDefined::modelSetupandSizingProgramCallingManager() const {
+    return getImpl<detail::CoilUserDefined_Impl>()->modelSetupandSizingProgramCallingManager();
+  }
+
+  int CoilUserDefined::numberofAirConnections() const {
+    return getImpl<detail::CoilUserDefined_Impl>()->numberofAirConnections();
+  }
+
+  boost::optional<ThermalZone> CoilUserDefined::ambientZone() const {
+    return getImpl<detail::CoilUserDefined_Impl>()->ambientZone();
+  }
+
+  EnergyManagementSystemProgram CoilUserDefined::overallSimulationProgram() const {
+    return getImpl<detail::CoilUserDefined_Impl>()->overallSimulationProgram();
+  }
+
+  EnergyManagementSystemProgram CoilUserDefined::initializationSimulationProgram() const {
+    return getImpl<detail::CoilUserDefined_Impl>()->initializationSimulationProgram();
+  }
+
+  EnergyManagementSystemActuator CoilUserDefined::airOutletTemperatureActuator() const {
+    return getImpl<detail::CoilUserDefined_Impl>()->airOutletTemperatureActuator();
+  }
+
+  EnergyManagementSystemActuator CoilUserDefined::airOutletHumidityRatioActuator() const {
+    return getImpl<detail::CoilUserDefined_Impl>()->airOutletHumidityRatioActuator();
+  }
+
+  EnergyManagementSystemActuator CoilUserDefined::airMassFlowRateActuator() const {
+    return getImpl<detail::CoilUserDefined_Impl>()->airMassFlowRateActuator();
+  }
+
+  EnergyManagementSystemActuator CoilUserDefined::plantMinimumMassFlowRateActuator() const {
+    return getImpl<detail::CoilUserDefined_Impl>()->plantMinimumMassFlowRateActuator();
+  }
+
+  EnergyManagementSystemActuator CoilUserDefined::plantMaximumMassFlowRateActuator() const {
+    return getImpl<detail::CoilUserDefined_Impl>()->plantMaximumMassFlowRateActuator();
+  }
+
+  EnergyManagementSystemActuator CoilUserDefined::plantDesignVolumeFlowRateActuator() const {
+    return getImpl<detail::CoilUserDefined_Impl>()->plantDesignVolumeFlowRateActuator();
+  }
+
+  EnergyManagementSystemActuator CoilUserDefined::plantOutletTemperatureActuator() const {
+    return getImpl<detail::CoilUserDefined_Impl>()->plantOutletTemperatureActuator();
+  }
+
+  EnergyManagementSystemActuator CoilUserDefined::plantMassFlowRateActuator() const {
+    return getImpl<detail::CoilUserDefined_Impl>()->plantMassFlowRateActuator();
+  }
+
+  bool CoilUserDefined::setOverallModelSimulationProgramCallingManager(const EnergyManagementSystemProgramCallingManager& emsProgramCallingManager) {
+    return getImpl<detail::CoilUserDefined_Impl>()->setOverallModelSimulationProgramCallingManager(emsProgramCallingManager);
+  }
+
+  bool CoilUserDefined::setModelSetupandSizingProgramCallingManager(const EnergyManagementSystemProgramCallingManager& emsProgramCallingManager) {
+    return getImpl<detail::CoilUserDefined_Impl>()->setModelSetupandSizingProgramCallingManager(emsProgramCallingManager);
+  }
+
+  bool CoilUserDefined::setAmbientZone(const ThermalZone& thermalZone) {
+    return getImpl<detail::CoilUserDefined_Impl>()->setAmbientZone(thermalZone);
+  }
+
+  void CoilUserDefined::resetAmbientZone() {
+    getImpl<detail::CoilUserDefined_Impl>()->resetAmbientZone();
+  }
+
+  bool CoilUserDefined::setOverallSimulationProgram(const EnergyManagementSystemProgram& emsProgram) {
+    return getImpl<detail::CoilUserDefined_Impl>()->setOverallSimulationProgram(emsProgram);
+  }
+
+  bool CoilUserDefined::setInitializationSimulationProgram(const EnergyManagementSystemProgram& emsProgram) {
+    return getImpl<detail::CoilUserDefined_Impl>()->setInitializationSimulationProgram(emsProgram);
+  }
+
+  bool CoilUserDefined::setAirOutletTemperatureActuator(const EnergyManagementSystemActuator& emsActuator) {
+    return getImpl<detail::CoilUserDefined_Impl>()->setAirOutletTemperatureActuator(emsActuator);
+  }
+
+  bool CoilUserDefined::setAirOutletHumidityRatioActuator(const EnergyManagementSystemActuator& emsActuator) {
+    return getImpl<detail::CoilUserDefined_Impl>()->setAirOutletHumidityRatioActuator(emsActuator);
+  }
+
+  bool CoilUserDefined::setAirMassFlowRateActuator(const EnergyManagementSystemActuator& emsActuator) {
+    return getImpl<detail::CoilUserDefined_Impl>()->setAirMassFlowRateActuator(emsActuator);
+  }
+
+  bool CoilUserDefined::setPlantMinimumMassFlowRateActuator(const EnergyManagementSystemActuator& emsActuator) {
+    return getImpl<detail::CoilUserDefined_Impl>()->setPlantMinimumMassFlowRateActuator(emsActuator);
+  }
+
+  bool CoilUserDefined::setPlantMaximumMassFlowRateActuator(const EnergyManagementSystemActuator& emsActuator) {
+    return getImpl<detail::CoilUserDefined_Impl>()->setPlantMaximumMassFlowRateActuator(emsActuator);
+  }
+
+  bool CoilUserDefined::setPlantDesignVolumeFlowRateActuator(const EnergyManagementSystemActuator& emsActuator) {
+    return getImpl<detail::CoilUserDefined_Impl>()->setPlantDesignVolumeFlowRateActuator(emsActuator);
+  }
+
+  bool CoilUserDefined::setPlantOutletTemperatureActuator(const EnergyManagementSystemActuator& emsActuator) {
+    return getImpl<detail::CoilUserDefined_Impl>()->setPlantOutletTemperatureActuator(emsActuator);
+  }
+
+  bool CoilUserDefined::setPlantMassFlowRateActuator(const EnergyManagementSystemActuator& emsActuator) {
+    return getImpl<detail::CoilUserDefined_Impl>()->setPlantMassFlowRateActuator(emsActuator);
+  }
+
+  void CoilUserDefined::renameEMSSubComponents() {
+    return getImpl<detail::CoilUserDefined_Impl>()->renameEMSSubComponents();
+  }
+
+  /// @cond
+  CoilUserDefined::CoilUserDefined(std::shared_ptr<detail::CoilUserDefined_Impl> impl) : WaterToAirComponent(std::move(impl)) {}
+  /// @endcond
+
+}  // namespace model
+}  // namespace openstudio
diff --git a/src/model/CoilUserDefined.hpp b/src/model/CoilUserDefined.hpp
new file mode 100644
index 00000000000..39f8d074362
--- /dev/null
+++ b/src/model/CoilUserDefined.hpp
@@ -0,0 +1,165 @@
+/***********************************************************************************************************************
+*  OpenStudio(R), Copyright (c) 2008-2022, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
+*
+*  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+*  following conditions are met:
+*
+*  (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+*  disclaimer.
+*
+*  (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+*  disclaimer in the documentation and/or other materials provided with the distribution.
+*
+*  (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
+*  derived from this software without specific prior written permission from the respective party.
+*
+*  (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
+*  may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
+*  written permission from Alliance for Sustainable Energy, LLC.
+*
+*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+*  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+*  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
+*  STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+*  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+*  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+*  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+***********************************************************************************************************************/
+
+#ifndef MODEL_COILUSERDEFINED_HPP
+#define MODEL_COILUSERDEFINED_HPP
+
+#include <model/ModelAPI.hpp>
+#include "WaterToAirComponent.hpp"
+
+namespace openstudio {
+namespace model {
+
+  class EnergyManagementSystemActuator;
+  class EnergyManagementSystemProgram;
+  class EnergyManagementSystemProgramCallingManager;
+  class ThermalZone;
+
+  namespace detail {
+
+    class CoilUserDefined_Impl;
+
+  }  // namespace detail
+
+  /** CoilUserDefined is a WaterToAirComponent that wraps the OpenStudio IDD object 'OS:Coil:UserDefined'. */
+  class MODEL_API CoilUserDefined : public WaterToAirComponent
+  {
+   public:
+    /** @name Constructors and Destructors */
+    //@{
+
+    explicit CoilUserDefined(const Model& model);
+
+    virtual ~CoilUserDefined() = default;
+    // Default the copy and move operators because the virtual dtor is explicit
+    CoilUserDefined(const CoilUserDefined& other) = default;
+    CoilUserDefined(CoilUserDefined&& other) = default;
+    CoilUserDefined& operator=(const CoilUserDefined&) = default;
+    CoilUserDefined& operator=(CoilUserDefined&&) = default;
+
+    //@}
+
+    static IddObjectType iddObjectType();
+
+    /** @name Getters */
+    //@{
+
+    EnergyManagementSystemProgramCallingManager overallModelSimulationProgramCallingManager() const;
+
+    EnergyManagementSystemProgramCallingManager modelSetupandSizingProgramCallingManager() const;
+
+    boost::optional<ThermalZone> ambientZone() const;
+
+    EnergyManagementSystemProgram overallSimulationProgram() const;
+
+    EnergyManagementSystemProgram initializationSimulationProgram() const;
+
+    EnergyManagementSystemActuator airOutletTemperatureActuator() const;
+
+    EnergyManagementSystemActuator airOutletHumidityRatioActuator() const;
+
+    EnergyManagementSystemActuator airMassFlowRateActuator() const;
+
+    EnergyManagementSystemActuator plantMinimumMassFlowRateActuator() const;
+
+    EnergyManagementSystemActuator plantMaximumMassFlowRateActuator() const;
+
+    EnergyManagementSystemActuator plantDesignVolumeFlowRateActuator() const;
+
+    EnergyManagementSystemActuator plantOutletTemperatureActuator() const;
+
+    EnergyManagementSystemActuator plantMassFlowRateActuator() const;
+
+    //@}
+    /** @name Setters */
+    //@{
+
+    bool setOverallModelSimulationProgramCallingManager(const EnergyManagementSystemProgramCallingManager& emsProgramCallingManager);
+
+    bool setModelSetupandSizingProgramCallingManager(const EnergyManagementSystemProgramCallingManager& emsProgramCallingManager);
+
+    bool setAmbientZone(const ThermalZone& thermalZone);
+
+    void resetAmbientZone();
+
+    bool setOverallSimulationProgram(const EnergyManagementSystemProgram& emsProgram);
+
+    bool setInitializationSimulationProgram(const EnergyManagementSystemProgram& emsProgram);
+
+    bool setAirOutletTemperatureActuator(const EnergyManagementSystemActuator& emsActuator);
+
+    bool setAirOutletHumidityRatioActuator(const EnergyManagementSystemActuator& emsActuator);
+
+    bool setAirMassFlowRateActuator(const EnergyManagementSystemActuator& emsActuator);
+
+    bool setPlantMinimumMassFlowRateActuator(const EnergyManagementSystemActuator& emsActuator);
+
+    bool setPlantMaximumMassFlowRateActuator(const EnergyManagementSystemActuator& emsActuator);
+
+    bool setPlantDesignVolumeFlowRateActuator(const EnergyManagementSystemActuator& emsActuator);
+
+    bool setPlantOutletTemperatureActuator(const EnergyManagementSystemActuator& emsActuator);
+
+    bool setPlantMassFlowRateActuator(const EnergyManagementSystemActuator& emsActuator);
+
+    //@}
+    /** @name Other */
+    //@{
+
+    int numberofAirConnections() const;
+
+    // Will rename the EMS Program, Program Calling Manager, and Actuators to be nameString() + " xxx"
+    void renameEMSSubComponents();
+
+    //@}
+   protected:
+    /// @cond
+    using ImplType = detail::CoilUserDefined_Impl;
+
+    explicit CoilUserDefined(std::shared_ptr<detail::CoilUserDefined_Impl> impl);
+
+    friend class detail::CoilUserDefined_Impl;
+    friend class Model;
+    friend class IdfObject;
+    friend class openstudio::detail::IdfObject_Impl;
+    /// @endcond
+   private:
+    REGISTER_LOGGER("openstudio.model.CoilUserDefined");
+  };
+
+  /** \relates CoilUserDefined*/
+  using OptionalCoilUserDefined = boost::optional<CoilUserDefined>;
+
+  /** \relates CoilUserDefined*/
+  using CoilUserDefinedVector = std::vector<CoilUserDefined>;
+
+}  // namespace model
+}  // namespace openstudio
+
+#endif  // MODEL_COILUSERDEFINED_HPP
diff --git a/src/model/CoilUserDefined_Impl.hpp b/src/model/CoilUserDefined_Impl.hpp
new file mode 100644
index 00000000000..18d35bf73ab
--- /dev/null
+++ b/src/model/CoilUserDefined_Impl.hpp
@@ -0,0 +1,185 @@
+/***********************************************************************************************************************
+*  OpenStudio(R), Copyright (c) 2008-2022, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
+*
+*  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+*  following conditions are met:
+*
+*  (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+*  disclaimer.
+*
+*  (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+*  disclaimer in the documentation and/or other materials provided with the distribution.
+*
+*  (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
+*  derived from this software without specific prior written permission from the respective party.
+*
+*  (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
+*  may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
+*  written permission from Alliance for Sustainable Energy, LLC.
+*
+*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+*  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+*  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
+*  STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+*  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+*  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+*  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+***********************************************************************************************************************/
+
+#ifndef MODEL_COILUSERDEFINED_IMPL_HPP
+#define MODEL_COILUSERDEFINED_IMPL_HPP
+
+#include <model/ModelAPI.hpp>
+#include "WaterToAirComponent_Impl.hpp"
+
+namespace openstudio {
+namespace model {
+
+  class EnergyManagementSystemActuator;
+  class EnergyManagementSystemProgram;
+  class EnergyManagementSystemProgramCallingManager;
+  class ThermalZone;
+
+  namespace detail {
+
+    /** CoilUserDefined_Impl is a WaterToAirComponent_Impl that is the implementation class for CoilUserDefined.*/
+    class MODEL_API CoilUserDefined_Impl : public WaterToAirComponent_Impl
+    {
+     public:
+      /** @name Constructors and Destructors */
+      //@{
+
+      CoilUserDefined_Impl(const IdfObject& idfObject, Model_Impl* model, bool keepHandle);
+
+      CoilUserDefined_Impl(const openstudio::detail::WorkspaceObject_Impl& other, Model_Impl* model, bool keepHandle);
+
+      CoilUserDefined_Impl(const CoilUserDefined_Impl& other, Model_Impl* model, bool keepHandle);
+
+      virtual ~CoilUserDefined_Impl() = default;
+
+      //@}
+      /** @name Virtual Methods */
+      //@{
+
+      virtual const std::vector<std::string>& outputVariableNames() const override;
+
+      virtual IddObjectType iddObjectType() const override;
+
+      virtual unsigned airInletPort() const override;
+
+      virtual unsigned airOutletPort() const override;
+
+      virtual unsigned waterInletPort() const override;
+
+      virtual unsigned waterOutletPort() const override;
+
+      // virtual std::vector<openstudio::IdfObject> remove() override;
+
+      // HVACComponent skips the ParentObject and goes straight to ModelObject so children aren't clone
+      virtual ModelObject clone(Model model) const override;
+
+      virtual std::vector<IddObjectType> allowableChildTypes() const override;
+
+      virtual std::vector<ModelObject> children() const override;
+
+      virtual boost::optional<HVACComponent> containingHVACComponent() const override;
+
+      //@}
+      /** @name Getters */
+      //@{
+
+      EnergyManagementSystemProgramCallingManager overallModelSimulationProgramCallingManager() const;
+
+      EnergyManagementSystemProgramCallingManager modelSetupandSizingProgramCallingManager() const;
+
+      boost::optional<ThermalZone> ambientZone() const;
+
+      EnergyManagementSystemProgram overallSimulationProgram() const;
+
+      EnergyManagementSystemProgram initializationSimulationProgram() const;
+
+      EnergyManagementSystemActuator airOutletTemperatureActuator() const;
+
+      EnergyManagementSystemActuator airOutletHumidityRatioActuator() const;
+
+      EnergyManagementSystemActuator airMassFlowRateActuator() const;
+
+      EnergyManagementSystemActuator plantMinimumMassFlowRateActuator() const;
+
+      EnergyManagementSystemActuator plantMaximumMassFlowRateActuator() const;
+
+      EnergyManagementSystemActuator plantDesignVolumeFlowRateActuator() const;
+
+      EnergyManagementSystemActuator plantOutletTemperatureActuator() const;
+
+      EnergyManagementSystemActuator plantMassFlowRateActuator() const;
+
+      //@}
+      /** @name Setters */
+      //@{
+
+      bool setOverallModelSimulationProgramCallingManager(const EnergyManagementSystemProgramCallingManager& emsProgramCallingManager);
+
+      bool setModelSetupandSizingProgramCallingManager(const EnergyManagementSystemProgramCallingManager& emsProgramCallingManager);
+
+      bool setAmbientZone(const ThermalZone& thermalZone);
+
+      void resetAmbientZone();
+
+      bool setOverallSimulationProgram(const EnergyManagementSystemProgram& emsProgram);
+
+      bool setInitializationSimulationProgram(const EnergyManagementSystemProgram& emsProgram);
+
+      bool setAirOutletTemperatureActuator(const EnergyManagementSystemActuator& emsActuator);
+
+      bool setAirOutletHumidityRatioActuator(const EnergyManagementSystemActuator& emsActuator);
+
+      bool setAirMassFlowRateActuator(const EnergyManagementSystemActuator& emsActuator);
+
+      bool setPlantMinimumMassFlowRateActuator(const EnergyManagementSystemActuator& emsActuator);
+
+      bool setPlantMaximumMassFlowRateActuator(const EnergyManagementSystemActuator& emsActuator);
+
+      bool setPlantDesignVolumeFlowRateActuator(const EnergyManagementSystemActuator& emsActuator);
+
+      bool setPlantOutletTemperatureActuator(const EnergyManagementSystemActuator& emsActuator);
+
+      bool setPlantMassFlowRateActuator(const EnergyManagementSystemActuator& emsActuator);
+
+      //@}
+      /** @name Other */
+      //@{
+
+      int numberofAirConnections() const;
+
+      void renameEMSSubComponents();
+
+      //@}
+     protected:
+     private:
+      REGISTER_LOGGER("openstudio.model.CoilUserDefined");
+
+      // Optional getters for use by methods like children() so can remove() if the constructor fails.
+      // There are other ways for the public versions of these getters to fail--perhaps all required
+      // objects should be returned as boost::optionals
+      boost::optional<EnergyManagementSystemProgramCallingManager> optionalOverallModelSimulationProgramCallingManager() const;
+      boost::optional<EnergyManagementSystemProgramCallingManager> optionalModelSetupandSizingProgramCallingManager() const;
+      boost::optional<EnergyManagementSystemProgram> optionalOverallSimulationProgram() const;
+      boost::optional<EnergyManagementSystemProgram> optionalInitializationSimulationProgram() const;
+      boost::optional<EnergyManagementSystemActuator> optionalAirOutletTemperatureActuator() const;
+      boost::optional<EnergyManagementSystemActuator> optionalAirOutletHumidityRatioActuator() const;
+      boost::optional<EnergyManagementSystemActuator> optionalAirMassFlowRateActuator() const;
+      boost::optional<EnergyManagementSystemActuator> optionalPlantMinimumMassFlowRateActuator() const;
+      boost::optional<EnergyManagementSystemActuator> optionalPlantMaximumMassFlowRateActuator() const;
+      boost::optional<EnergyManagementSystemActuator> optionalPlantDesignVolumeFlowRateActuator() const;
+      boost::optional<EnergyManagementSystemActuator> optionalPlantOutletTemperatureActuator() const;
+      boost::optional<EnergyManagementSystemActuator> optionalPlantMassFlowRateActuator() const;
+    };
+
+  }  // namespace detail
+
+}  // namespace model
+}  // namespace openstudio
+
+#endif  // MODEL_COILUSERDEFINED_IMPL_HPP
diff --git a/src/model/ConcreteModelObjects.hpp b/src/model/ConcreteModelObjects.hpp
index 79bbfcd5d24..d69360890bc 100644
--- a/src/model/ConcreteModelObjects.hpp
+++ b/src/model/ConcreteModelObjects.hpp
@@ -161,6 +161,7 @@
 #include "CoilSystemCoolingWaterHeatExchangerAssisted.hpp"
 #include "CoilSystemCoolingDXHeatExchangerAssisted.hpp"
 #include "CoilSystemIntegratedHeatPumpAirSource.hpp"
+#include "CoilUserDefined.hpp"
 #include "CoilWaterHeatingAirToWaterHeatPump.hpp"
 #include "CoilWaterHeatingAirToWaterHeatPumpVariableSpeed.hpp"
 #include "CoilWaterHeatingAirToWaterHeatPumpVariableSpeedSpeedData.hpp"
@@ -709,6 +710,7 @@
 #include "CoilSystemCoolingWaterHeatExchangerAssisted_Impl.hpp"
 #include "CoilSystemCoolingDXHeatExchangerAssisted_Impl.hpp"
 #include "CoilSystemIntegratedHeatPumpAirSource_Impl.hpp"
+#include "CoilUserDefined_Impl.hpp"
 #include "CoilWaterHeatingAirToWaterHeatPump_Impl.hpp"
 #include "CoilWaterHeatingAirToWaterHeatPumpVariableSpeed_Impl.hpp"
 #include "CoilWaterHeatingAirToWaterHeatPumpVariableSpeedSpeedData_Impl.hpp"
diff --git a/src/model/Model.cpp b/src/model/Model.cpp
index 07c85ff8440..680a79d638c 100644
--- a/src/model/Model.cpp
+++ b/src/model/Model.cpp
@@ -3990,6 +3990,7 @@ namespace model {
     REGISTER_CONSTRUCTOR(CoilSystemCoolingWaterHeatExchangerAssisted);
     REGISTER_CONSTRUCTOR(CoilSystemCoolingDXHeatExchangerAssisted);
     REGISTER_CONSTRUCTOR(CoilSystemIntegratedHeatPumpAirSource);
+    REGISTER_CONSTRUCTOR(CoilUserDefined);
     REGISTER_CONSTRUCTOR(CoilWaterHeatingAirToWaterHeatPump);
     REGISTER_CONSTRUCTOR(CoilWaterHeatingAirToWaterHeatPumpVariableSpeed);
     REGISTER_CONSTRUCTOR(CoilWaterHeatingAirToWaterHeatPumpVariableSpeedSpeedData);
@@ -4558,6 +4559,7 @@ namespace model {
     REGISTER_COPYCONSTRUCTORS(CoilSystemCoolingWaterHeatExchangerAssisted);
     REGISTER_COPYCONSTRUCTORS(CoilSystemCoolingDXHeatExchangerAssisted);
     REGISTER_COPYCONSTRUCTORS(CoilSystemIntegratedHeatPumpAirSource);
+    REGISTER_COPYCONSTRUCTORS(CoilUserDefined);
     REGISTER_COPYCONSTRUCTORS(CoilWaterHeatingAirToWaterHeatPump);
     REGISTER_COPYCONSTRUCTORS(CoilWaterHeatingAirToWaterHeatPumpVariableSpeed);
     REGISTER_COPYCONSTRUCTORS(CoilWaterHeatingAirToWaterHeatPumpVariableSpeedSpeedData);
diff --git a/src/model/ModelCore.i b/src/model/ModelCore.i
index afbca89ad6b..c9c5a16af2b 100644
--- a/src/model/ModelCore.i
+++ b/src/model/ModelCore.i
@@ -37,6 +37,7 @@
   %ignore openstudio::model::Model::outputControlFiles;
   %ignore openstudio::model::Model::outputControlReportingTolerances;
   %ignore openstudio::model::Model::outputControlTableStyle;
+  %ignore openstudio::model::Model::outputControlTimestamp;
   %ignore openstudio::model::Model::outputDebuggingData;
   %ignore openstudio::model::Model::outputDiagnostics;
   %ignore openstudio::model::Model::outputJSON;
diff --git a/src/model/ModelHVAC.i b/src/model/ModelHVAC.i
index eb1db772c24..230c86d9dd2 100644
--- a/src/model/ModelHVAC.i
+++ b/src/model/ModelHVAC.i
@@ -194,6 +194,7 @@ MODELOBJECT_TEMPLATES(CoilHeatingWaterToAirHeatPumpEquationFit);
 MODELOBJECT_TEMPLATES(CoilHeatingWaterToAirHeatPumpVariableSpeedEquationFit);
 MODELOBJECT_TEMPLATES(CoilHeatingWaterToAirHeatPumpVariableSpeedEquationFitSpeedData);
 MODELOBJECT_TEMPLATES(CoilPerformanceDXCooling);
+MODELOBJECT_TEMPLATES(CoilUserDefined);
 MODELOBJECT_TEMPLATES(CoilWaterHeatingAirToWaterHeatPump);
 MODELOBJECT_TEMPLATES(CoilWaterHeatingAirToWaterHeatPumpVariableSpeed);
 MODELOBJECT_TEMPLATES(CoilWaterHeatingAirToWaterHeatPumpVariableSpeedSpeedData);
@@ -323,6 +324,7 @@ SWIG_MODELOBJECT(CoilHeatingWaterToAirHeatPumpEquationFit, 1);
 SWIG_MODELOBJECT(CoilHeatingWaterToAirHeatPumpVariableSpeedEquationFit, 1);
 SWIG_MODELOBJECT(CoilHeatingWaterToAirHeatPumpVariableSpeedEquationFitSpeedData, 1);
 SWIG_MODELOBJECT(CoilPerformanceDXCooling, 1);
+SWIG_MODELOBJECT(CoilUserDefined, 1);
 SWIG_MODELOBJECT(CoilWaterHeatingAirToWaterHeatPump, 1);
 SWIG_MODELOBJECT(CoilWaterHeatingAirToWaterHeatPumpVariableSpeed, 1);
 SWIG_MODELOBJECT(CoilWaterHeatingAirToWaterHeatPumpVariableSpeedSpeedData, 1);


Test patch:
diff --git a/src/model/test/CoilUserDefined_GTest.cpp b/src/model/test/CoilUserDefined_GTest.cpp
new file mode 100644
index 00000000000..6686e085062
--- /dev/null
+++ b/src/model/test/CoilUserDefined_GTest.cpp
@@ -0,0 +1,518 @@
+/***********************************************************************************************************************
+*  OpenStudio(R), Copyright (c) 2008-2022, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
+*
+*  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+*  following conditions are met:
+*
+*  (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+*  disclaimer.
+*
+*  (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+*  disclaimer in the documentation and/or other materials provided with the distribution.
+*
+*  (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
+*  derived from this software without specific prior written permission from the respective party.
+*
+*  (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
+*  may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
+*  written permission from Alliance for Sustainable Energy, LLC.
+*
+*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+*  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+*  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
+*  STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+*  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+*  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+*  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+***********************************************************************************************************************/
+
+#include <gtest/gtest.h>
+#include "ModelFixture.hpp"
+
+#include "../AirLoopHVAC.hpp"
+#include "../CoilUserDefined.hpp"
+#include "../CoilUserDefined_Impl.hpp"
+#include "../EnergyManagementSystemProgramCallingManager.hpp"
+#include "../EnergyManagementSystemProgram.hpp"
+#include "../EnergyManagementSystemActuator.hpp"
+#include "../ThermalZone.hpp"
+#include "../Node.hpp"
+#include "../Node_Impl.hpp"
+#include "../AirLoopHVACZoneSplitter.hpp"
+#include "../AirLoopHVACUnitarySystem.hpp"
+
+#include "../../utilities/core/Path.hpp"
+
+using namespace openstudio;
+using namespace openstudio::model;
+
+TEST_F(ModelFixture, CoilUserDefined_GettersSetters) {
+  Model m;
+
+  CoilUserDefined coilUserDefined(m);
+  coilUserDefined.setName("My CoilUserDefined");
+
+  // Convenience method provided
+  coilUserDefined.renameEMSSubComponents();
+
+  // Test the convenience method + getters
+  EXPECT_EQ("My_CoilUserDefined_overallModelSimulationProgramCallingManager",
+            coilUserDefined.overallModelSimulationProgramCallingManager().nameString());
+  EXPECT_EQ("My_CoilUserDefined_modelSetupandSizingProgramCallingManager", coilUserDefined.modelSetupandSizingProgramCallingManager().nameString());
+  EXPECT_EQ("My_CoilUserDefined_overallSimulationProgram", coilUserDefined.overallSimulationProgram().nameString());
+  EXPECT_EQ("My_CoilUserDefined_initializationSimulationProgram", coilUserDefined.initializationSimulationProgram().nameString());
+  EXPECT_EQ("My_CoilUserDefined_airOutletTemperatureActuator", coilUserDefined.airOutletTemperatureActuator().nameString());
+  EXPECT_EQ("My_CoilUserDefined_airOutletHumidityRatioActuator", coilUserDefined.airOutletHumidityRatioActuator().nameString());
+  EXPECT_EQ("My_CoilUserDefined_airMassFlowRateActuator", coilUserDefined.airMassFlowRateActuator().nameString());
+  EXPECT_EQ("My_CoilUserDefined_plantMinimumMassFlowRateActuator", coilUserDefined.plantMinimumMassFlowRateActuator().nameString());
+  EXPECT_EQ("My_CoilUserDefined_plantMaximumMassFlowRateActuator", coilUserDefined.plantMaximumMassFlowRateActuator().nameString());
+  EXPECT_EQ("My_CoilUserDefined_plantDesignVolumeFlowRateActuator", coilUserDefined.plantDesignVolumeFlowRateActuator().nameString());
+  EXPECT_EQ("My_CoilUserDefined_plantMassFlowRateActuator", coilUserDefined.plantMassFlowRateActuator().nameString());
+  EXPECT_EQ("My_CoilUserDefined_plantOutletTemperatureActuator", coilUserDefined.plantOutletTemperatureActuator().nameString());
+
+  // Make sure the actuators are correct
+  ASSERT_TRUE(coilUserDefined.airOutletTemperatureActuator().actuatedComponent());
+  EXPECT_EQ(coilUserDefined, coilUserDefined.airOutletTemperatureActuator().actuatedComponent().get());
+  ASSERT_TRUE(coilUserDefined.airOutletHumidityRatioActuator().actuatedComponent());
+  EXPECT_EQ(coilUserDefined, coilUserDefined.airOutletHumidityRatioActuator().actuatedComponent().get());
+  ASSERT_TRUE(coilUserDefined.airMassFlowRateActuator().actuatedComponent());
+  EXPECT_EQ(coilUserDefined, coilUserDefined.airMassFlowRateActuator().actuatedComponent().get());
+  ASSERT_TRUE(coilUserDefined.plantMinimumMassFlowRateActuator().actuatedComponent());
+  EXPECT_EQ(coilUserDefined, coilUserDefined.plantMinimumMassFlowRateActuator().actuatedComponent().get());
+  ASSERT_TRUE(coilUserDefined.plantMaximumMassFlowRateActuator().actuatedComponent());
+  EXPECT_EQ(coilUserDefined, coilUserDefined.plantMaximumMassFlowRateActuator().actuatedComponent().get());
+  ASSERT_TRUE(coilUserDefined.plantDesignVolumeFlowRateActuator().actuatedComponent());
+  EXPECT_EQ(coilUserDefined, coilUserDefined.plantDesignVolumeFlowRateActuator().actuatedComponent().get());
+  ASSERT_TRUE(coilUserDefined.plantMassFlowRateActuator().actuatedComponent());
+  EXPECT_EQ(coilUserDefined, coilUserDefined.plantMassFlowRateActuator().actuatedComponent().get());
+  ASSERT_TRUE(coilUserDefined.plantOutletTemperatureActuator().actuatedComponent());
+  EXPECT_EQ(coilUserDefined, coilUserDefined.plantOutletTemperatureActuator().actuatedComponent().get());
+
+  // Overall Model Simulation Program Calling Manager Name: Required Object
+  EnergyManagementSystemProgramCallingManager overallModelSimulationProgramCallingManager(m);
+  EXPECT_TRUE(coilUserDefined.setOverallModelSimulationProgramCallingManager(overallModelSimulationProgramCallingManager));
+  EXPECT_EQ(overallModelSimulationProgramCallingManager, coilUserDefined.overallModelSimulationProgramCallingManager());
+
+  // Model Setup and Sizing Program Calling Manager Name: Required Object
+  EnergyManagementSystemProgramCallingManager modelSetupandSizingProgramCallingManager(m);
+  EXPECT_TRUE(coilUserDefined.setModelSetupandSizingProgramCallingManager(modelSetupandSizingProgramCallingManager));
+  EXPECT_EQ(modelSetupandSizingProgramCallingManager, coilUserDefined.modelSetupandSizingProgramCallingManager());
+
+  // Ambient Zone Name: Optional Object
+  EXPECT_FALSE(coilUserDefined.ambientZone());
+  ThermalZone ambientZone(m);
+  EXPECT_TRUE(coilUserDefined.setAmbientZone(ambientZone));
+  ASSERT_TRUE(coilUserDefined.ambientZone());
+  EXPECT_EQ(ambientZone, coilUserDefined.ambientZone().get());
+  coilUserDefined.resetAmbientZone();
+  EXPECT_FALSE(coilUserDefined.ambientZone());
+
+  // Overall Simulation Program Name: Required Object
+  EnergyManagementSystemProgram overallSimulationProgram(m);
+  EXPECT_TRUE(coilUserDefined.setOverallSimulationProgram(overallSimulationProgram));
+  EXPECT_EQ(overallSimulationProgram, coilUserDefined.overallSimulationProgram());
+
+  // Initialization Simulation Program Name: Required Object
+  EnergyManagementSystemProgram initializationSimulationProgram(m);
+  EXPECT_TRUE(coilUserDefined.setInitializationSimulationProgram(initializationSimulationProgram));
+  EXPECT_EQ(initializationSimulationProgram, coilUserDefined.initializationSimulationProgram());
+
+  // Air Outlet Temperature Actuator: Required Object
+  EnergyManagementSystemActuator airOutletTemperatureActuator(coilUserDefined, "Air Connection 1", "Outlet Temperature");
+  EXPECT_TRUE(coilUserDefined.setAirOutletTemperatureActuator(airOutletTemperatureActuator));
+  EXPECT_EQ(airOutletTemperatureActuator, coilUserDefined.airOutletTemperatureActuator());
+
+  // Air Outlet Humidity Ratio Actuator: Required Object
+  EnergyManagementSystemActuator airOutletHumidityRatioActuator(coilUserDefined, "Air Connection 1", "Outlet Humidity Ratio");
+  EXPECT_TRUE(coilUserDefined.setAirOutletHumidityRatioActuator(airOutletHumidityRatioActuator));
+  EXPECT_EQ(airOutletHumidityRatioActuator, coilUserDefined.airOutletHumidityRatioActuator());
+
+  // Air Mass Flow Rate Actuator: Required Object
+  EnergyManagementSystemActuator airMassFlowRateActuator(coilUserDefined, "Air Connection 1", "Mass Flow Rate");
+  EXPECT_TRUE(coilUserDefined.setAirMassFlowRateActuator(airMassFlowRateActuator));
+  EXPECT_EQ(airMassFlowRateActuator, coilUserDefined.airMassFlowRateActuator());
+
+  // Plant Minimum Mass Flow Rate Actuator: Required Object
+  EnergyManagementSystemActuator plantMinimumMassFlowRateActuator(coilUserDefined, "Plant Connection", "Minimum Mass Flow Rate");
+  EXPECT_TRUE(coilUserDefined.setPlantMinimumMassFlowRateActuator(plantMinimumMassFlowRateActuator));
+  EXPECT_EQ(plantMinimumMassFlowRateActuator, coilUserDefined.plantMinimumMassFlowRateActuator());
+
+  // Plant Maximum Mass Flow Rate Actuator: Required Object
+  EnergyManagementSystemActuator plantMaximumMassFlowRateActuator(coilUserDefined, "Plant Connection", "Maximum Mass Flow Rate");
+  EXPECT_TRUE(coilUserDefined.setPlantMaximumMassFlowRateActuator(plantMaximumMassFlowRateActuator));
+  EXPECT_EQ(plantMaximumMassFlowRateActuator, coilUserDefined.plantMaximumMassFlowRateActuator());
+
+  // Plant Design Volume Flow Rate Actuator: Required Object
+  EnergyManagementSystemActuator plantDesignVolumeFlowRateActuator(coilUserDefined, "Plant Connection", "Design Volume Flow Rate");
+  EXPECT_TRUE(coilUserDefined.setPlantDesignVolumeFlowRateActuator(plantDesignVolumeFlowRateActuator));
+  EXPECT_EQ(plantDesignVolumeFlowRateActuator, coilUserDefined.plantDesignVolumeFlowRateActuator());
+
+  // Plant Outlet Temperature Actuator: Required Object
+  EnergyManagementSystemActuator plantOutletTemperatureActuator(coilUserDefined, "Plant Connection", "Outlet Temperature");
+  EXPECT_TRUE(coilUserDefined.setPlantOutletTemperatureActuator(plantOutletTemperatureActuator));
+  EXPECT_EQ(plantOutletTemperatureActuator, coilUserDefined.plantOutletTemperatureActuator());
+
+  // Plant Mass Flow Rate Actuator: Required Object
+  EnergyManagementSystemActuator plantMassFlowRateActuator(coilUserDefined, "Plant Connection", "Mass Flow Rate");
+  EXPECT_TRUE(coilUserDefined.setPlantMassFlowRateActuator(plantMassFlowRateActuator));
+  EXPECT_EQ(plantMassFlowRateActuator, coilUserDefined.plantMassFlowRateActuator());
+}
+
+TEST_F(ModelFixture, CoilUserDefined_addToNode) {
+
+  Model m;
+
+  CoilUserDefined coilUserDefined(m);
+  EXPECT_FALSE(coilUserDefined.airLoopHVAC());
+  EXPECT_FALSE(coilUserDefined.airInletModelObject());
+  EXPECT_FALSE(coilUserDefined.airOutletModelObject());
+  EXPECT_FALSE(coilUserDefined.plantLoop());
+  EXPECT_FALSE(coilUserDefined.waterInletModelObject());
+  EXPECT_FALSE(coilUserDefined.waterOutletModelObject());
+
+  {
+    AirLoopHVAC airLoop(m);
+
+    EXPECT_EQ(0, coilUserDefined.numberofAirConnections());
+
+    // Accepted on supply
+    Node supplyOutletNode = airLoop.supplyOutletNode();
+    EXPECT_FALSE(coilUserDefined.airLoopHVAC());
+    EXPECT_EQ(2, airLoop.supplyComponents().size());
+    EXPECT_TRUE(coilUserDefined.addToNode(supplyOutletNode));
+    EXPECT_EQ(3, airLoop.supplyComponents().size());
+    ASSERT_TRUE(coilUserDefined.airLoopHVAC());
+    EXPECT_EQ(1, coilUserDefined.numberofAirConnections());
+    EXPECT_EQ(airLoop, coilUserDefined.airLoopHVAC().get());
+    ASSERT_TRUE(coilUserDefined.airInletModelObject());
+    ASSERT_TRUE(coilUserDefined.airOutletModelObject());
+    EXPECT_EQ(supplyOutletNode, coilUserDefined.airOutletModelObject().get());
+    EXPECT_FALSE(coilUserDefined.plantLoop());
+    EXPECT_FALSE(coilUserDefined.waterInletModelObject());
+    EXPECT_FALSE(coilUserDefined.waterOutletModelObject());
+
+    // Rejected on demand
+    Node demandNode = airLoop.zoneSplitter().lastOutletModelObject()->cast<Node>();
+    EXPECT_FALSE(coilUserDefined.addToNode(demandNode));
+    // 5u: inlet splitter node mixer outlet.
+    EXPECT_EQ(5, airLoop.demandComponents().size());
+    EXPECT_EQ(3, airLoop.supplyComponents().size());
+    ASSERT_TRUE(coilUserDefined.airLoopHVAC());
+    EXPECT_EQ(airLoop, coilUserDefined.airLoopHVAC().get());
+    ASSERT_TRUE(coilUserDefined.airInletModelObject());
+    ASSERT_TRUE(coilUserDefined.airOutletModelObject());
+    EXPECT_EQ(supplyOutletNode, coilUserDefined.airOutletModelObject().get());
+    EXPECT_FALSE(coilUserDefined.plantLoop());
+    EXPECT_FALSE(coilUserDefined.waterInletModelObject());
+    EXPECT_FALSE(coilUserDefined.waterOutletModelObject());
+
+    // Remove
+    EXPECT_TRUE(coilUserDefined.removeFromAirLoopHVAC());
+    EXPECT_EQ(0, coilUserDefined.numberofAirConnections());
+    EXPECT_EQ(2, airLoop.supplyComponents().size());
+    EXPECT_FALSE(coilUserDefined.airLoopHVAC());
+    EXPECT_FALSE(coilUserDefined.airInletModelObject());
+    EXPECT_FALSE(coilUserDefined.airOutletModelObject());
+    EXPECT_FALSE(coilUserDefined.plantLoop());
+    EXPECT_FALSE(coilUserDefined.waterInletModelObject());
+    EXPECT_FALSE(coilUserDefined.waterOutletModelObject());
+  }
+
+  {
+    PlantLoop plantLoop(m);
+
+    // Rejected on supply
+    auto supplyOutletNode = plantLoop.supplyOutletNode();
+    EXPECT_EQ(5, plantLoop.supplyComponents().size());
+    EXPECT_EQ(5, plantLoop.demandComponents().size());
+    EXPECT_FALSE(coilUserDefined.addToNode(supplyOutletNode));
+    EXPECT_EQ(5, plantLoop.supplyComponents().size());
+    EXPECT_EQ(5, plantLoop.demandComponents().size());
+    EXPECT_FALSE(coilUserDefined.airLoopHVAC());
+    EXPECT_FALSE(coilUserDefined.airInletModelObject());
+    EXPECT_FALSE(coilUserDefined.airOutletModelObject());
+    EXPECT_FALSE(coilUserDefined.plantLoop());
+    EXPECT_FALSE(coilUserDefined.waterInletModelObject());
+    EXPECT_FALSE(coilUserDefined.waterOutletModelObject());
+
+    // Accepted on demand
+    auto demandOutletNode = plantLoop.demandOutletNode();
+    EXPECT_TRUE(coilUserDefined.addToNode(demandOutletNode));
+    EXPECT_EQ(5, plantLoop.supplyComponents().size());
+    EXPECT_EQ(7, plantLoop.demandComponents().size());
+    EXPECT_FALSE(coilUserDefined.airLoopHVAC());
+    EXPECT_FALSE(coilUserDefined.airInletModelObject());
+    EXPECT_FALSE(coilUserDefined.airOutletModelObject());
+    ASSERT_TRUE(coilUserDefined.plantLoop());
+    EXPECT_EQ(plantLoop, coilUserDefined.plantLoop().get());
+    ASSERT_TRUE(coilUserDefined.waterInletModelObject());
+    ASSERT_TRUE(coilUserDefined.waterOutletModelObject());
+    EXPECT_EQ(demandOutletNode, coilUserDefined.waterOutletModelObject().get());
+
+    // Remove
+    EXPECT_TRUE(coilUserDefined.removeFromPlantLoop());
+    EXPECT_EQ(5, plantLoop.supplyComponents().size());
+    EXPECT_EQ(5, plantLoop.demandComponents().size());
+    EXPECT_FALSE(coilUserDefined.airLoopHVAC());
+    EXPECT_FALSE(coilUserDefined.airInletModelObject());
+    EXPECT_FALSE(coilUserDefined.airOutletModelObject());
+    EXPECT_FALSE(coilUserDefined.plantLoop());
+    EXPECT_FALSE(coilUserDefined.waterInletModelObject());
+    EXPECT_FALSE(coilUserDefined.waterOutletModelObject());
+  }
+}
+
+// Add CoilUserDefined to AirLoopHVAC and PlantLoop
+// Test CoilUserDefined::remove and make sure that the loops are intact
+TEST_F(ModelFixture, CoilUserDefined_remove) {
+  Model m;
+  CoilUserDefined coil(m);
+
+  AirLoopHVAC airLoop(m);
+  PlantLoop plantLoop(m);
+  EXPECT_EQ(2, airLoop.supplyComponents().size());
+  EXPECT_EQ(5, airLoop.demandComponents().size());
+  EXPECT_EQ(5, plantLoop.supplyComponents().size());
+  EXPECT_EQ(5, plantLoop.demandComponents().size());
+
+  Node airSupplyOutletNode = airLoop.supplyOutletNode();
+  coil.addToNode(airSupplyOutletNode);
+  Node waterDemandOutletNode = plantLoop.demandOutletNode();
+  coil.addToNode(waterDemandOutletNode);
+  EXPECT_EQ(3, airLoop.supplyComponents().size());
+  EXPECT_EQ(5, airLoop.demandComponents().size());
+  EXPECT_EQ(5, plantLoop.supplyComponents().size());
+  EXPECT_EQ(7, plantLoop.demandComponents().size());
+
+  EXPECT_EQ(1, airLoop.supplyComponents(CoilUserDefined::iddObjectType()).size());
+  EXPECT_EQ(1, plantLoop.demandComponents(CoilUserDefined::iddObjectType()).size());
+
+  coil.remove();
+  EXPECT_EQ(2, airLoop.supplyComponents().size());
+  EXPECT_EQ(5, airLoop.demandComponents().size());
+  EXPECT_EQ(5, plantLoop.supplyComponents().size());
+  EXPECT_EQ(5, plantLoop.demandComponents().size());
+
+  EXPECT_TRUE(m.getConcreteModelObjects<CoilUserDefined>().empty());
+  EXPECT_EQ(0, airLoop.supplyComponents(CoilUserDefined::iddObjectType()).size());
+  EXPECT_EQ(0, plantLoop.demandComponents(CoilUserDefined::iddObjectType()).size());
+}
+
+// Add ONLY to PlantLoop
+// Test CoilUserDefined::remove and make sure that the plant is intact
+TEST_F(ModelFixture, CoilUserDefined_remove2) {
+  Model m;
+  CoilUserDefined coil(m);
+
+  PlantLoop plant(m);
+  plant.addDemandBranchForComponent(coil);
+  EXPECT_EQ(1, m.getConcreteModelObjects<CoilUserDefined>().size());
+  EXPECT_EQ(1, plant.demandComponents(CoilUserDefined::iddObjectType()).size());
+
+  coil.remove();
+  EXPECT_TRUE(plant.demandComponents(CoilUserDefined::iddObjectType()).empty());
+  EXPECT_TRUE(m.getConcreteModelObjects<CoilUserDefined>().empty());
+}
+
+// Add ONLY to PlantLoop
+// This time use removeDemandBranchWithComponent
+TEST_F(ModelFixture, CoilUserDefined_remove3) {
+  Model m;
+  CoilUserDefined coil(m);
+
+  PlantLoop plant(m);
+  plant.addDemandBranchForComponent(coil);
+
+  EXPECT_EQ(1, m.getConcreteModelObjects<CoilUserDefined>().size());
+  EXPECT_EQ(1, plant.demandComponents(CoilUserDefined::iddObjectType()).size());
+
+  plant.removeDemandBranchWithComponent(coil);
+  EXPECT_TRUE(plant.demandComponents(CoilUserDefined::iddObjectType()).empty());
+  EXPECT_TRUE(m.getConcreteModelObjects<CoilUserDefined>().empty());
+}
+
+// Add CoilUserDefined to AirLoopHVAC and PlantLoop
+// Test PlantLoop::removeDemandBranchWithComponent and make sure that the loops are intact
+// Test that the coil is still in the model and hooked up to AirLoopHVAC
+TEST_F(ModelFixture, CoilUserDefined_remove4) {
+  Model m;
+  CoilUserDefined coil(m);
+
+  AirLoopHVAC airLoop(m);
+  Node supplyOutletNode = airLoop.supplyOutletNode();
+
+  coil.addToNode(supplyOutletNode);
+
+  PlantLoop plant(m);
+  EXPECT_TRUE(plant.addDemandBranchForComponent(coil));
+
+  EXPECT_EQ(3, airLoop.supplyComponents().size());
+  EXPECT_EQ(1, m.getConcreteModelObjects<CoilUserDefined>().size());
+  EXPECT_EQ(1, plant.demandComponents(CoilUserDefined::iddObjectType()).size());
+
+  plant.removeDemandBranchWithComponent(coil);
+  EXPECT_TRUE(plant.demandComponents(CoilUserDefined::iddObjectType()).empty());
+  EXPECT_EQ(3, airLoop.supplyComponents().size());
+  EXPECT_EQ(1, m.getConcreteModelObjects<CoilUserDefined>().size());
+}
+
+TEST_F(ModelFixture, CoilUserDefined_Clone) {
+  Model m;
+  Model m2;
+
+  // Make a coil and fill out every field
+  CoilUserDefined coil(m);
+
+  EXPECT_EQ(13, m.numObjects());
+  EXPECT_EQ(0, m2.numObjects());
+  PlantLoop plantLoop(m);
+  AirLoopHVAC airLoop(m);
+  EXPECT_TRUE(plantLoop.addDemandBranchForComponent(coil));
+  Node supplyOutletNode = airLoop.supplyOutletNode();
+  EXPECT_TRUE(coil.addToNode(supplyOutletNode));
+  ThermalZone z(m);
+  EXPECT_TRUE(coil.setAmbientZone(z));
+
+  const int numObjects = m.numObjects();
+  EXPECT_EQ(0, m2.numObjects());
+
+  EXPECT_TRUE(coil.airLoopHVAC());
+  EXPECT_TRUE(coil.airInletModelObject());
+  EXPECT_TRUE(coil.airOutletModelObject());
+  EXPECT_TRUE(coil.plantLoop());
+  EXPECT_TRUE(coil.waterInletModelObject());
+  EXPECT_TRUE(coil.waterOutletModelObject());
+
+  {
+    auto clonedCoil = coil.clone(m2).cast<CoilUserDefined>();
+
+    EXPECT_FALSE(clonedCoil.airLoopHVAC());
+    EXPECT_FALSE(clonedCoil.airInletModelObject());
+    EXPECT_FALSE(clonedCoil.airOutletModelObject());
+    EXPECT_FALSE(clonedCoil.plantLoop());
+    EXPECT_FALSE(clonedCoil.waterInletModelObject());
+    EXPECT_FALSE(clonedCoil.waterOutletModelObject());
+
+    EXPECT_EQ(13, m2.numObjects());
+
+    EXPECT_NE(coil.handle(), clonedCoil.handle());
+    EXPECT_NE(coil.overallModelSimulationProgramCallingManager(), clonedCoil.overallModelSimulationProgramCallingManager());
+    EXPECT_NE(coil.modelSetupandSizingProgramCallingManager(), clonedCoil.modelSetupandSizingProgramCallingManager());
+    EXPECT_NE(coil.overallSimulationProgram(), clonedCoil.overallSimulationProgram());
+    EXPECT_NE(coil.initializationSimulationProgram(), clonedCoil.initializationSimulationProgram());
+    EXPECT_NE(coil.airOutletTemperatureActuator(), clonedCoil.airOutletTemperatureActuator());
+    EXPECT_NE(coil.airOutletHumidityRatioActuator(), clonedCoil.airOutletHumidityRatioActuator());
+    EXPECT_NE(coil.airMassFlowRateActuator(), clonedCoil.airMassFlowRateActuator());
+    EXPECT_NE(coil.plantMinimumMassFlowRateActuator(), clonedCoil.plantMinimumMassFlowRateActuator());
+    EXPECT_NE(coil.plantMaximumMassFlowRateActuator(), clonedCoil.plantMaximumMassFlowRateActuator());
+    EXPECT_NE(coil.plantDesignVolumeFlowRateActuator(), clonedCoil.plantDesignVolumeFlowRateActuator());
+    EXPECT_NE(coil.plantMassFlowRateActuator(), clonedCoil.plantMassFlowRateActuator());
+    EXPECT_NE(coil.plantOutletTemperatureActuator(), clonedCoil.plantOutletTemperatureActuator());
+    EXPECT_FALSE(clonedCoil.ambientZone());
+
+    ASSERT_TRUE(clonedCoil.airOutletTemperatureActuator().actuatedComponent());
+    EXPECT_EQ(clonedCoil, clonedCoil.airOutletTemperatureActuator().actuatedComponent().get());
+    ASSERT_TRUE(clonedCoil.airOutletHumidityRatioActuator().actuatedComponent());
+    EXPECT_EQ(clonedCoil, clonedCoil.airOutletHumidityRatioActuator().actuatedComponent().get());
+    ASSERT_TRUE(clonedCoil.airMassFlowRateActuator().actuatedComponent());
+    EXPECT_EQ(clonedCoil, clonedCoil.airMassFlowRateActuator().actuatedComponent().get());
+    ASSERT_TRUE(clonedCoil.plantMinimumMassFlowRateActuator().actuatedComponent());
+    EXPECT_EQ(clonedCoil, clonedCoil.plantMinimumMassFlowRateActuator().actuatedComponent().get());
+    ASSERT_TRUE(clonedCoil.plantMaximumMassFlowRateActuator().actuatedComponent());
+    EXPECT_EQ(clonedCoil, clonedCoil.plantMaximumMassFlowRateActuator().actuatedComponent().get());
+    ASSERT_TRUE(clonedCoil.plantDesignVolumeFlowRateActuator().actuatedComponent());
+    EXPECT_EQ(clonedCoil, clonedCoil.plantDesignVolumeFlowRateActuator().actuatedComponent().get());
+    ASSERT_TRUE(clonedCoil.plantMassFlowRateActuator().actuatedComponent());
+    EXPECT_EQ(clonedCoil, clonedCoil.plantMassFlowRateActuator().actuatedComponent().get());
+    ASSERT_TRUE(clonedCoil.plantOutletTemperatureActuator().actuatedComponent());
+    EXPECT_EQ(clonedCoil, clonedCoil.plantOutletTemperatureActuator().actuatedComponent().get());
+  }
+
+  {
+    // Same model
+    auto clonedCoil = coil.clone(m).cast<CoilUserDefined>();
+
+    EXPECT_FALSE(clonedCoil.airLoopHVAC());
+    EXPECT_FALSE(clonedCoil.airInletModelObject());
+    EXPECT_FALSE(clonedCoil.airOutletModelObject());
+    EXPECT_FALSE(clonedCoil.plantLoop());
+    EXPECT_FALSE(clonedCoil.waterInletModelObject());
+    EXPECT_FALSE(clonedCoil.waterOutletModelObject());
+
+    EXPECT_EQ(numObjects + 13, m.numObjects());
+
+    EXPECT_NE(coil.handle(), clonedCoil.handle());
+    EXPECT_NE(coil.overallModelSimulationProgramCallingManager(), clonedCoil.overallModelSimulationProgramCallingManager());
+    EXPECT_NE(coil.modelSetupandSizingProgramCallingManager(), clonedCoil.modelSetupandSizingProgramCallingManager());
+    EXPECT_NE(coil.overallSimulationProgram(), clonedCoil.overallSimulationProgram());
+    EXPECT_NE(coil.initializationSimulationProgram(), clonedCoil.initializationSimulationProgram());
+    EXPECT_NE(coil.airOutletTemperatureActuator(), clonedCoil.airOutletTemperatureActuator());
+    EXPECT_NE(coil.airOutletHumidityRatioActuator(), clonedCoil.airOutletHumidityRatioActuator());
+    EXPECT_NE(coil.airMassFlowRateActuator(), clonedCoil.airMassFlowRateActuator());
+    EXPECT_NE(coil.plantMinimumMassFlowRateActuator(), clonedCoil.plantMinimumMassFlowRateActuator());
+    EXPECT_NE(coil.plantMaximumMassFlowRateActuator(), clonedCoil.plantMaximumMassFlowRateActuator());
+    EXPECT_NE(coil.plantDesignVolumeFlowRateActuator(), clonedCoil.plantDesignVolumeFlowRateActuator());
+    EXPECT_NE(coil.plantMassFlowRateActuator(), clonedCoil.plantMassFlowRateActuator());
+    EXPECT_NE(coil.plantOutletTemperatureActuator(), clonedCoil.plantOutletTemperatureActuator());
+    EXPECT_FALSE(clonedCoil.ambientZone());
+
+    ASSERT_TRUE(clonedCoil.airOutletTemperatureActuator().actuatedComponent());
+    EXPECT_EQ(clonedCoil, clonedCoil.airOutletTemperatureActuator().actuatedComponent().get());
+    ASSERT_TRUE(clonedCoil.airOutletHumidityRatioActuator().actuatedComponent());
+    EXPECT_EQ(clonedCoil, clonedCoil.airOutletHumidityRatioActuator().actuatedComponent().get());
+    ASSERT_TRUE(clonedCoil.airMassFlowRateActuator().actuatedComponent());
+    EXPECT_EQ(clonedCoil, clonedCoil.airMassFlowRateActuator().actuatedComponent().get());
+    ASSERT_TRUE(clonedCoil.plantMinimumMassFlowRateActuator().actuatedComponent());
+    EXPECT_EQ(clonedCoil, clonedCoil.plantMinimumMassFlowRateActuator().actuatedComponent().get());
+    ASSERT_TRUE(clonedCoil.plantMaximumMassFlowRateActuator().actuatedComponent());
+    EXPECT_EQ(clonedCoil, clonedCoil.plantMaximumMassFlowRateActuator().actuatedComponent().get());
+    ASSERT_TRUE(clonedCoil.plantDesignVolumeFlowRateActuator().actuatedComponent());
+    EXPECT_EQ(clonedCoil, clonedCoil.plantDesignVolumeFlowRateActuator().actuatedComponent().get());
+    ASSERT_TRUE(clonedCoil.plantMassFlowRateActuator().actuatedComponent());
+    EXPECT_EQ(clonedCoil, clonedCoil.plantMassFlowRateActuator().actuatedComponent().get());
+    ASSERT_TRUE(clonedCoil.plantOutletTemperatureActuator().actuatedComponent());
+    EXPECT_EQ(clonedCoil, clonedCoil.plantOutletTemperatureActuator().actuatedComponent().get());
+  }
+}
+
+TEST_F(ModelFixture, CoilUserDefined_containingComponent) {
+
+  Model m;
+
+  {
+    CoilUserDefined coil(m);
+    EXPECT_EQ(0, coil.numberofAirConnections());
+
+    AirLoopHVACUnitarySystem unitary(m);
+    EXPECT_TRUE(unitary.setCoolingCoil(coil));
+    EXPECT_EQ(1, coil.numberofAirConnections());
+    boost::optional<HVACComponent> _c = coil.containingHVACComponent();
+    ASSERT_TRUE(_c);
+    EXPECT_EQ(_c->handle(), unitary.handle());
+  }
+
+  {
+    CoilUserDefined coil(m);
+    EXPECT_EQ(0, coil.numberofAirConnections());
+
+    AirLoopHVACUnitarySystem unitary(m);
+    EXPECT_TRUE(unitary.setHeatingCoil(coil));
+    EXPECT_EQ(1, coil.numberofAirConnections());
+    boost::optional<HVACComponent> _c = coil.containingHVACComponent();
+    ASSERT_TRUE(_c);
+    EXPECT_EQ(_c->handle(), unitary.handle());
+  }
+
+  {
+    CoilUserDefined coil(m);
+    EXPECT_EQ(0, coil.numberofAirConnections());
+
+    AirLoopHVACUnitarySystem unitary(m);
+    EXPECT_TRUE(unitary.setSupplementalHeatingCoil(coil));
+    EXPECT_EQ(1, coil.numberofAirConnections());
+    boost::optional<HVACComponent> _c = coil.containingHVACComponent();
+    ASSERT_TRUE(_c);
+    EXPECT_EQ(_c->handle(), unitary.handle());
+  }
+}