Repository: NREL/OpenStudio (language: python)

Issue:
Python Measures: Introduce "PythonEngine" library
Similar to `RubyEngine`, add a library with an API that derives from `ScriptEngine`. Look at the OpenStudioLabs project for a reference. It would be nice to have consistency between the ruby cmake target names, and the python targets.
Python Measures: Introduce "PythonEngine" library
Similar to `RubyEngine`, add a library with an API that derives from `ScriptEngine`. Look at the OpenStudioLabs project for a reference. It would be nice to have consistency between the ruby cmake target names, and the python targets.
Mod to Python Engine
Pull request overview
---------------------



### 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: Add Link
 - [ ] 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/CMake/compiler_flags.cmake b/CMake/compiler_flags.cmake
new file mode 100644
index 00000000000..3a68074318d
--- /dev/null
+++ b/CMake/compiler_flags.cmake
@@ -0,0 +1,6 @@
+#######################################################################
+#             G E N E R A T O R    E X P R E S S I O N S              #
+#######################################################################
+
+set(is_msvc_genex "$<CXX_COMPILER_ID:MSVC>")
+set(is_gnu_or_clang_genex "$<OR:$<CXX_COMPILER_ID:GNU>,$<CXX_COMPILER_ID:Clang>,$<CXX_COMPILER_ID:AppleClang>>")
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0683de4fb52..ae46365d02e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,4 +1,5 @@
-cmake_minimum_required(VERSION 3.14.0)
+message(STATUS "Using CMake ${CMAKE_VERSION}")
+cmake_minimum_required(VERSION 3.19.0)
 cmake_policy(SET CMP0048 NEW)
 
 set(CMAKE_CXX_STANDARD 20)
@@ -17,6 +18,8 @@ endif()
 # Project macro can only take digits in the version, it splits these out to specific variables
 project(OpenStudio VERSION 3.5.0)
 
+include(CMake/compiler_flags.cmake)
+
 # The RT Manifest file and C# SDK only support the digit portion of patch
 string(APPEND PROJECT_VERSION_PATCH_DIGIT ${PROJECT_VERSION_PATCH})
 
@@ -277,7 +280,8 @@ option(BUILD_NUGET_PACKAGE "Build NuGet Package" OFF)
 
 # Build Python bindings
 # Requires: SWIG Python
-option(BUILD_PYTHON_BINDINGS "Build Python bindings" OFF)
+option(BUILD_PYTHON_BINDINGS "Build Python bindings" ON)
+mark_as_advanced(BUILD_PYTHON_BINDINGS)
 
 # Build ctest testing
 # Requires: EnergyPlus
@@ -306,6 +310,12 @@ if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
   add_definitions(-Qunused-arguments)
 endif()
 
+include(CheckIncludeFile)
+check_include_file("unistd.h" HAVE_UNISTD_H)
+if(HAVE_UNISTD_H)
+  add_definitions(-DHAVE_UNISTD_H)
+endif()
+
 # Enable runtime checking features
 if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES ".*Clang")
 
@@ -343,6 +353,9 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES ".*Clan
 endif()
 
 if(APPLE)
+  # Without this, cmake will give "Module" libraries the ".so" extension on Apple
+  # This appears to be a legacy choice by the cmake developers, and seems out of place,
+  # so the OpenStudio project will use the library suffix ".dylib"
   find_library(COREFOUNDATION_LIBRARY CoreFoundation)
   add_definitions(-D_LIBCPP_ENABLE_CXX17_REMOVED_AUTO_PTR)
   #add_definitions(-DBOOST_SYSTEM_NO_DEPRECATED)
@@ -911,40 +924,7 @@ if(BUILD_NODE_MODULES)
 endif()
 
 if(BUILD_PYTHON_BINDINGS)
-  option(BUILD_PYTHON_PIP_PACKAGE "Prepare a folder to build a pip module" OFF)
-
-  # find_package(Python) has the problem that on github actions in particular it'll pick up the most recent python (eg 3.9) from the tool cache
-  # even if you have used the setup-python action and set it to 3.8
-  if (PYTHON_VERSION)
-    find_package(Python ${PYTHON_VERSION} EXACT COMPONENTS Interpreter Development REQUIRED)
-  else()
-    find_package(Python COMPONENTS Interpreter Development REQUIRED)
-  endif()
-
-  include_directories(SYSTEM ${Python_INCLUDE_DIRS})
-  set(ALL_PYTHON_BINDING_TARGETS "") # global list of python bindings
-  set(ALL_PYTHON_BINDING_DEPENDS "") # global list of library dependencies of the generated wrapper cxx files
-  set(ALL_PYTHON_WRAPPER_FILES "") # global list of generated wrapper cxx files
-  set(ALL_PYTHON_WRAPPER_TARGETS "") # global list targets that build generated wrapper cxx files
-  set(ALL_PYTHON_GENERATED_SRCS "") # global list of generated .py files
-
-  if(BUILD_PYTHON_PIP_PACKAGE)
-
-    set(PipRepositoryValues "testpypi;pypi" CACHE INTERNAL
-      "List of possible values for which repository to upload to")
-    set(PYTHON_PIP_REPOSITORY "testpypi" CACHE STRING "Which pip repository to use for upload")
-    set_property(CACHE PYTHON_PIP_REPOSITORY PROPERTY STRINGS ${PipRepositoryValues})
-
-    if(NOT PYTHON_PIP_REPOSITORY IN_LIST PipRepositoryValues)
-      message(FATAL_ERROR "PYTHON_PIP_REPOSITORY must be one of ${PipRepositoryValues}")
-    endif()
-
-    set(PYTHON_PACKAGE_FOLDER "${PROJECT_BINARY_DIR}/Products/python_package")
-    file(MAKE_DIRECTORY "${PYTHON_PACKAGE_FOLDER}")
-    set(ALL_PYTHON_PACKAGE_TARGETS "") # global list of python bindings
-    set(ALL_PYTHON_PACKAGE_GENERATED_SRCS "") # global list of generated .py files for pyhton_package
-
-  endif()
+  include(python/SetupPython.cmake)
 endif()
 
 # Threading library
@@ -1005,6 +985,8 @@ set(project_directories
   sdd
   lib
   install_utility
+  scriptengine
+  workflow
 )
 
 # resources must be added after find EnergyPlus
diff --git a/ConanInstall.cmake b/ConanInstall.cmake
index 10f56fd2983..e3057a485d2 100644
--- a/ConanInstall.cmake
+++ b/ConanInstall.cmake
@@ -177,6 +177,7 @@ if(NOT CONAN_OPENSTUDIO_ALREADY_RUN)
     "geographiclib/1.52#76536a9315a003ef3511919310b2fe37"
     "swig/4.0.2#9fcccb1e39eed9acd53a4363d8129be5"
     "tinygltf/2.5.0#c8b2aca9505e86312bb42aa0e1c639ec"
+    "cli11/2.2.0#33cd38722fa134b15ae308dfb4e6c942"
     ${CONAN_GTEST}
     ${CONAN_BENCHMARK}
     # Override to avoid dependency mismatches
diff --git a/Jenkinsfile_develop_osx b/Jenkinsfile_develop_osx
index d7d18e8711f..5226bdb90e0 100644
--- a/Jenkinsfile_develop_osx
+++ b/Jenkinsfile_develop_osx
@@ -1,6 +1,6 @@
 //Jenkins pipelines are stored in shared libaries. Please see: https://github.com/NREL/cbci_jenkins_libs
 
-@Library('cbci_shared_libs') _
+@Library('cbci_shared_libs@pythonengine') _
 
 // Build for PR to develop branch only.
 if ((env.CHANGE_ID) && (env.CHANGE_TARGET) ) {
diff --git a/Jenkinsfile_develop_ubuntu_1804 b/Jenkinsfile_develop_ubuntu_1804
index d056d7939a9..149882c9e9e 100644
--- a/Jenkinsfile_develop_ubuntu_1804
+++ b/Jenkinsfile_develop_ubuntu_1804
@@ -1,7 +1,7 @@
 
 //Jenkins pipelines are stored in shared libaries. Please see: https://github.com/NREL/cbci_jenkins_libs
 
-@Library('cbci_shared_libs') _
+@Library('cbci_shared_libs@pythonengine') _
 
 // Build for PR to develop branch only.
 if ((env.CHANGE_ID) && (env.CHANGE_TARGET) ) {
diff --git a/Jenkinsfile_develop_ubuntu_2004 b/Jenkinsfile_develop_ubuntu_2004
index 1c9eea6c747..0d841d799f4 100644
--- a/Jenkinsfile_develop_ubuntu_2004
+++ b/Jenkinsfile_develop_ubuntu_2004
@@ -1,7 +1,7 @@
 
 //Jenkins pipelines are stored in shared libaries. Please see: https://github.com/NREL/cbci_jenkins_libs
 
-@Library('cbci_shared_libs') _
+@Library('cbci_shared_libs@pythonengine') _
 
 // Build for PR to develop branch only.
 if ((env.CHANGE_ID) && (env.CHANGE_TARGET) ) {
diff --git a/Jenkinsfile_develop_windows b/Jenkinsfile_develop_windows
index cb285700d5a..f7c055b5c35 100644
--- a/Jenkinsfile_develop_windows
+++ b/Jenkinsfile_develop_windows
@@ -1,6 +1,6 @@
 //Jenkins pipelines are stored in shared libaries. Please see: https://github.com/NREL/cbci_jenkins_libs
 
-@Library('cbci_shared_libs') _
+@Library('cbci_shared_libs@pythonengine') _
 
 // Build for PR to develop branch only.
 if ((env.CHANGE_ID) && (env.CHANGE_TARGET) ) {
diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt
index 4d124987b7f..8fbce4ff188 100644
--- a/python/CMakeLists.txt
+++ b/python/CMakeLists.txt
@@ -1,81 +1,4 @@
+# OpenStudio Python components
 
-# DLM: currently there are some multiply defined symbols, force them for now, investigate later
-#set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} /FORCE:MULTIPLE")
-
-# ignore deprecated warnings since we will export deprecated methods
-#add_definitions(/wd4996)
-
-# ignore macro redefinition
-#add_definitions(/wd4005)
-
-## build a single python dll
-#add_library(
-#  openstudio_python
-#  MODULE
-#  ${ALL_PYTHON_WRAPPER_FILES}
-#)
-#add_dependencies(openstudio_python ${ALL_PYTHON_WRAPPER_TARGETS})
-
-#set_source_files_properties(${ALL_PYTHON_WRAPPER_FILES} PROPERTIES GENERATED TRUE)
-
-#target_link_libraries(openstudio_python
-# ${ALL_PYTHON_BINDING_DEPENDS}
-# ${Boost_LIBRARIES}
-# ${Python_LIBRARIES}
-#)
-
-#install(TARGETS openstudio_python DESTINATION Python CONFIGURATIONS COMPONENT "Python")
-
-install(FILES "openstudio.py" DESTINATION Python COMPONENT "Python")
-
-configure_file(${CMAKE_CURRENT_SOURCE_DIR}/openstudio.py ${PROJECT_BINARY_DIR}/Products/python/openstudio.py COPYONLY)
-
-foreach(p ${ALL_PYTHON_BINDING_TARGETS})
-  target_link_libraries(${p}
-    PRIVATE
-    openstudiolib
-  )
-endforeach()
-
-# convenience target to build the SDK
-add_custom_target(python_sdk ALL
-  DEPENDS
-    ${ALL_PYTHON_BINDING_TARGETS}
-    ${ALL_PYTHON_GENERATED_SRCS}
-)
-
-
-if (BUILD_PYTHON_PIP_PACKAGE)
-
-  # configure_file(${CMAKE_CURRENT_SOURCE_DIR}/openstudio_dynamic.py ${PROJECT_BINARY_DIR}/Products/python/openstudio_dynamic.py COPYONLY)
-  configure_file(${CMAKE_CURRENT_SOURCE_DIR}/openstudio.py ${PYTHON_PACKAGE_FOLDER}/openstudio/__init__.py COPYONLY)
-
-  if(PYTHON_PIP_REPOSITORY STREQUAL "testpypi")
-    execute_process( COMMAND ${Python_EXECUTABLE} "${CMAKE_CURRENT_SOURCE_DIR}/find_pypi_tag.py" OUTPUT_VARIABLE PYPI_VERSION)
-  else()
-    execute_process( COMMAND ${Python_EXECUTABLE} "${CMAKE_CURRENT_SOURCE_DIR}/find_pypi_tag.py" "--pypi" OUTPUT_VARIABLE PYPI_VERSION)
-  endif()
-  message("On Repository '${PYTHON_PIP_REPOSITORY}', computed next available version as PYPI_VERSION=${PYPI_VERSION}")
-  configure_file(${CMAKE_CURRENT_SOURCE_DIR}/setup.py ${PYTHON_PACKAGE_FOLDER}/setup.py @ONLY)
-
-  foreach(p ${ALL_PYTHON_PACKAGE_TARGETS})
-    target_link_libraries(${p}
-      PRIVATE
-      openstudiolib
-    )
-  endforeach()
-
-  add_custom_target(python_package
-    COMMAND ${CMAKE_COMMAND} -E rm -Rf "build/" "dist/" "openstudio.egg-info/"
-    COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:openstudiolib> "openstudio/"
-    COMMAND ${CMAKE_COMMAND} -E echo "To create the package, cd 'Products/python_package/'"
-    COMMAND ${CMAKE_COMMAND} -E echo "${Python_EXECUTABLE} setup.py bdist_wheel"
-    COMMAND ${CMAKE_COMMAND} -E echo "${Python_EXECUTABLE} -m twine upload --verbose --skip-existing --repository ${PYTHON_PIP_REPOSITORY} dist/*"
-    DEPENDS
-      ${ALL_PYTHON_BINDING_TARGETS}
-      ${ALL_PYTHON_PACKAGE_TARGETS}
-      ${ALL_PYTHON_PACKAGE_GENERATED_SRCS}
-    WORKING_DIRECTORY ${PYTHON_PACKAGE_FOLDER}
-  )
-
-endif()
+add_subdirectory(module)
+add_subdirectory(engine)
diff --git a/python/SetupPython.cmake b/python/SetupPython.cmake
new file mode 100644
index 00000000000..8fb4af23926
--- /dev/null
+++ b/python/SetupPython.cmake
@@ -0,0 +1,34 @@
+option(BUILD_PYTHON_PIP_PACKAGE "Prepare a folder to build a pip module" OFF)
+
+# find_package(Python) has the problem that on github actions in particular it'll pick up the most recent python (eg 3.9) from the tool cache
+# even if you have used the setup-python action and set it to 3.8
+if (PYTHON_VERSION)
+  find_package(Python ${PYTHON_VERSION} EXACT COMPONENTS Interpreter Development REQUIRED)
+else()
+  find_package(Python COMPONENTS Interpreter Development REQUIRED)
+endif()
+
+get_filename_component(Python_PROGRAM_NAME ${Python_EXECUTABLE} NAME)
+
+include_directories(SYSTEM ${Python_INCLUDE_DIRS})
+set(ALL_PYTHON_BINDING_TARGETS "") # global list of python bindings
+set(ALL_PYTHON_BINDING_DEPENDS "") # global list of library dependencies of the generated wrapper cxx files
+set(ALL_PYTHON_WRAPPER_FILES "") # global list of generated wrapper cxx files
+set(ALL_PYTHON_WRAPPER_TARGETS "") # global list targets that build generated wrapper cxx files
+set(ALL_PYTHON_GENERATED_SRCS "") # global list of generated .py files
+
+if(BUILD_PYTHON_PIP_PACKAGE)
+  set(PipRepositoryValues "testpypi;pypi" CACHE INTERNAL
+    "List of possible values for which repository to upload to")
+  set(PYTHON_PIP_REPOSITORY "testpypi" CACHE STRING "Which pip repository to use for upload")
+  set_property(CACHE PYTHON_PIP_REPOSITORY PROPERTY STRINGS ${PipRepositoryValues})
+
+  if(NOT PYTHON_PIP_REPOSITORY IN_LIST PipRepositoryValues)
+    message(FATAL_ERROR "PYTHON_PIP_REPOSITORY must be one of ${PipRepositoryValues}")
+  endif()
+
+  set(PYTHON_PACKAGE_FOLDER "${PROJECT_BINARY_DIR}/Products/python_package")
+  file(MAKE_DIRECTORY "${PYTHON_PACKAGE_FOLDER}")
+  set(ALL_PYTHON_PACKAGE_TARGETS "") # global list of python bindings
+  set(ALL_PYTHON_PACKAGE_GENERATED_SRCS "") # global list of generated .py files for pyhton_package
+endif()
diff --git a/python/engine/CMakeLists.txt b/python/engine/CMakeLists.txt
new file mode 100644
index 00000000000..1ba93d136fd
--- /dev/null
+++ b/python/engine/CMakeLists.txt
@@ -0,0 +1,47 @@
+add_custom_command(
+  OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/SWIGPythonRuntime.hxx"
+  COMMAND ${CMAKE_COMMAND} -E env SWIG_LIB="${SWIG_LIB}"
+          "${SWIG_EXECUTABLE}"
+          "-python"
+          -external-runtime "${CMAKE_CURRENT_BINARY_DIR}/SWIGPythonRuntime.hxx"
+)
+
+configure_file(PythonConfig.hxx.in PythonConfig.hxx)
+
+add_library(
+  pythonengine
+  MODULE
+  PythonEngine.hpp
+  PythonEngine.cpp
+  ${CMAKE_CURRENT_BINARY_DIR}/SWIGPythonRuntime.hxx
+)
+
+file(GENERATE
+  OUTPUT $<TARGET_FILE_DIR:pythonengine>/openstudiodev.py
+  INPUT openstudiodev.py.in
+)
+
+target_include_directories(pythonengine
+  PRIVATE
+  ${CMAKE_CURRENT_SOURCE_DIR}
+  ${CMAKE_CURRENT_BINARY_DIR}
+)
+
+target_link_libraries(
+  pythonengine
+  PRIVATE
+  openstudio_scriptengine
+  openstudio_utilities_minimal
+  CONAN_PKG::fmt
+  Python::Python
+)
+
+# tp_print is deprecated at of 3.8.
+target_compile_options(pythonengine PRIVATE
+  "$<${is_msvc_genex}:/wd4996>"
+  "$<${is_gnu_or_clang_genex}:-Wno-deprecated-declarations>"
+)
+
+target_compile_definitions(pythonengine PRIVATE openstudio_scriptengine_EXPORTS SHARED_OS_LIBS)
+
+install(TARGETS pythonengine EXPORT openstudio DESTINATION bin COMPONENT "CLI")
diff --git a/python/engine/PythonConfig.hxx.in b/python/engine/PythonConfig.hxx.in
new file mode 100644
index 00000000000..98f7ac44ebe
--- /dev/null
+++ b/python/engine/PythonConfig.hxx.in
@@ -0,0 +1,12 @@
+#ifndef PythonConfig_Included
+#define PythonConfig_Included
+
+#include <string>
+
+namespace openstudio {
+
+constexpr auto pythonProgramName = "${Python_PROGRAM_NAME}";
+
+} // namespace openstudio
+
+#endif // PythonConfig_Included
diff --git a/python/engine/PythonEngine.cpp b/python/engine/PythonEngine.cpp
new file mode 100644
index 00000000000..e7ead6941ed
--- /dev/null
+++ b/python/engine/PythonEngine.cpp
@@ -0,0 +1,182 @@
+#include "PythonEngine.hpp"
+#include <utilities/core/ApplicationPathHelpers.hpp>
+#include <fmt/format.h>
+#include <stdexcept>
+#include <string>
+#include <iostream>
+
+#ifdef __GNUC__
+#  pragma GCC diagnostic push
+#  pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+#endif
+
+#include <PythonConfig.hxx>
+#include <Python.h>
+#include <SWIGPythonRuntime.hxx>
+
+#ifdef __GNUC__
+#  pragma GCC diagnostic pop
+#endif
+
+namespace openstudio {
+
+void PythonEngine::pyimport(const std::string& importName, const std::string& includePath) {
+  if (!includePath.empty()) {
+    PyObject* sys = PyImport_ImportModule("sys");
+    PyObject* sysPath = PyObject_GetAttrString(sys, "path");
+    // fmt::print("Prepending '{}' to sys.path\n", includePath);
+    PyObject* unicodeIncludePath = PyUnicode_FromString(includePath.c_str());
+    PyList_Insert(sysPath, 0, unicodeIncludePath);
+  }
+  PyImport_ImportModule(importName.c_str());
+}
+
+PythonEngine::PythonEngine(int argc, char* argv[]) : ScriptEngine(argc, argv), program(Py_DecodeLocale(pythonProgramName, nullptr)) {
+  Py_SetProgramName(program);  // optional but recommended
+
+  Py_Initialize();
+  PyObject* m = PyImport_AddModule("__main__");
+  if (m == nullptr) {
+    throw std::runtime_error("Unable to add module __main__ for python script execution");
+  }
+  m_globalDict = PyModule_GetDict(m);
+
+  //PyRun_SimpleString("from time import time,ctime\n"
+  //                   "print('Today is', ctime(time()))\n");
+  importOpenStudio();
+}
+
+PythonEngine::~PythonEngine() {
+  if (Py_FinalizeEx() < 0) {
+    exit(120);
+  }
+  PyMem_RawFree(program);
+}
+
+void PythonEngine::importOpenStudio() {
+  // generic_string() converts to a POSIX path, with forward slashes, so that pyimport doesn't choke on backslashes understood as escape sequence
+  if (moduleIsRunningFromBuildDirectory()) {
+    const auto bindingsDir = getOpenStudioModuleDirectory();
+    pyimport("openstudiodev", bindingsDir.generic_string());
+  } else {
+    const auto bindingsDir = getOpenStudioModuleDirectory() / "../Python";
+    pyimport("openstudio", bindingsDir.generic_string());
+  }
+  // Somehow that doesn't suffice to register it...
+  exec("import openstudio");
+}
+
+struct PythonObject
+{
+  PythonObject() = default;
+
+  PythonObject(PyObject* obj) noexcept : obj_(obj) {
+    if (obj_) {
+      Py_INCREF(obj_);
+    }
+  }
+
+  PythonObject(const PythonObject& other) noexcept : obj_(other.obj_) {
+    if (obj_) {
+      Py_INCREF(obj_);
+    }
+  }
+
+  PythonObject(PythonObject&& other) noexcept : obj_(other.obj_) {
+    // no reason to inc/dec, we just stole the ref counted object
+    // from other
+    other.obj_ = nullptr;
+  }
+
+  PythonObject& operator=(const PythonObject& rhs) noexcept {
+    if (&rhs != this) {
+      obj_ = rhs.obj_;
+
+      if (obj_) {
+        Py_INCREF(obj_);
+      }
+    }
+
+    return *this;
+  }
+
+  PythonObject& operator=(PythonObject&& rhs) noexcept {
+    if (&rhs != this) {
+      obj_ = rhs.obj_;
+      rhs.obj_ = nullptr;
+    }
+
+    return *this;
+  }
+
+  ~PythonObject() {
+    if (obj_) {
+      Py_DECREF(obj_);
+    }
+  }
+
+  PyObject* obj_ = nullptr;
+};
+
+void PythonEngine::exec(std::string_view sv) {
+  std::string command{sv};
+
+  PyObject* v = PyRun_String(command.c_str(), Py_file_input, m_globalDict, m_globalDict);
+  // PyObject* v = PyRun_SimpleString(command.c_str());
+  if (v == nullptr) {
+    PyErr_Print();
+    throw std::runtime_error("Error executing Python code");
+  }
+
+  Py_DECREF(v);
+}
+
+ScriptObject PythonEngine::eval(std::string_view sv) {
+  std::string command{sv};
+
+  PyObject* v = PyRun_String(command.c_str(), Py_eval_input, m_globalDict, m_globalDict);
+
+  if (v == nullptr) {
+    PyErr_Print();
+    throw std::runtime_error("Error executing Python code");
+  }
+
+  // share in ownership
+  PythonObject return_value(v);
+
+  // decref count returned from Python
+  Py_DECREF(v);
+
+  return ScriptObject{return_value};
+}
+
+void* PythonEngine::getAs_impl(ScriptObject& obj, const std::type_info& ti) {
+
+  auto val = std::any_cast<PythonObject>(obj.object);
+
+  const auto& type_name = getRegisteredTypeName(ti);
+
+  void* return_value = nullptr;
+
+  auto* type = SWIG_Python_TypeQuery(type_name.c_str());
+
+  if (!type) {
+    throw std::runtime_error("Unable to find type in SWIG");
+  }
+
+  const auto result = SWIG_Python_ConvertPtr(val.obj_, &return_value, type, 0);
+
+  if (!SWIG_IsOK(result)) {
+    throw std::runtime_error("Error getting object from SWIG/Python");
+  }
+
+  return return_value;
+}
+}  // namespace openstudio
+
+extern "C"
+{
+  openstudio::ScriptEngine* makeScriptEngine(int argc, char* argv[]) {
+    return new openstudio::PythonEngine(argc, argv);
+  }
+}
diff --git a/python/engine/PythonEngine.hpp b/python/engine/PythonEngine.hpp
new file mode 100644
index 00000000000..9bd5754b8a5
--- /dev/null
+++ b/python/engine/PythonEngine.hpp
@@ -0,0 +1,45 @@
+#ifndef PYTHONENGINE_included
+#define PYTHONENGINE_included
+
+#include <ScriptEngine.hpp>
+#include <ScriptEngineAPI.hpp>
+
+#ifndef PyObject_HEAD
+struct _object;
+using PyObject = _object;
+#endif
+
+namespace openstudio {
+
+class PythonEngine final : public ScriptEngine
+{
+ public:
+  PythonEngine(int argc, char* argv[]);
+  ~PythonEngine() override;
+
+  PythonEngine(const PythonEngine&) = delete;
+  PythonEngine(PythonEngine&&) = delete;
+  PythonEngine& operator=(const PythonEngine&) = delete;
+  PythonEngine& operator=(PythonEngine&&) = delete;
+
+  ScriptObject eval(std::string_view sv) override;
+  void exec(std::string_view sv) override;
+
+ protected:
+  void* getAs_impl(ScriptObject& obj, const std::type_info&) override;
+  void importOpenStudio();
+  void pyimport(const std::string& importName, const std::string& includePath);
+
+ private:
+  wchar_t* program;
+  PyObject* m_globalDict;
+};
+
+}  // namespace openstudio
+
+extern "C"
+{
+  SCRIPTENGINE_API openstudio::ScriptEngine* makeScriptEngine(int argc, char* argv[]);
+}
+
+#endif
diff --git a/python/engine/openstudiodev.py.in b/python/engine/openstudiodev.py.in
new file mode 100644
index 00000000000..3e6e0f56eeb
--- /dev/null
+++ b/python/engine/openstudiodev.py.in
@@ -0,0 +1,3 @@
+import sys
+sys.path.insert(0, '$<TARGET_FILE_DIR:python_OpenStudioUtilitiesCore>')
+import openstudio
diff --git a/python/module/CMakeLists.txt b/python/module/CMakeLists.txt
new file mode 100644
index 00000000000..f174e2bc85f
--- /dev/null
+++ b/python/module/CMakeLists.txt
@@ -0,0 +1,54 @@
+install(FILES "openstudio.py" DESTINATION Python COMPONENT "Python")
+
+configure_file(${CMAKE_CURRENT_SOURCE_DIR}/openstudio.py ${PROJECT_BINARY_DIR}/Products/python/openstudio.py COPYONLY)
+
+foreach(p ${ALL_PYTHON_BINDING_TARGETS})
+  target_link_libraries(${p}
+    PRIVATE
+    openstudiolib
+    Python::Python
+  )
+endforeach()
+
+# convenience target to build the SDK
+add_custom_target(python_sdk ALL
+  DEPENDS
+    ${ALL_PYTHON_BINDING_TARGETS}
+    ${ALL_PYTHON_GENERATED_SRCS}
+)
+
+
+if (BUILD_PYTHON_PIP_PACKAGE)
+
+  # configure_file(${CMAKE_CURRENT_SOURCE_DIR}/openstudio_dynamic.py ${PROJECT_BINARY_DIR}/Products/python/openstudio_dynamic.py COPYONLY)
+  configure_file(${CMAKE_CURRENT_SOURCE_DIR}/openstudio.py ${PYTHON_PACKAGE_FOLDER}/openstudio/__init__.py COPYONLY)
+
+  if(PYTHON_PIP_REPOSITORY STREQUAL "testpypi")
+    execute_process( COMMAND ${Python_EXECUTABLE} "${CMAKE_CURRENT_SOURCE_DIR}/find_pypi_tag.py" OUTPUT_VARIABLE PYPI_VERSION)
+  else()
+    execute_process( COMMAND ${Python_EXECUTABLE} "${CMAKE_CURRENT_SOURCE_DIR}/find_pypi_tag.py" "--pypi" OUTPUT_VARIABLE PYPI_VERSION)
+  endif()
+  message("On Repository '${PYTHON_PIP_REPOSITORY}', computed next available version as PYPI_VERSION=${PYPI_VERSION}")
+  configure_file(${CMAKE_CURRENT_SOURCE_DIR}/setup.py ${PYTHON_PACKAGE_FOLDER}/setup.py @ONLY)
+
+  foreach(p ${ALL_PYTHON_PACKAGE_TARGETS})
+    target_link_libraries(${p}
+      PRIVATE
+      openstudiolib
+    )
+  endforeach()
+
+  add_custom_target(python_package
+    COMMAND ${CMAKE_COMMAND} -E rm -Rf "build/" "dist/" "openstudio.egg-info/"
+    COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:openstudiolib> "openstudio/"
+    COMMAND ${CMAKE_COMMAND} -E echo "To create the package, cd 'Products/python_package/'"
+    COMMAND ${CMAKE_COMMAND} -E echo "${Python_EXECUTABLE} setup.py bdist_wheel"
+    COMMAND ${CMAKE_COMMAND} -E echo "${Python_EXECUTABLE} -m twine upload --verbose --skip-existing --repository ${PYTHON_PIP_REPOSITORY} dist/*"
+    DEPENDS
+      ${ALL_PYTHON_BINDING_TARGETS}
+      ${ALL_PYTHON_PACKAGE_TARGETS}
+      ${ALL_PYTHON_PACKAGE_GENERATED_SRCS}
+    WORKING_DIRECTORY ${PYTHON_PACKAGE_FOLDER}
+  )
+
+endif()
diff --git a/python/find_pypi_tag.py b/python/module/find_pypi_tag.py
similarity index 99%
rename from python/find_pypi_tag.py
rename to python/module/find_pypi_tag.py
index 1b3ec3c5b20..e6828ec86e6 100644
--- a/python/find_pypi_tag.py
+++ b/python/module/find_pypi_tag.py
@@ -5,7 +5,7 @@
 import argparse
 from typing import List
 
-REPO_ROOT = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..')
+REPO_ROOT = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../..')
 
 
 def parse_pypi_version(pypi: bool = False):
diff --git a/python/openstudio.py b/python/module/openstudio.py
similarity index 100%
rename from python/openstudio.py
rename to python/module/openstudio.py
diff --git a/python/setup.py b/python/module/setup.py
similarity index 100%
rename from python/setup.py
rename to python/module/setup.py
diff --git a/resources/CMakeLists.txt b/resources/CMakeLists.txt
index a7b682aebd0..ca59c74f600 100644
--- a/resources/CMakeLists.txt
+++ b/resources/CMakeLists.txt
@@ -396,4 +396,7 @@ CREATE_SRC_GROUPS("${airflow_resources_src}")
 
 ########################################################
 
+# Copy it for testing
+file(COPY "Examples" DESTINATION .)
+# And install
 install(DIRECTORY "Examples" DESTINATION . COMPONENT "CLI")
diff --git a/resources/Examples/compact_osw/compact_python_only.osw b/resources/Examples/compact_osw/compact_python_only.osw
new file mode 100644
index 00000000000..6041607d25d
--- /dev/null
+++ b/resources/Examples/compact_osw/compact_python_only.osw
@@ -0,0 +1,12 @@
+{
+	"weather_file": "srrl_2013_amy.epw",
+	"seed_file": "seb.osm",
+	"steps": [
+		{
+			"measure_dir_name": "IncreaseRoofRValuePython",
+			"arguments": {
+				"r_value": 45
+			}
+		}
+	]
+}
diff --git a/resources/Examples/compact_osw/compact_python_only_twomeasures.osw b/resources/Examples/compact_osw/compact_python_only_twomeasures.osw
new file mode 100644
index 00000000000..0662c4c9c4a
--- /dev/null
+++ b/resources/Examples/compact_osw/compact_python_only_twomeasures.osw
@@ -0,0 +1,18 @@
+{
+	"weather_file": "srrl_2013_amy.epw",
+	"seed_file": "seb.osm",
+	"steps": [
+		{
+			"measure_dir_name": "IncreaseRoofRValuePython",
+			"arguments": {
+				"r_value": 45
+			}
+		},
+		{
+			"measure_dir_name": "PythonExampleEnergyPlusMeasure",
+			"arguments": {
+				"zone_name": "My New Zone"
+			}
+		}
+	]
+}
diff --git a/resources/Examples/compact_osw/compact_ruby_and_python.osw b/resources/Examples/compact_osw/compact_ruby_and_python.osw
new file mode 100644
index 00000000000..de4debee4e8
--- /dev/null
+++ b/resources/Examples/compact_osw/compact_ruby_and_python.osw
@@ -0,0 +1,26 @@
+{
+	"weather_file": "srrl_2013_amy.epw",
+	"seed_file": "seb.osm",
+	"steps": [
+		{
+			"measure_dir_name": "IncreaseWallRValue",
+			"arguments": {}
+		},
+		{
+			"measure_dir_name": "IncreaseRoofRValuePython",
+			"arguments": {
+				"r_value": 45
+			}
+		},
+		{
+			"measure_dir_name": "SetEplusInfiltration",
+			"arguments": {
+				"flowPerZoneFloorArea": 10.76
+			}
+		},
+		{
+			"measure_dir_name": "Standard Reports",
+			"arguments": {}
+		}
+	]
+}
diff --git a/resources/Examples/compact_osw/compact.osw b/resources/Examples/compact_osw/compact_ruby_only.osw
similarity index 100%
rename from resources/Examples/compact_osw/compact.osw
rename to resources/Examples/compact_osw/compact_ruby_only.osw
diff --git a/resources/Examples/compact_osw/measures/IncreaseRoofRValue/measure.rb b/resources/Examples/compact_osw/measures/IncreaseRoofRValue/measure.rb
index da4961dd7ef..59c63b35bf8 100644
--- a/resources/Examples/compact_osw/measures/IncreaseRoofRValue/measure.rb
+++ b/resources/Examples/compact_osw/measures/IncreaseRoofRValue/measure.rb
@@ -1,5 +1,5 @@
 #start the measure
-class IncreaseInsulationRValueForRoofsByPercentage < OpenStudio::Ruleset::ModelUserScript
+class IncreaseInsulationRValueForRoofsByPercentage < OpenStudio::Measure::ModelMeasure
 
   #define the name that a user will see
   def name
@@ -8,10 +8,10 @@ def name
 
   #define the arguments that the user will input
   def arguments(model)
-    args = OpenStudio::Ruleset::OSArgumentVector.new
+    args = OpenStudio::Measure::OSArgumentVector.new
 
     #make an argument insulation R-value
-    r_value = OpenStudio::Ruleset::OSArgument::makeDoubleArgument("r_value",true)
+    r_value = OpenStudio::Measure::OSArgument::makeDoubleArgument("r_value",true)
     r_value.setDisplayName("Percentage Increase of R-value for Roof Insulation.")
     r_value.setDefaultValue(30.0)
     args << r_value
diff --git a/resources/Examples/compact_osw/measures/IncreaseRoofRValuePython/measure.py b/resources/Examples/compact_osw/measures/IncreaseRoofRValuePython/measure.py
new file mode 100644
index 00000000000..81edef77520
--- /dev/null
+++ b/resources/Examples/compact_osw/measures/IncreaseRoofRValuePython/measure.py
@@ -0,0 +1,284 @@
+import openstudio
+import typing
+
+
+class IncreaseInsulationRValueForRoofsByPercentagePython(openstudio.measure.ModelMeasure):
+
+    def name(self):
+        """
+        Return the human readable name.
+        Measure name should be the title case of the class name.
+        """
+        return "Increase R-value of Insulation for Roofs by a Specified Percentage."
+
+    def description(self):
+        """
+        human readable description
+        """
+        return "DESCRIPTION_TEXT"
+
+    def modeler_description(self):
+        """
+        human readable description of modeling approach
+        """
+        return "MODELER_DESCRIPTION_TEXT"
+
+    def arguments(self, model: typing.Optional[openstudio.model.Model] = None):
+        """
+        define what happens when the measure is run
+        """
+        args = openstudio.measure.OSArgumentVector()
+
+        r_value = openstudio.measure.OSArgument.makeDoubleArgument("r_value", True)
+        r_value.setDisplayName("Percentage Increase of R-value for Roof Insulation.")
+        r_value.setDefaultValue(30.0)
+
+        args.append(r_value)
+
+        return args
+
+    def run(self,
+            model: openstudio.model.Model,
+            runner: openstudio.measure.OSRunner,
+            user_arguments: openstudio.measure.OSArgumentMap):
+        """
+        define what happens when the measure is run
+        """
+        super().run(model, runner, user_arguments)  # Do **NOT** remove this line
+
+        if not(runner.validateUserArguments(self.arguments(model),
+                                            user_arguments)):
+            return False
+
+        r_value = runner.getDoubleArgumentValue("r_value", user_arguments)
+
+        # set limit for minimum insulation. This is used to limit input and for inferring insulation layer in construction.
+        min_expected_r_value_ip = 1  # ip units
+        min_expected_r_value_si = openstudio.convert(min_expected_r_value_ip, "ft^2*h*R/Btu","m^2*K/W").get()
+
+        # check the R-value for reasonableness
+        if r_value < -100:
+            runner.registerError("Percentage increase less than -100% is not valid.")
+            return False
+
+        # create an array of roofs and find range of starting construction R-value (not just insulation layer)
+        surfaces = model.getSurfaces()
+        exterior_surfaces = []
+        exterior_surface_constructions = []
+        exterior_surface_construction_names = []
+        roof_resistance = []
+        for surface in surfaces:
+            if surface.outsideBoundaryCondition() != "Outdoors" or surface.surfaceType() != "RoofCeiling":
+                continue
+            exterior_surfaces.append(surface)
+            roof_const = surface.construction().get()
+            # only add construction if it hasn't been added yet
+            if not roof_const.nameString() in exterior_surface_construction_names:
+                exterior_surface_constructions.append(roof_const.to_Construction().get())
+
+            exterior_surface_construction_names.append(roof_const.nameString())
+            roof_resistance.append(1.0 / roof_const.thermalConductance().get())
+
+        # nothing will be done if there are no exterior surfaces
+        if not exterior_surfaces:
+            runner.registerAsNotApplicable("Model does not have any roofs.")
+            return True
+
+        # report strings for initial condition
+        initial_string = []
+        for exterior_surface_construction in exterior_surface_constructions:
+            # unit conversion of roof insulation from SI units (M^2*K/W) to IP units (ft^2*h*R/Btu)
+            initial_conductance_ip = openstudio.convert(
+                1.0 / exterior_surface_construction.thermalConductance().get(),
+                "m^2*K/W", "ft^2*h*R/Btu").get()
+            initial_string.append(
+                f"{exterior_surface_construction.nameString()} (R-{initial_conductance_ip:.1f})"
+            )
+
+        runner.registerInitialCondition(f"The building had {len(initial_string)} roof constructions: {', '.join(sorted(initial_string))}.")
+
+        # hashes to track constructions and materials made by the measure, to avoid duplicates
+        constructions_hash_old_new = {}
+        constructions_hash_new_old = {} # used to get netArea of new construction and then cost objects of construction it replaced
+        materials_hash = {}
+
+        # array and counter for new constructions that are made, used for reporting final condition
+        final_constructions_array = []
+
+        # loop through all constructions and materials used on roofs, edit and clone
+        for exterior_surface_construction in exterior_surface_constructions:
+            construction_layers = exterior_surface_construction.layers()
+            max_thermal_resistance_material = ""
+            max_thermal_resistance_material_index = 0
+            thermal_resistance_values = []
+
+            # loop through construction layers and infer insulation layer/material
+            for i, construction_layer in enumerate(construction_layers):
+                construction_layer_r_value = construction_layer.to_OpaqueMaterial().get().thermalResistance()
+                if thermal_resistance_values:
+                    if construction_layer_r_value > max(thermal_resistance_values):
+                        max_thermal_resistance_material = construction_layer
+                        max_thermal_resistance_material_index = i
+
+                thermal_resistance_values.append(construction_layer_r_value)
+
+            if max(thermal_resistance_values) <= min_expected_r_value_si:
+                runner.registerWarning(f"Construction '{exterior_surface_construction.nameString()}' does not appear to have an insulation layer and was not altered.")
+                continue
+
+            # clone the construction
+            final_construction = exterior_surface_construction.clone(model)
+            final_construction = final_construction.to_Construction().get()
+            final_construction.setName(f"{exterior_surface_construction.nameString()} adj roof insulation")
+            final_constructions_array.append(final_construction)
+
+            # add construction object if it didnt exist to start with
+
+            # push to hashes
+            constructions_hash_old_new[exterior_surface_construction.nameString()] = final_construction
+            constructions_hash_new_old[final_construction.nameString()] = exterior_surface_construction #push the object to hash key vs. name
+
+            # find already cloned insulation material and link to construction
+            target_material = max_thermal_resistance_material
+            found_material = False
+            for orig,new in materials_hash.items():
+                if target_material.nameString() == orig:
+                    new_material = new
+                    materials_hash[max_thermal_resistance_material.nameString()] = new_material
+                    final_construction.eraseLayer(max_thermal_resistance_material_index)
+                    final_construction.insertLayer(max_thermal_resistance_material_index, new_material)
+                    found_material = True
+                    # TODO: break?
+
+            # clone and edit insulation material and link to construction
+            if not found_material:
+                new_material = max_thermal_resistance_material.clone(model)
+                new_material = new_material.to_OpaqueMaterial().get()
+                new_material.setName(f"{max_thermal_resistance_material.nameString()}_R-value {r_value}% increase")
+                materials_hash[max_thermal_resistance_material.nameString()] = new_material
+                final_construction.eraseLayer(max_thermal_resistance_material_index)
+                final_construction.insertLayer(max_thermal_resistance_material_index, new_material)
+                runner.registerInfo(f"For construction'{final_construction.nameString()}', material '{new_material.nameString()}' was altered.")
+
+                # edit insulation material
+                new_material_matt = new_material.to_Material()
+                if new_material_matt.is_initialized():
+                    starting_thickness = new_material_matt.get().thickness()
+                    target_thickness = starting_thickness * (1.0 + r_value / 100.0)
+                    final_thickness = new_material_matt.get().setThickness(target_thickness)
+
+                new_material_massless = new_material.to_MasslessOpaqueMaterial()
+                if new_material_massless.is_initialized():
+                    starting_thermal_resistance = new_material_massless.get().thermalResistance()
+                    final_thermal_resistance = new_material_massless.get().setThermalResistance(starting_thermal_resistance)
+
+                new_material_airgap = new_material.to_AirGap()
+                if new_material_airgap.is_initialized():
+                    starting_thermal_resistance = new_material_airgap.get().thermalResistance()
+                    final_thermal_resistance = new_material_airgap.get().setThermalResistance(starting_thermal_resistance)
+
+
+        # loop through construction sets used in the model
+        default_construction_sets = model.getDefaultConstructionSets()
+        for default_construction_set in default_construction_sets:
+            if default_construction_set.directUseCount() == 0:
+                continue
+            default_surface_const_set = default_construction_set.defaultExteriorSurfaceConstructions()
+            if not default_surface_const_set.is_initialized():
+                continue
+
+            starting_construction = default_surface_const_set.get().roofCeilingConstruction()
+
+            # creating new default construction set
+            new_default_construction_set = default_construction_set.clone(model).to_DefaultConstructionSet().get()
+            new_default_construction_set.setName(f"{default_construction_set.nameString()} adj roof insulation")
+
+            # create new surface set and link to construction set
+            new_default_surface_const_set = default_surface_const_set.get().clone(model).to_DefaultSurfaceConstructions().get()
+            new_default_surface_const_set.setName(f"{default_surface_const_set.get().nameString()} adj roof insulation")
+            new_default_construction_set.setDefaultExteriorSurfaceConstructions(new_default_surface_const_set)
+
+            # use the hash to find the proper construction and link to new_default_surface_const_set
+            target_const = new_default_surface_const_set.roofCeilingConstruction()
+            if target_const.is_initialized():
+                target_const = target_const.get().nameString()
+                found_const_flag = False
+                for orig, new in constructions_hash_old_new.items():
+                    if target_const == orig:
+                        final_construction = new
+                        new_default_surface_const_set.setRoofCeilingConstruction(final_construction)
+                        found_const_flag = True
+
+                if not found_const_flag: # this should never happen but is just an extra test in case something goes wrong with the measure code
+                    runner.registerWarning(f"Measure couldn't find the construction named '{target_const}' in the exterior surface hash.")
+
+
+            # swap all uses of the old construction set for the new
+            construction_set_sources = default_construction_set.sources()
+            for construction_set_source in construction_set_sources:
+                building_source = construction_set_source.to_Building()
+                # if statement for each type of object than can use a DefaultConstructionSet
+                if building_source.is_initialized():
+                    building_source = building_source.get()
+                    building_source.setDefaultConstructionSet(new_default_construction_set)
+
+                building_story_source = construction_set_source.to_BuildingStory()
+                if building_story_source.is_initialized():
+                    building_story_source = building_story_source.get()
+                    building_story_source.setDefaultConstructionSet(new_default_construction_set)
+
+                space_type_source = construction_set_source.to_SpaceType()
+                if space_type_source.is_initialized():
+                    space_type_source = space_type_source.get()
+                    space_type_source.setDefaultConstructionSet(new_default_construction_set)
+
+                space_source = construction_set_source.to_Space()
+                if space_source.is_initialized():
+                    space_source = space_source.get()
+                    space_source.setDefaultConstructionSet(new_default_construction_set)
+
+        # link cloned and edited constructions for surfaces with hard assigned constructions
+        for exterior_surface in exterior_surfaces:
+            if exterior_surface.isConstructionDefaulted() or not exterior_surface.construction().is_initialized():
+                continue
+
+            # use the hash to find the proper construction and link to surface
+            target_const = exterior_surface.construction()
+            if target_const.is_initialized():
+                target_const = target_const.get().nameString()
+                for orig, new in constructions_hash_old_new.items():
+                    if target_const == orig:
+                        final_construction = new
+
+        # report strings for final condition
+        final_string = []   # not all exterior roof constructions, but only new ones made. If  roof didn't have insulation and was not altered we don't want to show it
+        affected_area_si = 0
+        for final_construction in final_constructions_array:
+
+            # unit conversion of roof insulation from SI units (M^2*K/W) to IP units (ft^2*h*R/Btu)
+            final_conductance_ip = openstudio.convert(1.0 / final_construction.thermalConductance().get(),
+                                                      "m^2*K/W", "ft^2*h*R/Btu").get()
+            final_string.append(f"{final_construction.nameString()} (R-{final_conductance_ip:.1f})")
+            affected_area_si = affected_area_si + final_construction.getNetArea()
+
+
+        # add not applicable test if there were exterior roof constructions but none of them were altered (already enough insulation or doesn't look like insulated wall)
+        if affected_area_si == 0:
+            runner.registerAsNotApplicable("No roofs were altered.")
+            return True
+            # affected_area_ip = affected_area_si
+        else:
+            # ip construction area for reporting
+            affected_area_ip = openstudio.convert(affected_area_si, "m^2", "ft^2").get()
+
+        # report final condition
+        runner.registerFinalCondition(
+            f"The existing insulation for roofs was increased by {r_value}%. "
+            f"This was applied to {affected_area_ip:,.0f} (ft^2) across "
+            f"{len(final_string)} roof constructions: {', '.join(sorted(final_string))}.")
+
+        return True
+
+
+# this allows the measure to be used by the application
+IncreaseInsulationRValueForRoofsByPercentagePython().registerWithApplication()
diff --git a/resources/Examples/compact_osw/measures/IncreaseRoofRValuePython/measure.xml b/resources/Examples/compact_osw/measures/IncreaseRoofRValuePython/measure.xml
new file mode 100644
index 00000000000..29feea0c574
--- /dev/null
+++ b/resources/Examples/compact_osw/measures/IncreaseRoofRValuePython/measure.xml
@@ -0,0 +1,67 @@
+<measure>
+  <schema_version>3.0</schema_version>
+  <name>increase_insulation_r_value_for_roofs_by_percentage</name>
+  <uid>42a4a936-5f9a-48fd-9157-e4981b9a9dca</uid>
+  <version_id>566a54c2-3683-424a-8aec-571861ea78e5</version_id>
+  <version_modified>20220921T215418Z</version_modified>
+  <xml_checksum>350B6836</xml_checksum>
+  <class_name>IncreaseInsulationRValueForRoofsByPercentagePython</class_name>
+  <display_name>Increase R-value of Insulation for Roofs by a Specified Percentage.</display_name>
+  <description>Increase the R value of the insulation layer in roof constructinons by a percentage.</description>
+  <modeler_description>Insulation later is inferred by finding the existing layer with the highest thermal resistance.</modeler_description>
+  <arguments>
+    <argument>
+      <name>r_value</name>
+      <display_name>Percentage Increase of R-value for Roof Insulation.</display_name>
+      <type>Double</type>
+      <required>true</required>
+      <model_dependent>false</model_dependent>
+      <default_value>30</default_value>
+    </argument>
+  </arguments>
+  <outputs/>
+  <provenances/>
+  <tags>
+    <tag>Envelope.Opaque</tag>
+  </tags>
+  <attributes>
+    <attribute>
+      <name>Measure Function</name>
+      <value>Measure</value>
+      <datatype>string</datatype>
+    </attribute>
+    <attribute>
+      <name>Requires EnergyPlus Results</name>
+      <value>false</value>
+      <datatype>boolean</datatype>
+    </attribute>
+    <attribute>
+      <name>Measure Type</name>
+      <value>ModelMeasure</value>
+      <datatype>string</datatype>
+    </attribute>
+    <attribute>
+      <name>Measure Language</name>
+      <value>Python</value>
+      <datatype>string</datatype>
+    </attribute>
+    <attribute>
+      <name>Uses SketchUp API</name>
+      <value>false</value>
+      <datatype>boolean</datatype>
+    </attribute>
+  </attributes>
+  <files>
+    <file>
+      <version>
+        <software_program>OpenStudio</software_program>
+        <identifier>3.4.1</identifier>
+        <min_compatible>3.4.1</min_compatible>
+      </version>
+      <filename>measure.py</filename>
+      <filetype>py</filetype>
+      <usage_type>script</usage_type>
+      <checksum>DC927612</checksum>
+    </file>
+  </files>
+</measure>
diff --git a/resources/Examples/compact_osw/measures/IncreaseWallRValue/measure.rb b/resources/Examples/compact_osw/measures/IncreaseWallRValue/measure.rb
index 928a32bde7c..fb10970755b 100644
--- a/resources/Examples/compact_osw/measures/IncreaseWallRValue/measure.rb
+++ b/resources/Examples/compact_osw/measures/IncreaseWallRValue/measure.rb
@@ -1,5 +1,5 @@
 #start the measure
-class IncreaseInsulationRValueForExteriorWallsByPercentage < OpenStudio::Ruleset::ModelUserScript
+class IncreaseInsulationRValueForExteriorWallsByPercentage < OpenStudio::Measure::ModelMeasure
 
   #define the name that a user will see
   def name
@@ -8,10 +8,10 @@ def name
 
   #define the arguments that the user will input
   def arguments(model)
-    args = OpenStudio::Ruleset::OSArgumentVector.new
+    args = OpenStudio::Measure::OSArgumentVector.new
 
     #make an argument insulation R-value
-    r_value = OpenStudio::Ruleset::OSArgument::makeDoubleArgument("r_value",true)
+    r_value = OpenStudio::Measure::OSArgument::makeDoubleArgument("r_value",true)
     r_value.setDisplayName("Percentage Increase of R-value for Exterior Wall Insulation.")
     r_value.setDefaultValue(30.0)
     args << r_value
diff --git a/resources/Examples/compact_osw/measures/PythonExampleEnergyPlusMeasure/LICENSE.md b/resources/Examples/compact_osw/measures/PythonExampleEnergyPlusMeasure/LICENSE.md
new file mode 100644
index 00000000000..d6a0c69d326
--- /dev/null
+++ b/resources/Examples/compact_osw/measures/PythonExampleEnergyPlusMeasure/LICENSE.md
@@ -0,0 +1,1 @@
+Insert your license here
\ No newline at end of file
diff --git a/resources/Examples/compact_osw/measures/PythonExampleEnergyPlusMeasure/measure.py b/resources/Examples/compact_osw/measures/PythonExampleEnergyPlusMeasure/measure.py
new file mode 100644
index 00000000000..c9e79c769ce
--- /dev/null
+++ b/resources/Examples/compact_osw/measures/PythonExampleEnergyPlusMeasure/measure.py
@@ -0,0 +1,112 @@
+"""insert your copyright here.
+
+# see the URL below for information on how to write OpenStudio measures
+# http://nrel.github.io/OpenStudio-user-documentation/reference/measure_writing_guide/
+"""
+
+import openstudio
+
+
+class PythonExampleEnergyPlusMeasure(openstudio.measure.EnergyPlusMeasure):
+    """An EnergyPlusMeasure."""
+
+    def name(self):
+        """Returns the human readable name.
+
+        Measure name should be the title case of the class name.
+        The measure name is the first contact a user has with the measure;
+        it is also shared throughout the measure workflow, visible in the OpenStudio Application,
+        PAT, Server Management Consoles, and in output reports.
+        As such, measure names should clearly describe the measure's function,
+        while remaining general in nature
+        """
+        return "Python Example EnergyPlus Measure"
+
+    def description(self):
+        """Human readable description.
+
+        The measure description is intended for a general audience and should not assume
+        that the reader is familiar with the design and construction practices suggested by the measure.
+        """
+        return "This is just a sample"
+
+    def modeler_description(self):
+        """Human readable description of modeling approach.
+
+        The modeler description is intended for the energy modeler using the measure.
+        It should explain the measure's intent, and include any requirements about
+        how the baseline model must be set up, major assumptions made by the measure,
+        and relevant citations or references to applicable modeling resources
+        """
+        return "it uses python"
+
+    def arguments(self, workspace: openstudio.Workspace):
+        """Prepares user arguments for the measure.
+
+        Measure arguments define which -- if any -- input parameters the user may set before running the measure.
+        """
+        args = openstudio.measure.OSArgumentVector()
+
+        zone_name = openstudio.measure.OSArgument.makeStringArgument("zone_name", True)
+        zone_name.setDisplayName("New zone name")
+        zone_name.setDescription("This name will be used as the name of the new zone.")
+        args.append(zone_name)
+
+        return args
+
+    def run(
+        self,
+        workspace: openstudio.Workspace,
+        runner: openstudio.measure.OSRunner,
+        user_arguments: openstudio.measure.OSArgumentMap,
+    ):
+        """Defines what happens when the measure is run."""
+        super().run(workspace, runner, user_arguments)  # Do **NOT** remove this line
+
+        if not (runner.validateUserArguments(self.arguments(workspace), user_arguments)):
+            return False
+
+        # assign the user inputs to variables
+        zone_name = runner.getStringArgumentValue("zone_name", user_arguments)
+
+        # check the zone_name for reasonableness
+        if not zone_name:
+            runner.registerError("Empty zone name was entered.")
+            return False
+
+        # get all thermal zones in the starting workspace
+        zones = workspace.getObjectsByType("Zone")
+        # report initial condition of workspace
+        runner.registerInitialCondition(f"The building started with {len(zones)} zones.")
+
+        # add a new zone to the model with the new name
+        # http://apps1.eere.energy.gov/buildings/energyplus/pdfs/inputoutputreference.pdf#nameddest=Zone
+        new_zone_string = f"""
+  Zone,
+    {zone_name},             !- Name
+    0,                       !- Direction of Relative North {{deg}}
+    0,                       !- X Origin {{m}}
+    0,                       !- Y Origin {{m}}
+    0,                       !- Z Origin {{m}}
+    1,                       !- Type
+    1,                       !- Multiplier
+    autocalculate,           !- Ceiling Height {{m}}
+    autocalculate;           !- Volume {{m3}}
+          """
+
+        idfObject_: openstudio.OptionalIdfObject = openstudio.IdfObject.load(new_zone_string)
+        idfObject: openstudio.IdfObject = idfObject_.get()
+        wsObject: openstudio.OptionalWorkspaceObject = workspace.addObject(idfObject)
+        new_zone: openstudio.WorkspaceObject = wsObject.get()
+
+        runner.registerInfo(f"A zone named '{new_zone.nameString()}' was added.")
+
+        # report final condition of model
+        finishing_zones = workspace.getObjectsByType("Zone")
+        runner.registerFinalCondition(f"The building finished with {len(finishing_zones)} zones.")
+
+        return True
+
+
+# register the measure to be used by the application
+PythonExampleEnergyPlusMeasure().registerWithApplication()
diff --git a/resources/Examples/compact_osw/measures/PythonExampleEnergyPlusMeasure/measure.xml b/resources/Examples/compact_osw/measures/PythonExampleEnergyPlusMeasure/measure.xml
new file mode 100644
index 00000000000..aded9a347ba
--- /dev/null
+++ b/resources/Examples/compact_osw/measures/PythonExampleEnergyPlusMeasure/measure.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0"?>
+<measure>
+  <schema_version>3.0</schema_version>
+  <name>python_example_energyplus_measure</name>
+  <uid>5a5d3f23-10eb-43ec-b845-176bd294410c</uid>
+  <version_id>1743a20a-eb23-42ce-aed9-f03bfaeac803</version_id>
+  <version_modified>20221005T143511Z</version_modified>
+  <xml_checksum>00000000</xml_checksum>
+  <class_name>PythonExampleEnergyPlusMeasure</class_name>
+  <display_name>Python Example EnergyPlus Measure</display_name>
+  <description>This is just a sample</description>
+  <modeler_description>it uses python</modeler_description>
+  <arguments>
+    <argument>
+      <name>zone_name</name>
+      <display_name>New zone name</display_name>
+      <description>This name will be used as the name of the new zone.</description>
+      <type>String</type>
+      <required>true</required>
+      <model_dependent>false</model_dependent>
+    </argument>
+  </arguments>
+  <outputs />
+  <provenances />
+  <tags>
+    <tag>Envelope.Infiltration</tag>
+  </tags>
+  <attributes>
+    <attribute>
+      <name>Measure Type</name>
+      <value>EnergyPlusMeasure</value>
+      <datatype>string</datatype>
+    </attribute>
+    <attribute>
+      <name>Measure Language</name>
+      <value>Python</value>
+      <datatype>string</datatype>
+    </attribute>
+  </attributes>
+  <files>
+    <file>
+      <version>
+        <software_program>OpenStudio</software_program>
+        <identifier>3.5.0</identifier>
+      </version>
+      <filename>measure.py</filename>
+      <filetype>py</filetype>
+      <usage_type>script</usage_type>
+      <checksum>DD7219EB</checksum>
+    </file>
+    <file>
+      <filename>LICENSE.md</filename>
+      <filetype>md</filetype>
+      <usage_type>license</usage_type>
+      <checksum>CD7F5672</checksum>
+    </file>
+  </files>
+</measure>
diff --git a/resources/Examples/compact_osw/measures/SetEplusInfiltration/measure.rb b/resources/Examples/compact_osw/measures/SetEplusInfiltration/measure.rb
index 3aecb3a4fa6..bb44f77f5fa 100644
--- a/resources/Examples/compact_osw/measures/SetEplusInfiltration/measure.rb
+++ b/resources/Examples/compact_osw/measures/SetEplusInfiltration/measure.rb
@@ -11,7 +11,7 @@
 # http://openstudio.nrel.gov/sites/openstudio.nrel.gov/files/nv_data/cpp_documentation_it/utilities/html/idf_page.html
 
 #start the measure
-class SetEnergyPlusInfiltrationFlowRatePerFloorArea < OpenStudio::Ruleset::WorkspaceUserScript
+class SetEnergyPlusInfiltrationFlowRatePerFloorArea < OpenStudio::Measure::EnergyPlusMeasure
 
   #define the name that a user will see, this method may be deprecated as
   #the display name in PAT comes from the name field in measure.xml
@@ -21,10 +21,10 @@ def name
 
   #define the arguments that the user will input
   def arguments(workspace)
-    args = OpenStudio::Ruleset::OSArgumentVector.new
+    args = OpenStudio::Measure::OSArgumentVector.new
 
     #make an argument
-    flowPerZoneFloorArea = OpenStudio::Ruleset::OSArgument::makeDoubleArgument("flowPerZoneFloorArea",true)
+    flowPerZoneFloorArea = OpenStudio::Measure::OSArgument::makeDoubleArgument("flowPerZoneFloorArea",true)
     flowPerZoneFloorArea.setDisplayName("Flow per Zone Floor Area (m^3/s-m^2).")
     #flowPerZoneFloorArea.setDefaultValue(10.76)
     args << flowPerZoneFloorArea
@@ -105,4 +105,4 @@ def run(workspace, runner, user_arguments)
 end #end the measure
 
 #this allows the measure to be use by the application
-SetEnergyPlusInfiltrationFlowRatePerFloorArea.new.registerWithApplication
\ No newline at end of file
+SetEnergyPlusInfiltrationFlowRatePerFloorArea.new.registerWithApplication
diff --git a/resources/Examples/compact_osw/measures/Standard Reports/measure.rb b/resources/Examples/compact_osw/measures/Standard Reports/measure.rb
index fd199de68a2..b03ff76c0c4 100644
--- a/resources/Examples/compact_osw/measures/Standard Reports/measure.rb
+++ b/resources/Examples/compact_osw/measures/Standard Reports/measure.rb
@@ -1,7 +1,7 @@
 require 'erb'
 
 #start the measure
-class StandardReports < OpenStudio::Ruleset::ReportingUserScript
+class StandardReports < OpenStudio::Measure::ReportingMeasure
 
   #define the name that a user will see, this method may be deprecated as
   #the display name in PAT comes from the name field in measure.xml
@@ -10,8 +10,8 @@ def name
   end
 
   #define the arguments that the user will input
-  def arguments()
-    args = OpenStudio::Ruleset::OSArgumentVector.new
+  def arguments(model = nil)
+    args = OpenStudio::Measure::OSArgumentVector.new
 
     return args
   end #end the arguments method
diff --git a/ruby/CMakeLists.txt b/ruby/CMakeLists.txt
index 951372199d4..95658408d0a 100644
--- a/ruby/CMakeLists.txt
+++ b/ruby/CMakeLists.txt
@@ -1,191 +1,7 @@
-if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "^(Apple)?Clang$")
-  # using Clang
-  set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -undefined dynamic_lookup")
-elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
-  # using GCC
-  #set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --unresolved-symbols=ignore-all")
-  set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --unresolved-symbols=ignore-all")
-endif()
+# OpenStudio Ruby components
 
-
-include_directories(${CMAKE_CURRENT_BINARY_DIR} ${PROJECT_BINARY_DIR} ${PROJECT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR} )
-
-# common library for openstudio.so and openstudio.exe
-
-add_library( init_openstudio OBJECT
-  init_openstudio.cpp
-  RubyAPI.hpp
-)
-target_include_directories(init_openstudio SYSTEM PRIVATE ${RUBY_INCLUDE_DIRS})
-
-if(MSVC)
-  # TODO: JM 2019-01-04: @macumber it seems like all three targets use the same COMPILE_FLAGS,
-  # so instead of defining that three times, perhaps modify CMAKE_CXX_FLAGS?
-  # wd4996=no deprecated warnings ; wd5033=register
-  set_target_properties(init_openstudio PROPERTIES COMPILE_FLAGS "/bigobj /wd4996 /wd5033")
-elseif (UNIX)
-  # Disable register warnings
-  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-error=register -Wno-register")
-endif()
-
-
-# openstudio ruby bindings to load into normal ruby
-
-add_library(
-  openstudio_rb
-  MODULE
-  RubyAPI.hpp
-  openstudio_rb.cpp
-)
-set_target_properties(openstudio_rb PROPERTIES PREFIX "")
-set_target_properties(openstudio_rb PROPERTIES OUTPUT_NAME openstudio)
-target_include_directories(openstudio_rb SYSTEM PRIVATE ${RUBY_INCLUDE_DIRS})
-
-# change output directory so the openstudio.pdb file does not conflict between this target and the CLI
-set_target_properties(openstudio_rb PROPERTIES ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_ARCHIVE_OUTPUT_DIRECTORY}/ruby/")
-set_target_properties(openstudio_rb PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/ruby/")
-set_target_properties(openstudio_rb PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/ruby/")
-
-if(APPLE)
-  set_target_properties(openstudio_rb PROPERTIES SUFFIX ".bundle" )
-else()
-  set_target_properties(openstudio_rb PROPERTIES SUFFIX ".so" )
-endif()
-
-if(MSVC)
-  set_target_properties(openstudio_rb PROPERTIES COMPILE_FLAGS "/bigobj /wd4996 /wd5033")
-endif()
-
-add_custom_command(TARGET openstudio_rb
-  POST_BUILD
-  COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/openstudio.rb $<TARGET_FILE_DIR:openstudio_rb>/openstudio.rb
-)
-
-file(GLOB_RECURSE OPENSTUDIO_FILES FOLLOW_SYMLINKS RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/openstudio/" "${CMAKE_CURRENT_SOURCE_DIR}/openstudio/**/*.*")
-foreach(OPENSTUDIO_FILE ${OPENSTUDIO_FILES})
-  add_custom_command(TARGET openstudio_rb
-    POST_BUILD
-    COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/openstudio/${OPENSTUDIO_FILE}" "$<TARGET_FILE_DIR:openstudio_rb>/openstudio/${OPENSTUDIO_FILE}"
-  )
-endforeach()
-
-
-target_link_libraries(
-  openstudio_rb
-  PRIVATE
-  init_openstudio
-  ${ALL_RUBY_BINDING_TARGETS}
-)
-
-if( WIN32 )
-  # Dynamically link to openstudiolib.dll on Windows only
-  target_link_libraries(
-    openstudio_rb
-    PUBLIC
-    openstudiolib
-  )
-  target_link_libraries(openstudio_rb PRIVATE ${RUBY_MINGW_STUB_LIB})
-else()
-  # statically link everywhere else
-  target_link_libraries(
-    openstudio_rb
-    PRIVATE
-    openstudio_utilities
-    openstudio_airflow
-    openstudio_model
-    openstudio_energyplus
-    openstudio_epjson
-    openstudio_measure
-    openstudio_osversion
-    openstudio_sdd
-    openstudio_isomodel
-    openstudio_gbxml
-    openstudio_gltf
-    openstudio_radiance
-  )
-endif()
-
-if (APPLE)
-  target_link_libraries(openstudio_rb
-    PRIVATE
-    ${OPENSSL_CRYPTO_LIBRARY}
-    ${OPENSSL_SSL_LIBRARY}
-  )
-endif()
-
-
-install(TARGETS openstudio_rb
-  EXPORT openstudio
-  DESTINATION Ruby
-  COMPONENT "RubyAPI"
-  INCLUDES DESTINATION include include/openstudio
-)
-install(FILES openstudio.rb DESTINATION Ruby COMPONENT "RubyAPI")
-install(DIRECTORY openstudio DESTINATION Ruby COMPONENT "RubyAPI")
-
-
-###############################################################################
-#                        T E S T I N G:   C T E S T S                         #
-###############################################################################
-
-# find all tests
-file(GLOB RUBY_TEST_SRC "test/*.rb")
-
-# TODO: It doesn't not work with this executable that's in build/Ruby-install/bin
-# message("RUBY_EXECUTABLE=${RUBY_EXECUTABLE}")
-
-set(_RUBY_POSSIBLE_EXECUTABLE_NAMES
-  ruby
-  ruby2.7
-  ruby2.7.2
-  ruby27
-  ruby272)
-
-# TODO: this isn't great but I haven't found a better way to locate the system ruby (and avoid the one in build/Ruby-install/)
-find_program(SYSTEM_RUBY_EXECUTABLE NAMES ${_RUBY_POSSIBLE_EXECUTABLE_NAMES}
-  HINTS
-    "/usr/local/rvm/rubies/ruby-2.7.2/bin/"
-    "/usr/local/rvm/rubies/ruby-2.7.2/bin/ruby"
-    "/usr/share/rvm/rubies/ruby-2.7.2/bin/"
-    "$ENV{HOME}/.rvm/rubies/ruby-2.7.2/bin/"
-
-    "C:/Ruby27-x64/bin/"
-
-     "/usr/local/ruby272/bin/"
-     "/usr/local/ruby27/bin/"
-     "/usr/bin/"
-     "/usr/local/bin/"
-
-
-  NO_DEFAULT_PATH)
-
-if(NOT SYSTEM_RUBY_EXECUTABLE)
-  message(WARNING "Your system ruby wasn't found, you won't be able to run the `ctest -R RubyTest` command and the tests won't be created at all.")
-else()
-  message(STATUS "Found SYSTEM_RUBY_EXECUTABLE=${SYSTEM_RUBY_EXECUTABLE}")
-
-  # add a test for each unit test
-  set(RUBY_TEST_REQUIRES "#include test files")
-  foreach(f ${RUBY_TEST_SRC})
-
-    file(READ "${f}" CONTENTS)
-    string(REGEX MATCHALL "def +test_([A-Za-z_0-9 ]+)" FOUND_TESTS ${CONTENTS})
-
-    foreach(HIT ${FOUND_TESTS})
-      string(REGEX REPLACE "def +test_([A-Za-z_0-9]+)" "\\1" TEST_NAME ${HIT})
-      string(REGEX MATCH "/?([A-Za-z_0-9 ]+)\\.rb" FILE_NAME ${f})
-      string(REGEX REPLACE "/?([A-Za-z_0-9 ]+)\\.rb" "\\1" FILE_NAME ${FILE_NAME})
-      if(BUILD_TESTING)
-
-        # Call with Ruby itself
-        add_test(NAME "RubyTest-${FILE_NAME}-${TEST_NAME}"
-          COMMAND "${CMAKE_COMMAND}" -E chdir "${CMAKE_CURRENT_BINARY_DIR}"
-          "${SYSTEM_RUBY_EXECUTABLE}" "-I" "$<TARGET_FILE_DIR:openstudio_rb>" "${f}" "--name=test_${TEST_NAME}"
-        )
-
-        set_tests_properties("RubyTest-${FILE_NAME}-${TEST_NAME}" PROPERTIES TIMEOUT 660 )
-      endif()
-    endforeach()
-  endforeach()
-
-endif()
+add_subdirectory(interpreter)
+add_subdirectory(bindings)
+add_subdirectory(module)
+add_subdirectory(engine)
+add_subdirectory(test)
diff --git a/ruby/README.md b/ruby/README.md
new file mode 100644
index 00000000000..26ecea30058
--- /dev/null
+++ b/ruby/README.md
@@ -0,0 +1,17 @@
+# OpenStudio Ruby
+
+OpenStudio's Ruby implementation is divided into several pieces
+
+* `interpreter`: This is the bare bones Ruby interpreter with only some very minimal OpenStudio
+initializations and abstractions. There are two separate cmake targets related to the interpreter.
+
+    * The target `rubyinterpreter-declarations` is an interface library that merely adds Ruby to the include path. This is used by the standalone ruby bindings.
+    * The target `rubyinterpreter` adds Ruby to the include paths, but also links to the Ruby library. This is used by the OpenStudio Ruby engine.
+
+* `bindings`: This contains an interface to initialize the OpenStudio API bindings for Ruby. (e.g. `init_openstudio_model`)
+The `module` and `engine` components consume this.
+
+* `module`: This is the conventional OpenStudio Ruby bindings, for loading into a standalone Ruby interpreter.
+
+* `engine`: This is a combination of the OpenStudio Ruby bindings, the Ruby interpreter, and embedded files
+to be used by the OpenStudio cli to run Ruby based workflows.
diff --git a/ruby/bindings/CMakeLists.txt b/ruby/bindings/CMakeLists.txt
new file mode 100644
index 00000000000..e7e4eb51bfe
--- /dev/null
+++ b/ruby/bindings/CMakeLists.txt
@@ -0,0 +1,17 @@
+add_library( rubybindings OBJECT
+  InitRubyBindings.hpp
+  InitRubyBindings.cpp
+)
+
+target_include_directories(rubybindings PUBLIC
+    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
+)
+target_link_libraries(rubybindings PRIVATE ${ALL_RUBY_BINDING_TARGETS} rubyinterpreter-declarations)
+
+# wd4996=no deprecated warnings ; wd5033=register
+target_compile_options(rubybindings PRIVATE
+  $<$<CXX_COMPILER_ID:MSVC>:/bigobj /wd4996 /wd5033>
+  $<$<OR:$<CXX_COMPILER_ID:Clang>,$<CXX_COMPILER_ID:AppleClang>,$<CXX_COMPILER_ID:GNU>>:-Wno-error=register -Wno-register>
+  $<$<OR:$<CXX_COMPILER_ID:Clang>,$<CXX_COMPILER_ID:AppleClang>>:-Wno-deprecated-volatile>
+  $<$<CXX_COMPILER_ID:GNU>:-Wno-volatile>
+)
diff --git a/ruby/init_openstudio.cpp b/ruby/bindings/InitRubyBindings.cpp
similarity index 93%
rename from ruby/init_openstudio.cpp
rename to ruby/bindings/InitRubyBindings.cpp
index 20a94ad7a00..e48ecf0a669 100644
--- a/ruby/init_openstudio.cpp
+++ b/ruby/bindings/InitRubyBindings.cpp
@@ -27,7 +27,8 @@
 *  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 ***********************************************************************************************************************/
 
-#include "init_openstudio.hpp"
+#include "InitRubyBindings.hpp"
+#include "RubyEval.hpp"
 #include <ruby.h>
 #include <stdexcept>
 #include <iostream>
@@ -74,7 +75,10 @@ extern "C"
   ////void Init_openstudiomodeleditor(void); # happens separately in openstudio.so only, for SketchUp plug-in
 }
 
-void init_openstudio_internal_basic() {
+namespace openstudio {
+namespace ruby {
+
+void initBasicRubyBindings() {
   rb_provide("openstudio");
   rb_provide("openstudio.so");
 
@@ -119,7 +123,7 @@ void init_openstudio_internal_basic() {
   rb_provide("openstudioutilities.so");
 }
 
-void init_openstudio_internal_extended() {
+void initExtendedRubyBindings() {
   Init_openstudiomodel();
   rb_provide("openstudiomodel");
   rb_provide("openstudiomodel.so");
@@ -366,49 +370,13 @@ end # module OpenStudio
 
 )END";
 
-  evalString(ruby_typedef_script);
-}
-
-void init_openstudio_internal() {
-  init_openstudio_internal_basic();
-  init_openstudio_internal_extended();
+  openstudio::evalString(ruby_typedef_script);
 }
 
-class RubyException : public std::runtime_error
-{
- public:
-  RubyException(const std::string& msg, const std::string& location) : std::runtime_error(msg), m_location(location) {}
-
-  virtual ~RubyException() throw() {}
-
-  std::string location() const {
-    return m_location;
-  }
-
- private:
-  std::string m_location;
-};
-
-static VALUE evaluateSimpleImpl(VALUE arg) {
-  return rb_eval_string(StringValuePtr(arg));
+void initRubyBindings() {
+  initBasicRubyBindings();
+  initExtendedRubyBindings();
 }
 
-void evalString(const std::string& t_str) {
-
-  VALUE val = rb_str_new2(t_str.c_str());
-  int error;
-
-  rb_protect(evaluateSimpleImpl, val, &error);
-
-  if (error != 0) {
-    VALUE errval = rb_eval_string("$!.to_s");
-    char* str = StringValuePtr(errval);
-    std::string err(str);
-
-    VALUE locval = rb_eval_string("$@.to_s");
-    str = StringValuePtr(locval);
-    std::string loc(str);
-
-    throw RubyException(err, loc);
-  }
-}
+}  // namespace ruby
+}  // namespace openstudio
diff --git a/ruby/bindings/InitRubyBindings.hpp b/ruby/bindings/InitRubyBindings.hpp
new file mode 100644
index 00000000000..419c389e8b7
--- /dev/null
+++ b/ruby/bindings/InitRubyBindings.hpp
@@ -0,0 +1,14 @@
+#ifndef INITRUBYBINDINGS_HPP
+#define INITRUBYBINDINGS_HPP
+
+namespace openstudio {
+namespace ruby {
+
+void initRubyBindings();
+void initBasicRubyBindings();
+void initExtendedRubyBindings();
+
+}  // namespace ruby
+}  // namespace openstudio
+
+#endif  // INITRUBYBINDINGS_HPP
diff --git a/ruby/engine/CMakeLists.txt b/ruby/engine/CMakeLists.txt
new file mode 100644
index 00000000000..5667857b6d7
--- /dev/null
+++ b/ruby/engine/CMakeLists.txt
@@ -0,0 +1,272 @@
+# This defines a list of import targets for the native extensions: ruby_extension_libs
+include("${OPENSTUDIO_GEMS_DIR}/export-extensions.cmake")
+
+# This had been broken for a long time (was using a wrong variable name for the list, so InitMacros.hxx is empty) and it still worked fine. TODO: remove?
+set(INIT_CALLS "")
+set(INIT_DECLARATIONS "")
+foreach(LIB ${ruby_extension_libs})
+  get_filename_component(LIB_NAME ${LIB} NAME_WE)
+  #list (FIND LIBS "${LIB_NAME}" _index)
+  #if( ${_index} GREATER -1 )
+  set(INIT_DECLARATIONS "${INIT_DECLARATIONS}  void Init_${LIB_NAME}();\\\n" )
+  set(INIT_CALLS "${INIT_CALLS}  Init_${LIB_NAME}();\\\n" )
+  set(INIT_CALLS "${INIT_CALLS}  rb_provide(\"${LIB_NAME}.so\");\\\n" )
+  #endif()
+endforeach()
+
+CONFIGURE_FILE_WITH_CHECKSUM("InitMacros.hxx.in" "${CMAKE_CURRENT_BINARY_DIR}/InitMacros.hxx")
+
+set(MODULE_ROOT "${CONAN_OPENSTUDIO_RUBY_ROOT}")
+if ("${MODULE_ROOT}" STREQUAL "")
+  set(MODULE_ROOT "${CONAN_OPENSTUDIO_RUBY_ROOT_DEBUG}")
+endif()
+
+message("Searching for ruby modules in '${MODULE_ROOT}/lib/**/*.rb'")
+file(GLOB_RECURSE  EXTENSION_RB  FOLLOW_SYMLINKS "${MODULE_ROOT}/lib/**/*.rb")
+
+foreach( _FILE ${EXTENSION_RB} )
+  file(RELATIVE_PATH LOCATION ${MODULE_ROOT}/lib ${_FILE})
+  list(APPEND FILES ${_FILE})
+  list(APPEND EMBEDDED_PATHS ${LOCATION})
+endforeach()
+
+set(GEMFILE_FILES "${OPENSTUDIO_GEMS_DIR}/openstudio-gems.gemspec" "${PROJECT_BINARY_DIR}/openstudio-gems/Gemfile" "${PROJECT_BINARY_DIR}/openstudio-gems/Gemfile.lock")
+file(GLOB_RECURSE RB_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.rb")
+file(GLOB_RECURSE ERB_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.erb")
+file(GLOB_RECURSE JS_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.js")
+file(GLOB_RECURSE CSS_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.css")
+file(GLOB_RECURSE GIF_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.gif")
+file(GLOB_RECURSE PNG_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.png")
+file(GLOB_RECURSE HTML_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.html")
+file(GLOB_RECURSE IDF_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.idf")
+file(GLOB_RECURSE OSM_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.osm")
+file(GLOB_RECURSE EPW_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.epw")
+file(GLOB_RECURSE DDY_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.ddy")
+file(GLOB_RECURSE STAT_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.stat")
+file(GLOB_RECURSE CSV_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.csv")
+file(GLOB_RECURSE JSON_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.json")
+file(GLOB_RECURSE SPEC_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.gemspec")
+file(GLOB_RECURSE GZ_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.gz")
+file(GLOB_RECURSE YML_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.yml")
+file(GLOB_RECURSE OS_FILES FOLLOW_SYMLINKS "${CMAKE_SOURCE_DIR}/ruby/module/openstudio/**/*.rb")
+
+foreach( _FILE ${GEMFILE_FILES} )
+  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
+  list(APPEND FILES ${_FILE})
+  list(APPEND EMBEDDED_PATHS ${LOCATION})
+endforeach()
+
+foreach( _FILE ${RB_FILES} )
+  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
+  list(APPEND FILES ${_FILE})
+  list(APPEND EMBEDDED_PATHS ${LOCATION})
+endforeach()
+
+foreach( _FILE ${ERB_FILES} )
+  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
+  list(APPEND FILES ${_FILE})
+  list(APPEND EMBEDDED_PATHS ${LOCATION})
+endforeach()
+
+foreach( _FILE ${JS_FILES} )
+  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
+  list(APPEND FILES ${_FILE})
+  list(APPEND EMBEDDED_PATHS ${LOCATION})
+endforeach()
+
+foreach( _FILE ${CSS_FILES} )
+  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
+  list(APPEND FILES ${_FILE})
+  list(APPEND EMBEDDED_PATHS ${LOCATION})
+endforeach()
+
+foreach( _FILE ${GIF_FILES} )
+  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
+  list(APPEND FILES ${_FILE})
+  list(APPEND EMBEDDED_PATHS ${LOCATION})
+endforeach()
+
+foreach( _FILE ${PNG_FILES} )
+  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
+  list(APPEND FILES ${_FILE})
+  list(APPEND EMBEDDED_PATHS ${LOCATION})
+endforeach()
+
+foreach( _FILE ${HTML_FILES} )
+  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
+  list(APPEND FILES ${_FILE})
+  list(APPEND EMBEDDED_PATHS ${LOCATION})
+endforeach()
+
+foreach( _FILE ${IDF_FILES} )
+  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
+  list(APPEND FILES ${_FILE})
+  list(APPEND EMBEDDED_PATHS ${LOCATION})
+endforeach()
+
+foreach( _FILE ${OSM_FILES} )
+  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
+  list(APPEND FILES ${_FILE})
+  list(APPEND EMBEDDED_PATHS ${LOCATION})
+endforeach()
+
+foreach( _FILE ${EPW_FILES} )
+  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
+  list(APPEND FILES ${_FILE})
+  list(APPEND EMBEDDED_PATHS ${LOCATION})
+endforeach()
+
+foreach( _FILE ${DDY_FILES} )
+  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
+  list(APPEND FILES ${_FILE})
+  list(APPEND EMBEDDED_PATHS ${LOCATION})
+endforeach()
+
+foreach( _FILE ${STAT_FILES} )
+  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
+  list(APPEND FILES ${_FILE})
+  list(APPEND EMBEDDED_PATHS ${LOCATION})
+endforeach()
+
+foreach( _FILE ${CSV_FILES} )
+  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
+  list(APPEND FILES ${_FILE})
+  list(APPEND EMBEDDED_PATHS ${LOCATION})
+endforeach()
+
+foreach( _FILE ${JSON_FILES} )
+  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
+  list(APPEND FILES ${_FILE})
+  list(APPEND EMBEDDED_PATHS ${LOCATION})
+endforeach()
+
+foreach( _FILE ${SPEC_FILES} )
+  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
+  list(APPEND FILES ${_FILE})
+  list(APPEND EMBEDDED_PATHS ${LOCATION})
+endforeach()
+
+foreach( _FILE ${GZ_FILES} )
+  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
+  list(APPEND FILES ${_FILE})
+  list(APPEND EMBEDDED_PATHS ${LOCATION})
+endforeach()
+
+foreach( _FILE ${YML_FILES} )
+  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
+  list(APPEND FILES ${_FILE})
+  list(APPEND EMBEDDED_PATHS ${LOCATION})
+endforeach()
+
+# DLM: this is temporary, these files should live somewhere else and be included in the shared Ruby lib
+foreach( _FILE ${OS_FILES} )
+  file(RELATIVE_PATH LOCATION "${CMAKE_SOURCE_DIR}/ruby/module" ${_FILE})
+  list(APPEND FILES ${_FILE})
+  list(APPEND EMBEDDED_PATHS ${LOCATION})
+endforeach()
+
+list(APPEND FILES "${CMAKE_CURRENT_SOURCE_DIR}/embedded_help.rb")
+list(APPEND EMBEDDED_PATHS "embedded_help.rb")
+
+list(APPEND FILES "${CMAKE_CURRENT_SOURCE_DIR}/openstudio_cli.rb")
+list(APPEND EMBEDDED_PATHS "openstudio_cli.rb")
+
+list(APPEND FILES "${CMAKE_CURRENT_SOURCE_DIR}/openstudio_init_extended.rb")
+list(APPEND EMBEDDED_PATHS "openstudio_init_extended.rb")
+
+
+list(APPEND FILES "${CMAKE_CURRENT_SOURCE_DIR}/measure_manager.rb")
+list(APPEND EMBEDDED_PATHS "measure_manager.rb")
+
+list(APPEND FILES "${CMAKE_CURRENT_SOURCE_DIR}/measure_manager_server.rb")
+list(APPEND EMBEDDED_PATHS "measure_manager_server.rb")
+
+embed_files("${FILES}" "${EMBEDDED_PATHS}" OUTPUT)
+
+set_source_files_properties(EmbeddedScripting.i
+  PROPERTIES CPLUSPLUS true
+)
+
+include_directories(${CMAKE_CURRENT_BINARY_DIR}
+  ${PROJECT_BINARY_DIR}
+  ${PROJECT_SOURCE_DIR}
+  ${CMAKE_CURRENT_SOURCE_DIR}
+  ${OPENSTUDIO_GEMS_DIR}
+)
+
+add_custom_command(
+  OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/embedded_scripting_wrap.cxx"
+  COMMAND ${CMAKE_COMMAND} -E env SWIG_LIB="${SWIG_LIB}"
+          "${SWIG_EXECUTABLE}"
+          "-ruby"
+          "-c++"
+          -o "${CMAKE_CURRENT_BINARY_DIR}/embedded_scripting_wrap.cxx"
+          "-fvirtual"
+          "-I${PROJECT_SOURCE_DIR}/src"
+          "-I${PROJECT_BINARY_DIR}/src"
+          "-I${OPENSTUDIO_GEMS_DIR}"
+          "-D_WINDOWS"
+          "-Fmicrosoft"
+          "${CMAKE_CURRENT_SOURCE_DIR}/EmbeddedScripting.i"
+  DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/EmbeddedScripting.i"
+          "${CMAKE_CURRENT_BINARY_DIR}/embedded_files.hxx"
+          "EmbeddedHelp.hpp"
+)
+
+set_source_files_properties(${EMBED_SOURCE_FILES} PROPERTIES HEADER_FILE_ONLY TRUE)
+source_group(embedded_files FILES ${OUTPUT})
+
+add_library(rubyengine
+  MODULE
+  RubyEngine.hpp
+  RubyEngine.cpp
+  InitRubyEngine.cpp
+  GC_Value.hpp
+  ${OUTPUT}
+  "${CMAKE_CURRENT_BINARY_DIR}/embedded_scripting_wrap.cxx"
+)
+
+include(GenerateExportHeader)
+generate_export_header(rubyengine)
+
+target_compile_options(rubyengine PRIVATE
+  $<${is_msvc_genex}:/bigobj>
+)
+# -Wno-deprecated-declaration, /wd4996: suppresses deprecated warning
+target_compile_options(rubyengine PRIVATE
+  $<${is_msvc_genex}:/wd4996>
+  $<${is_gnu_or_clang_genex}:-Wno-deprecated-declarations>
+)
+# -Wno-register, /wd5033: ISO C++1z does not allow "register" storage class specifier
+target_compile_options(rubyengine PRIVATE
+  $<${is_msvc_genex}:/wd5033>
+  $<${is_gnu_or_clang_genex}:-Wno-register>
+)
+set_source_files_properties("${CMAKE_CURRENT_BINARY_DIR}/embedded_scripting_wrap.cxx"
+  PROPERTIES COMPILE_FLAGS "-DRUBY_EMBEDDED $<${is_gnu_or_clang_genex}:-Wno-unused-variable>"
+)
+
+#add_definitions(-DRUBY_EXTCONF_H="osruby_config.h")
+
+target_link_libraries(rubyengine
+  PRIVATE
+  openstudio_scriptengine
+  openstudiolib
+  rubybindings
+  rubyinterpreter
+  CONAN_PKG::fmt
+  ${ruby_extension_libs}
+  ${ALL_RUBY_BINDING_TARGETS}
+)
+
+if( WIN32 )
+  target_link_libraries(rubyengine
+    PRIVATE
+    wsock32 ws2_32 Dbghelp Shlwapi Iphlpapi
+  )
+  target_link_options(rubyengine PRIVATE $<$<CONFIG:Debug>:/NODEFAULTLIB:MSVCRT>)
+endif()
+
+target_compile_definitions(rubyengine PRIVATE openstudio_scriptengine_EXPORTS SHARED_OS_LIBS)
+
+install(TARGETS rubyengine EXPORT openstudio DESTINATION bin COMPONENT "CLI")
diff --git a/src/cli/EmbeddedHelp.hpp b/ruby/engine/EmbeddedHelp.hpp
similarity index 100%
rename from src/cli/EmbeddedHelp.hpp
rename to ruby/engine/EmbeddedHelp.hpp
diff --git a/src/cli/EmbeddedScripting.i b/ruby/engine/EmbeddedScripting.i
similarity index 100%
rename from src/cli/EmbeddedScripting.i
rename to ruby/engine/EmbeddedScripting.i
diff --git a/src/cli/GC_Value.hpp b/ruby/engine/GC_Value.hpp
similarity index 100%
rename from src/cli/GC_Value.hpp
rename to ruby/engine/GC_Value.hpp
diff --git a/src/cli/InitMacros.hxx.in b/ruby/engine/InitMacros.hxx.in
similarity index 100%
rename from src/cli/InitMacros.hxx.in
rename to ruby/engine/InitMacros.hxx.in
diff --git a/ruby/engine/InitRuby.hpp b/ruby/engine/InitRuby.hpp
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/ruby/engine/InitRubyEngine.cpp b/ruby/engine/InitRubyEngine.cpp
new file mode 100644
index 00000000000..d2382f0379e
--- /dev/null
+++ b/ruby/engine/InitRubyEngine.cpp
@@ -0,0 +1,387 @@
+#include "./RubyEngine.hpp"
+#include "./InitRubyEngine.hpp"
+#include "./GC_Value.hpp"
+#include <RubyInterpreter.hpp>
+#include <iostream>
+#include <embedded_files.hxx>
+
+namespace openstudio {
+
+void RubyEngine::initRubyEngine() {
+  RUBY_INIT_STACK;
+  ruby_init();
+
+  swig::GC_VALUE::hash_id = rb_intern("hash");
+  swig::GC_VALUE::lt_id = rb_intern("<");
+  swig::GC_VALUE::gt_id = rb_intern(">");
+  swig::GC_VALUE::eq_id = rb_intern("==");
+  swig::GC_VALUE::le_id = rb_intern("<=");
+  swig::GC_VALUE::ge_id = rb_intern(">=");
+
+  swig::GC_VALUE::pos_id = rb_intern("+@");
+  swig::GC_VALUE::neg_id = rb_intern("-@");
+  swig::GC_VALUE::inv_id = rb_intern("~");
+
+  swig::GC_VALUE::add_id = rb_intern("+");
+  swig::GC_VALUE::sub_id = rb_intern("-");
+  swig::GC_VALUE::mul_id = rb_intern("*");
+  swig::GC_VALUE::div_id = rb_intern("/");
+  swig::GC_VALUE::mod_id = rb_intern("%");
+
+  swig::GC_VALUE::and_id = rb_intern("&");
+  swig::GC_VALUE::or_id = rb_intern("|");
+  swig::GC_VALUE::xor_id = rb_intern("^");
+
+  swig::GC_VALUE::lshift_id = rb_intern("<<");
+  swig::GC_VALUE::rshift_id = rb_intern(">>");
+
+  // Need embedded_help for requiring files out of the embedded system
+  auto embedded_extensions_string = embedded_files::getFileAsString(":/embedded_help.rb");
+
+  try {
+    rubyInterpreter.evalString(embedded_extensions_string);
+  } catch (const std::exception& e) {
+    rubyInterpreter.evalString(R"(STDOUT.flush)");
+    std::cout << "Exception in embedded_help: " << e.what() << std::endl;  // endl will flush
+    ruby_cleanup(1);
+    throw;
+  } catch (...) {
+    rubyInterpreter.evalString(R"(STDOUT.flush)");
+    std::cout << "Unknown Exception in embedded_help" << std::endl;  // endl will flush
+    ruby_cleanup(1);
+    throw;
+  }
+
+  // encodings
+  // Get the symbols from: `DUMPBIN /ARCHIVEMEMBERS  <path/to/ruby/>/lib/enc/libenc.lib`
+  Init_encdb();
+  rb_provide("enc/encdb.so");
+  Init_big5();
+  rb_provide("enc/big5.so");
+  Init_cesu_8();
+  rb_provide("enc/cesu_8.so");
+  Init_cp949();
+  rb_provide("enc/cp949.so");
+  Init_emacs_mule();
+  rb_provide("enc/emacs_mule.so");
+  Init_euc_jp();
+  rb_provide("enc/euc_jp.so");
+  Init_euc_kr();
+  rb_provide("enc/euc_kr.so");
+  Init_euc_tw();
+  rb_provide("enc/euc_tw.so");
+  Init_gb18030();
+  rb_provide("enc/gb18030.so");
+  Init_gb2312();
+  rb_provide("enc/gb2312.so");
+  Init_gbk();
+  rb_provide("enc/gbk.so");
+  Init_iso_8859_1();
+  rb_provide("enc/iso_8859_1.so");
+  Init_iso_8859_10();
+  rb_provide("enc/iso_8859_10.so");
+  Init_iso_8859_11();
+  rb_provide("enc/iso_8859_11.so");
+  Init_iso_8859_13();
+  rb_provide("enc/iso_8859_13.so");
+  Init_iso_8859_14();
+  rb_provide("enc/iso_8859_14.so");
+  Init_iso_8859_15();
+  rb_provide("enc/iso_8859_15.so");
+  Init_iso_8859_16();
+  rb_provide("enc/iso_8859_16.so");
+  Init_iso_8859_2();
+  rb_provide("enc/iso_8859_2.so");
+  Init_iso_8859_3();
+  rb_provide("enc/iso_8859_3.so");
+  Init_iso_8859_4();
+  rb_provide("enc/iso_8859_4.so");
+  Init_iso_8859_5();
+  rb_provide("enc/iso_8859_5.so");
+  Init_iso_8859_6();
+  rb_provide("enc/iso_8859_6.so");
+  Init_iso_8859_7();
+  rb_provide("enc/iso_8859_7.so");
+  Init_iso_8859_8();
+  rb_provide("enc/iso_8859_8.so");
+  Init_iso_8859_9();
+  rb_provide("enc/iso_8859_9.so");
+  Init_koi8_r();
+  rb_provide("enc/koi8_r.so");
+  Init_koi8_u();
+  rb_provide("enc/koi8_u.so");
+  Init_shift_jis();
+  rb_provide("enc/shift_jis.so");
+  Init_utf_16be();
+  rb_provide("enc/utf_16be.so");
+  Init_utf_16le();
+  rb_provide("enc/utf_16le.so");
+  Init_utf_32be();
+  rb_provide("enc/utf_32be.so");
+  Init_utf_32le();
+  rb_provide("enc/utf_32le.so");
+  Init_windows_1250();
+  rb_provide("enc/windows_1250.so");
+  Init_windows_1251();
+  rb_provide("enc/windows_1251.so");
+  Init_windows_1252();
+  rb_provide("enc/windows_1252.so");
+  Init_windows_1253();
+  rb_provide("enc/windows_1253.so");
+  Init_windows_1254();
+  rb_provide("enc/windows_1254.so");
+  Init_windows_1257();
+  rb_provide("enc/windows_1257.so");
+  Init_windows_31j();
+  rb_provide("enc/windows_31j.so");
+
+  // Get the symbols from: `DUMPBIN /ARCHIVEMEMBERS  <path/to/ruby>/lib/enc/libtrans.lib`
+  Init_transdb();
+  rb_provide("enc/trans/transdb.so");
+
+  Init_trans_big5();
+  rb_provide("enc/trans/big5.so");
+
+  Init_trans_cesu_8();
+  rb_provide("enc/trans/cesu_8.so");
+
+  Init_trans_chinese();
+  rb_provide("enc/trans/chinese.so");
+
+  Init_trans_ebcdic();
+  rb_provide("enc/trans/ebcdic.so");
+
+  Init_trans_emoji();
+  rb_provide("enc/trans/emoji.so");
+
+  Init_trans_emoji_iso2022_kddi();
+  rb_provide("enc/trans/emoji_iso2022_kddi.so");
+
+  Init_trans_emoji_sjis_docomo();
+  rb_provide("enc/trans/emoji_sjis_docomo.so");
+
+  Init_trans_emoji_sjis_kddi();
+  rb_provide("enc/trans/emoji_sjis_kddi.so");
+
+  Init_trans_emoji_sjis_softbank();
+  rb_provide("enc/trans/emoji_sjis_softbank.so");
+
+  Init_trans_escape();
+  rb_provide("enc/trans/escape.so");
+
+  Init_trans_gb18030();
+  rb_provide("enc/trans/gb18030.so");
+
+  Init_trans_gbk();
+  rb_provide("enc/trans/gbk.so");
+
+  Init_trans_iso2022();
+  rb_provide("enc/trans/iso2022.so");
+
+  Init_trans_japanese();
+  rb_provide("enc/trans/japanese.so");
+
+  Init_trans_japanese_euc();
+  rb_provide("enc/trans/japanese_euc.so");
+
+  Init_trans_japanese_sjis();
+  rb_provide("enc/trans/japanese_sjis.so");
+
+  Init_trans_korean();
+  rb_provide("enc/trans/korean.so");
+
+  Init_trans_single_byte();
+  rb_provide("enc/trans/single_byte.so");
+
+  Init_trans_utf8_mac();
+  rb_provide("enc/trans/utf8_mac.so");
+
+  Init_trans_utf_16_32();
+  rb_provide("enc/trans/utf_16_32.so");
+
+  Init_bigdecimal();
+  rb_provide("bigdecimal");
+  rb_provide("bigdecimal.so");
+
+  Init_continuation();
+  rb_provide("continuation");
+  rb_provide("continuation.so");
+
+  Init_coverage();
+  rb_provide("coverage");
+  rb_provide("coverage.so");
+
+  Init_cparse();
+  rb_provide("cparse");
+  rb_provide("racc/cparse");
+  rb_provide("cparse.so");
+  rb_provide("racc/cparse.so");
+
+  Init_date_core();
+  rb_provide("date_core");
+  rb_provide("date_core.so");
+
+  Init_digest();
+  rb_provide("digest");
+  rb_provide("digest.so");
+
+  Init_escape();
+  rb_provide("escape");
+  rb_provide("escape.so");
+
+  Init_etc();
+  rb_provide("etc");
+  rb_provide("etc.so");
+
+  Init_fcntl();
+  rb_provide("fcntl");
+  rb_provide("fcntl.so");
+
+  Init_fiber();
+  rb_provide("fiber");
+  rb_provide("fiber.so");
+
+  Init_fiddle();
+  rb_provide("fiddle");
+  rb_provide("fiddle.so");
+
+  // TODO: Fix this
+  // #ifndef _WIN32
+  Init_generator();
+  rb_provide("json/ext/generator");
+  rb_provide("json/ext/generator.so");
+  // #endif
+
+  Init_md5();
+  rb_provide("md5");
+  rb_provide("digest/md5");
+  rb_provide("md5.so");
+  rb_provide("digest/md5.so");
+
+  Init_monitor();
+  rb_provide("monitor");
+  rb_provide("monitor.so");
+
+  Init_nkf();
+  rb_provide("nkf");
+  rb_provide("nkf.so");
+
+  Init_nonblock();
+  rb_provide("nonblock");
+  rb_provide("nonblock.so");
+  rb_provide("io/nonblock");
+  rb_provide("io/nonblock.so");
+
+  Init_ruby_description();
+
+  Init_objspace();
+  rb_provide("objspace");
+  rb_provide("objspace.so");
+
+  // TODO: Fix this
+  // #ifndef _WIN32
+  Init_parser();
+  rb_provide("json/ext/parser");
+  rb_provide("json/ext/parser.so");
+  // #endif
+
+  Init_pathname();
+  rb_provide("pathname");
+  rb_provide("pathname.so");
+
+  // TODO: Fix this
+  // #ifndef _WIN32
+  Init_psych();
+  rb_provide("psych");
+  rb_provide("psych.so");
+  // #endif
+
+  Init_ripper();
+  rb_provide("ripper");
+  rb_provide("ripper.so");
+
+  Init_rmd160();
+  rb_provide("rmd160");
+  rb_provide("digest/rmd160");
+  rb_provide("rmd160.so");
+  rb_provide("digest/rmd160.so");
+
+  Init_sdbm();
+  rb_provide("sdbm");
+  rb_provide("sdbm.so");
+
+  Init_sha1();
+  rb_provide("sha1");
+  rb_provide("digest/sha1");
+  rb_provide("sha1.so");
+  rb_provide("digest/sha1.so");
+
+  Init_sha2();
+  rb_provide("sha2");
+  rb_provide("digest/sha2");
+  rb_provide("sha2.so");
+  rb_provide("digest/sha2.so");
+
+  Init_sizeof();
+  rb_provide("sizeof");
+  rb_provide("sizeof.so");
+
+  Init_socket();
+  rb_provide("socket");
+  rb_provide("socket.so");
+
+  Init_stringio();
+  rb_provide("stringio");
+  rb_provide("stringio.so");
+
+  Init_strscan();
+  rb_provide("strscan");
+  rb_provide("strscan.so");
+
+  Init_wait();
+  rb_provide("wait");
+  rb_provide("io/wait");
+  rb_provide("wait.so");
+  rb_provide("io/wait.so");
+
+  Init_zlib();
+  rb_provide("zlib");
+  rb_provide("zlib.so");
+
+  Init_openssl();
+  rb_provide("openssl");
+  rb_provide("openssl.so");
+
+  Init_nonblock();
+  rb_provide("io/nonblock");
+  rb_provide("io/nonblock.so");
+
+#ifndef _WIN32
+  // Init_console on Windows fails to load openssl
+  Init_console();
+  rb_provide("console");
+  rb_provide("console.so");
+
+  Init_dbm();
+  rb_provide("dbm");
+  rb_provide("dbm.so");
+
+  Init_gdbm();
+  rb_provide("gdbm");
+  rb_provide("gdbm.so");
+
+  Init_pty();
+  rb_provide("pty");
+  rb_provide("pty.so");
+
+  Init_readline();
+  rb_provide("readline");
+  rb_provide("readline.so");
+
+  Init_syslog();
+  rb_provide("syslog");
+  rb_provide("syslog.so");
+#endif
+}
+
+}  // namespace openstudio
diff --git a/ruby/engine/InitRubyEngine.hpp b/ruby/engine/InitRubyEngine.hpp
new file mode 100644
index 00000000000..6e8c94457e3
--- /dev/null
+++ b/ruby/engine/InitRubyEngine.hpp
@@ -0,0 +1,116 @@
+#ifndef INITRUBYENGINE_included
+#define INITRUBYENGINE_included
+
+extern "C"
+{
+  void Init_encdb();
+  void Init_big5();
+  void Init_cesu_8();
+  void Init_cp949();
+  void Init_emacs_mule();
+  void Init_euc_jp();
+  void Init_euc_kr();
+  void Init_euc_tw();
+  void Init_gb18030();
+  void Init_gb2312();
+  void Init_gbk();
+  void Init_iso_8859_1();
+  void Init_iso_8859_10();
+  void Init_iso_8859_11();
+  void Init_iso_8859_13();
+  void Init_iso_8859_14();
+  void Init_iso_8859_15();
+  void Init_iso_8859_16();
+  void Init_iso_8859_2();
+  void Init_iso_8859_3();
+  void Init_iso_8859_4();
+  void Init_iso_8859_5();
+  void Init_iso_8859_6();
+  void Init_iso_8859_7();
+  void Init_iso_8859_8();
+  void Init_iso_8859_9();
+  void Init_koi8_r();
+  void Init_koi8_u();
+  void Init_shift_jis();
+  void Init_utf_16be();
+  void Init_utf_16le();
+  void Init_utf_32be();
+  void Init_utf_32le();
+  void Init_windows_1250();
+  void Init_windows_1251();
+  void Init_windows_1252();
+  void Init_windows_1253();
+  void Init_windows_1254();
+  void Init_windows_1257();
+  void Init_windows_31j();
+
+  void Init_transdb();
+  void Init_trans_big5();
+  void Init_trans_cesu_8();
+  void Init_trans_chinese();
+  void Init_trans_ebcdic();
+  void Init_trans_emoji();
+  void Init_trans_emoji_iso2022_kddi();
+  void Init_trans_emoji_sjis_docomo();
+  void Init_trans_emoji_sjis_kddi();
+  void Init_trans_emoji_sjis_softbank();
+  void Init_trans_escape();
+  void Init_trans_gb18030();
+  void Init_trans_gbk();
+  void Init_trans_iso2022();
+  void Init_trans_japanese();
+  void Init_trans_japanese_euc();
+  void Init_trans_japanese_sjis();
+  void Init_trans_korean();
+  void Init_trans_single_byte();
+  void Init_trans_utf8_mac();
+  void Init_trans_utf_16_32();
+
+  void Init_bigdecimal();
+  void Init_bigdecimal(void);
+  void Init_continuation(void);
+  void Init_coverage(void);
+  void Init_cparse(void);
+  void Init_date_core(void);
+  void Init_digest(void);
+  void Init_escape(void);
+  void Init_etc(void);
+  void Init_fcntl(void);
+  void Init_fiber(void);
+  void Init_fiddle(void);
+  void Init_generator(void);
+  void Init_md5(void);
+  void Init_monitor(void);
+  void Init_nkf(void);
+  void Init_nonblock(void);
+  void Init_objspace(void);
+  void Init_parser(void);
+  void Init_pathname(void);
+  void Init_psych(void);
+  void Init_ripper(void);
+  void Init_rmd160(void);
+  void Init_sdbm(void);
+  void Init_sha1(void);
+  void Init_sha2(void);
+  void Init_sizeof(void);
+  void Init_socket(void);
+  void Init_stringio(void);
+  void Init_strscan(void);
+  void Init_wait(void);
+  void Init_zlib(void);
+
+  void Init_openssl(void);
+  void Init_ruby_description(void);
+
+#ifndef _WIN32
+  void Init_console(void);
+  void Init_dbm(void);
+  void Init_gdbm(void);
+  void Init_pty(void);
+  void Init_readline(void);
+  void Init_syslog(void);
+#endif
+
+}  // extern C
+
+#endif  // INITRUBYENGINE_included
diff --git a/ruby/engine/RubyEngine.cpp b/ruby/engine/RubyEngine.cpp
new file mode 100644
index 00000000000..6a0d698bebb
--- /dev/null
+++ b/ruby/engine/RubyEngine.cpp
@@ -0,0 +1,117 @@
+#include "RubyEngine.hpp"
+#include "InitRubyBindings.hpp"
+#include <embedded_files.hxx>
+#include <rubyengine_export.h>
+#include <csignal>
+#include <stdexcept>
+#include <string>
+#include <ctime>
+
+#ifdef __GNUC__
+#  pragma GCC diagnostic push
+#  pragma GCC diagnostic ignored "-Wregister"
+#  pragma GCC diagnostic ignored "-Wunused-parameter"
+#endif
+
+#ifdef __GNUC__
+#  pragma GCC diagnostic pop
+#endif
+
+static int argc = 0;
+static char** argv = nullptr;
+
+unsigned init() {
+  ruby_sysinit(&argc, &argv);
+  ruby_setup();
+  return 0u;
+}
+
+static unsigned i = init();
+
+extern "C"
+{
+  void Init_EmbeddedScripting(void);
+}
+
+namespace openstudio {
+
+VALUE initRestOfOpenStudio(...) {
+  openstudio::ruby::initExtendedRubyBindings();
+  return Qtrue;
+};
+
+RubyEngine::RubyEngine(int argc, char* argv[]) : ScriptEngine(argc, argv) {
+  Init_EmbeddedScripting();
+  initRubyEngine();
+
+  openstudio::ruby::initBasicRubyBindings();
+
+  auto rubymodule = rb_define_module("OpenStudio");
+  rb_define_module_function(rubymodule, "init_rest_of_openstudio", initRestOfOpenStudio, 0);
+
+  ruby_set_argv(argc, argv);
+
+  // DLM: this will interpret any strings passed on the command line as UTF-8
+  // can we be smarter and detect the correct encoding? use wmain on windows to get utf-16?
+  // or we might want to follow ruby and allow '--external-encoding=UTF-8' as an input argument?
+  rb_enc_set_default_external(rb_enc_from_encoding(rb_utf8_encoding()));
+}
+
+RubyEngine::~RubyEngine() {
+  ruby_finalize();
+}
+
+ScriptObject RubyEngine::eval(std::string_view sv) {
+  std::string str{sv};
+  return ScriptObject{openstudio::evalString(str)};
+}
+
+void RubyEngine::exec(std::string_view sv) {
+  std::string str{sv};
+  [[maybe_unused]] const auto result = openstudio::evalString(str);
+}
+
+// convert the underlying object to the correct type, then return it as a void *
+// so the above template function can provide it back to the caller.
+void* RubyEngine::getAs_impl(ScriptObject& obj, const std::type_info& ti) {
+  auto val = std::any_cast<VALUE>(obj.object);
+
+  const auto& type_name = getRegisteredTypeName(ti);
+
+  void* return_value = nullptr;
+
+  auto* type = SWIG_TypeQuery(type_name.c_str());
+
+  if (!type) {
+    throw std::runtime_error("Unable to find type in SWIG");
+  }
+
+  const auto result = SWIG_ConvertPtr(val, &return_value, type, 0);
+
+  if (!SWIG_IsOK(result)) {
+    throw std::runtime_error("Error getting object from SWIG/Ruby");
+  }
+
+  return return_value;
+}
+
+}  // namespace openstudio
+
+extern "C"
+{
+  openstudio::ScriptEngine* makeScriptEngine(int argc, char* argv[]) {
+    return new openstudio::RubyEngine(argc, argv);
+  }
+
+  int rb_hasFile(const char* t_filename) {
+    // TODO Consider expanding this to use the path which we have artificially defined in embedded_help.rb
+    std::string expandedName = std::string(":/ruby/2.7.0/") + std::string(t_filename) + ".rb";
+    return embedded_files::hasFile(expandedName);
+  }
+
+  int rb_require_embedded(const char* t_filename) {
+    std::string require_script = R"(require ')" + std::string(t_filename) + R"(')";
+    openstudio::evalString(require_script);
+    return 0;
+  }
+}
diff --git a/ruby/engine/RubyEngine.hpp b/ruby/engine/RubyEngine.hpp
new file mode 100644
index 00000000000..19af788039e
--- /dev/null
+++ b/ruby/engine/RubyEngine.hpp
@@ -0,0 +1,41 @@
+#ifndef RUBYENGINE_included
+#define RUBYENGINE_included
+
+#include <ScriptEngine.hpp>
+#include <ScriptEngineAPI.hpp>
+#include <RubyInterpreter.hpp>
+
+namespace openstudio {
+
+class RubyEngine final : public ScriptEngine
+{
+ public:
+  RubyEngine(int argc = 0, char* argv[] = nullptr);
+  ~RubyEngine() override;
+
+  RubyEngine(const RubyEngine&) = delete;
+  RubyEngine(RubyEngine&&) = delete;
+  RubyEngine& operator=(const RubyEngine&) = delete;
+  RubyEngine& operator=(RubyEngine&&) = delete;
+
+  ScriptObject eval(std::string_view sv) override;
+  void exec(std::string_view sv) override;
+
+ protected:
+  // convert the underlying object to the correct type, then return it as a void *
+  // so the above template function can provide it back to the caller.
+  void* getAs_impl(ScriptObject& obj, const std::type_info&) override;
+
+  void initRubyEngine();
+  std::vector<std::string> includePaths;
+  RubyInterpreter rubyInterpreter{includePaths};
+};
+
+}  // namespace openstudio
+
+extern "C"
+{
+  SCRIPTENGINE_API openstudio::ScriptEngine* makeScriptEngine(int argc, char* argv[]);
+}
+
+#endif
diff --git a/src/cli/embedded_help.rb b/ruby/engine/embedded_help.rb
similarity index 100%
rename from src/cli/embedded_help.rb
rename to ruby/engine/embedded_help.rb
diff --git a/src/cli/measure_manager.rb b/ruby/engine/measure_manager.rb
similarity index 100%
rename from src/cli/measure_manager.rb
rename to ruby/engine/measure_manager.rb
diff --git a/src/cli/measure_manager_driver.rb b/ruby/engine/measure_manager_driver.rb
similarity index 100%
rename from src/cli/measure_manager_driver.rb
rename to ruby/engine/measure_manager_driver.rb
diff --git a/src/cli/measure_manager_server.rb b/ruby/engine/measure_manager_server.rb
similarity index 95%
rename from src/cli/measure_manager_server.rb
rename to ruby/engine/measure_manager_server.rb
index b7cf1f0b08a..a329fe836d9 100644
--- a/src/cli/measure_manager_server.rb
+++ b/ruby/engine/measure_manager_server.rb
@@ -272,12 +272,13 @@ def do_POST (request, response)
         class_name = data[:class_name]
         taxonomy_tag = data[:taxonomy_tag]
         measure_type = data[:measure_type]
+        measure_language =  data.fetch(:measure_language, 'Ruby') # Fall back
         description = data[:description]
         modeler_description = data[:modeler_description]
 
         # creating measure will throw if directory exists but is not empty
         measure_dir = File.expand_path(measure_dir)
-        OpenStudio::BCLMeasure.new(display_name, class_name, measure_dir, taxonomy_tag, measure_type.to_MeasureType, description, modeler_description)
+        OpenStudio::BCLMeasure.new(display_name, class_name, measure_dir, taxonomy_tag, measure_type.to_MeasureType, description, modeler_description, measure_language.to_MeasureLanguage)
 
         measure = @measure_manager.get_measure(measure_dir, true)
         result = @measure_manager.measure_hash(measure_dir, measure)
@@ -296,6 +297,9 @@ def do_POST (request, response)
         class_name = data[:class_name]
         taxonomy_tag = data[:taxonomy_tag]
         measure_type = data[:measure_type]
+        # If not supplied it's fine, it's use old_measure.measureLanguage which
+        # if not found defaults to 'Ruby', so fall back is all good
+        measure_language = data[:measure_language]
         description = data[:description]
         modeler_description = data[:modeler_description]
         force_reload = data[:force_reload] ? data[:force_reload] : false
@@ -310,6 +314,7 @@ def do_POST (request, response)
         class_name = old_measure.className if class_name.nil?
         taxonomy_tag = old_measure.taxonomyTag if taxonomy_tag.nil?
         measure_type = old_measure.measureType.valueName if measure_type.nil?
+        measure_language = old_measure.measureLanguage.valueName if measure_language.nil?
         description = old_measure.description if description.nil?
         modeler_description = old_measure.modelerDescription if modeler_description.nil?
         name = OpenStudio::toUnderscoreCase(class_name)
@@ -329,10 +334,12 @@ def do_POST (request, response)
         new_measure.setClassName(class_name)
         new_measure.setTaxonomyTag(taxonomy_tag)
         new_measure.setMeasureType(measure_type.to_MeasureType)
+        new_measure.setMeasureLanguage(measure_language.to_MeasureLanguage)
         new_measure.setDescription(description)
         new_measure.setModelerDescription(modeler_description)
 
         new_measure.updateMeasureScript(old_measure.measureType, measure_type.to_MeasureType,
+                                        old_measure.measureLanguage, measure_language.to_MeasureLanguage,
                                         old_measure.className, class_name,
                                         display_name, description, modeler_description)
         new_measure.updateMeasureTests(old_measure.className, class_name)
diff --git a/src/cli/openstudio_cli.rb b/ruby/engine/openstudio_cli.rb
similarity index 100%
rename from src/cli/openstudio_cli.rb
rename to ruby/engine/openstudio_cli.rb
diff --git a/src/cli/openstudio_init_extended.rb b/ruby/engine/openstudio_init_extended.rb
similarity index 100%
rename from src/cli/openstudio_init_extended.rb
rename to ruby/engine/openstudio_init_extended.rb
diff --git a/ruby/interpreter/CMakeLists.txt b/ruby/interpreter/CMakeLists.txt
new file mode 100644
index 00000000000..0216a323a41
--- /dev/null
+++ b/ruby/interpreter/CMakeLists.txt
@@ -0,0 +1,38 @@
+# "Link" to this when you need to include ruby.h,
+# but don't want to link to libruby.
+add_library(rubyinterpreter-declarations INTERFACE)
+get_target_property(RUBY_INCLUDE_DIRS CONAN_PKG::openstudio_ruby INTERFACE_INCLUDE_DIRECTORIES)
+target_include_directories(rubyinterpreter-declarations INTERFACE
+  ${RUBY_INCLUDE_DIRS}
+  ${CMAKE_CURRENT_SOURCE_DIR}
+)
+
+# Must call CMake itself in order to set the SWIG_LIB env var for add_custom_command
+add_custom_command(
+  OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/SWIGRubyRuntime.hxx"
+  COMMAND ${CMAKE_COMMAND} -E env SWIG_LIB="${SWIG_LIB}"
+          "${SWIG_EXECUTABLE}"
+          "-ruby"
+          -external-runtime "${CMAKE_CURRENT_BINARY_DIR}/SWIGRubyRuntime.hxx"
+)
+
+# Link to rubyinterpreter when you need to link to OpenStudio's flavor of the Ruby interpreter
+if (CMAKE_VERSION VERSION_LESS 3.19.0)
+  add_library(rubyinterpreter INTERFACE)
+  target_sources(rubyinterpreter PUBLIC INTERFACE
+    RubyInterpreter.hpp
+    RubyEval.hpp
+    RubyException.hpp
+    "${CMAKE_CURRENT_BINARY_DIR}/SWIGRubyRuntime.hxx"
+  )
+else()
+  add_library(rubyinterpreter INTERFACE
+    RubyInterpreter.hpp
+    RubyEval.hpp
+    RubyException.hpp
+    "${CMAKE_CURRENT_BINARY_DIR}/SWIGRubyRuntime.hxx"
+  )
+endif()
+
+target_include_directories(rubyinterpreter INTERFACE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR})
+target_link_libraries(rubyinterpreter INTERFACE CONAN_PKG::openstudio_ruby)
diff --git a/ruby/interpreter/InitDeclarations.hpp b/ruby/interpreter/InitDeclarations.hpp
new file mode 100644
index 00000000000..990707904d6
--- /dev/null
+++ b/ruby/interpreter/InitDeclarations.hpp
@@ -0,0 +1,118 @@
+//void Init_EmbeddedScripting(void);
+//INIT_DECLARATIONS;
+
+//VALUE init_rest_of_openstudio(...) {
+//  init_openstudio_internal_extended();
+//  return Qtrue;
+//}
+
+extern "C"
+{
+  void Init_encdb();
+  void Init_big5();
+  void Init_cesu_8();
+  void Init_cp949();
+  void Init_emacs_mule();
+  void Init_euc_jp();
+  void Init_euc_kr();
+  void Init_euc_tw();
+  void Init_gb18030();
+  void Init_gb2312();
+  void Init_gbk();
+  void Init_iso_8859_1();
+  void Init_iso_8859_10();
+  void Init_iso_8859_11();
+  void Init_iso_8859_13();
+  void Init_iso_8859_14();
+  void Init_iso_8859_15();
+  void Init_iso_8859_16();
+  void Init_iso_8859_2();
+  void Init_iso_8859_3();
+  void Init_iso_8859_4();
+  void Init_iso_8859_5();
+  void Init_iso_8859_6();
+  void Init_iso_8859_7();
+  void Init_iso_8859_8();
+  void Init_iso_8859_9();
+  void Init_koi8_r();
+  void Init_koi8_u();
+  void Init_shift_jis();
+  void Init_utf_16be();
+  void Init_utf_16le();
+  void Init_utf_32be();
+  void Init_utf_32le();
+  void Init_windows_1250();
+  void Init_windows_1251();
+  void Init_windows_1252();
+  void Init_windows_1253();
+  void Init_windows_1254();
+  void Init_windows_1257();
+  void Init_windows_31j();
+
+  void Init_transdb();
+  void Init_trans_big5();
+  void Init_trans_cesu_8();
+  void Init_trans_chinese();
+  void Init_trans_ebcdic();
+  void Init_trans_emoji();
+  void Init_trans_emoji_iso2022_kddi();
+  void Init_trans_emoji_sjis_docomo();
+  void Init_trans_emoji_sjis_kddi();
+  void Init_trans_emoji_sjis_softbank();
+  void Init_trans_escape();
+  void Init_trans_gb18030();
+  void Init_trans_gbk();
+  void Init_trans_iso2022();
+  void Init_trans_japanese();
+  void Init_trans_japanese_euc();
+  void Init_trans_japanese_sjis();
+  void Init_trans_korean();
+  void Init_trans_single_byte();
+  void Init_trans_utf8_mac();
+  void Init_trans_utf_16_32();
+
+  void Init_bigdecimal();
+  void Init_bigdecimal(void);
+  void Init_continuation(void);
+  void Init_coverage(void);
+  void Init_cparse(void);
+  void Init_date_core(void);
+  void Init_digest(void);
+  void Init_escape(void);
+  void Init_etc(void);
+  void Init_fcntl(void);
+  void Init_fiber(void);
+  void Init_fiddle(void);
+  void Init_generator(void);
+  void Init_md5(void);
+  void Init_monitor(void);
+  void Init_nkf(void);
+  void Init_nonblock(void);
+  void Init_objspace(void);
+  void Init_parser(void);
+  void Init_pathname(void);
+  void Init_psych(void);
+  void Init_ripper(void);
+  void Init_rmd160(void);
+  void Init_sdbm(void);
+  void Init_sha1(void);
+  void Init_sha2(void);
+  void Init_sizeof(void);
+  void Init_socket(void);
+  void Init_stringio(void);
+  void Init_strscan(void);
+  void Init_wait(void);
+  void Init_zlib(void);
+
+  void Init_openssl(void);
+  void Init_ruby_description(void);
+
+#ifndef _WIN32
+  void Init_console(void);
+  void Init_dbm(void);
+  void Init_gdbm(void);
+  void Init_pty(void);
+  void Init_readline(void);
+  void Init_syslog(void);
+#endif
+}
diff --git a/ruby/interpreter/RubyEval.hpp b/ruby/interpreter/RubyEval.hpp
new file mode 100644
index 00000000000..6175f5c86b6
--- /dev/null
+++ b/ruby/interpreter/RubyEval.hpp
@@ -0,0 +1,33 @@
+#ifndef RUBYEVAL_included
+#define RUBYEVAL_included
+
+#include "ruby.h"
+#include "./RubyException.hpp"
+
+namespace openstudio {
+
+inline VALUE evaluateSimpleImpl(VALUE arg) {
+  return rb_eval_string(StringValuePtr(arg));
+}
+
+inline VALUE evalString(const std::string& t_str) {
+  VALUE val = rb_str_new2(t_str.c_str());
+  int error;
+  // save and restore the current working directory in case the call to ruby upsets it
+  VALUE result = rb_protect(evaluateSimpleImpl, val, &error);
+  if (error != 0) {
+    VALUE errval = rb_eval_string("$!.to_s");
+    char* str = StringValuePtr(errval);
+    std::string err(str);
+    VALUE locval = rb_eval_string("$@.to_s");
+    str = StringValuePtr(locval);
+    std::string loc(str);
+    throw RubyException(err, loc);
+  }
+
+  return result;
+}
+
+}  // namespace openstudio
+
+#endif  // RUBYEVAL_included
diff --git a/src/cli/RubyException.hpp b/ruby/interpreter/RubyException.hpp
similarity index 100%
rename from src/cli/RubyException.hpp
rename to ruby/interpreter/RubyException.hpp
diff --git a/src/cli/RubyInterpreter.hpp b/ruby/interpreter/RubyInterpreter.hpp
similarity index 94%
rename from src/cli/RubyInterpreter.hpp
rename to ruby/interpreter/RubyInterpreter.hpp
index b50575a2770..84d0175bdf8 100644
--- a/src/cli/RubyInterpreter.hpp
+++ b/ruby/interpreter/RubyInterpreter.hpp
@@ -27,11 +27,10 @@
 *  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 ***********************************************************************************************************************/
 
-#ifndef CLI_RUBYINTERPRETER_HPP
-#define CLI_RUBYINTERPRETER_HPP
-
-#include "RubyException.hpp"
+#ifndef RUBYINTERPRETER_HPP
+#define RUBYINTERPRETER_HPP
 
+#include "RubyEval.hpp"
 #include <vector>
 #include <string>
 #include <map>
@@ -686,16 +685,12 @@
 
 #include <ruby.h>
 #include <ruby/encoding.h>
+#include <SWIGRubyRuntime.hxx>
 
-#include <cli/SWIGRubyRuntime.hxx>
-
+// RubyInterpreter predates RubyEngine. It is possible that the two things should be merged into one.
 class RubyInterpreter
 {
  private:
-  // dummy arguments passed to ruby_options
-  // int m_argc;
-  // char** m_argv;
-
   static void addIncludePath(std::vector<std::string>& includePaths, const std::string& includePath) {
     includePaths.push_back("-I");
     includePaths.push_back(includePath);
@@ -703,33 +698,6 @@ class RubyInterpreter
 
  public:
   explicit RubyInterpreter(const std::vector<std::string>& t_includePaths) {
-
-    //// set load paths
-    //std::vector<std::string> rubyArgs;
-
-    //rubyArgs.emplace_back("-EUTF-8");
-
-    //for (const auto &p : t_includePaths) {
-    //  addIncludePath(rubyArgs, p);
-    //}
-
-    //// and now give the interpreter something to parse, so that it doesn't sit
-    //// waiting on stdin from us
-    //rubyArgs.emplace_back("-e");
-    //rubyArgs.emplace_back("");
-
-    //m_argc = static_cast<int>(rubyArgs.size());
-    //m_argv = new char*[m_argc];
-
-    //for (int i = 0; i < m_argc; ++i){
-    //  m_argv[i] = new char[rubyArgs[i].size() + 1];
-    //  strcpy(m_argv[i], rubyArgs[i].c_str());
-    //}
-
-    //// the return value of ruby_options is the parsed node of our "script"
-    //// from the -e "" we passed in. This could be used to actually parse / eval something if we wanted
-    //ruby_options(m_argc, m_argv);
-
     // register some default types that we want to pass in / out of the ruby system
     registerType<int>("int");
     registerType<long>("long");
@@ -737,17 +705,9 @@ class RubyInterpreter
     registerType<double>("double");
     registerType<std::string>("std::string");
     registerType<float>("float");
-
-    // set the script name if desired
-    //ruby_script("script_name");
   }
 
-  ~RubyInterpreter() {
-    //for (int i = 0; i < m_argc; i++){
-    //  delete[] m_argv[i];
-    //}
-    //delete[] m_argv;
-  }
+  ~RubyInterpreter() {}
 
   /// Register a type along with its vector versions so that it can be used
   /// from within the Ruby wrapper. These are necessary because there's no way to divine
@@ -765,26 +725,7 @@ class RubyInterpreter
   // TODO: should this be static?
   // cppcheck-suppress functionStatic
   void evalString(const std::string& t_str) {
-
-    VALUE val = rb_str_new2(t_str.c_str());
-    int error;
-
-    // save and restore the current working directory in case the call to ruby upsets it
-    //      const auto cwd = openstudio::filesystem::current_dir();
-    rb_protect(evaluateSimpleImpl, val, &error);
-    //      openstudio::filesystem::current_dir(cwd);
-
-    if (error != 0) {
-      VALUE errval = rb_eval_string("$!.to_s");
-      char* str = StringValuePtr(errval);
-      std::string err(str);
-
-      VALUE locval = rb_eval_string("$@.to_s");
-      str = StringValuePtr(locval);
-      std::string loc(str);
-
-      throw RubyException(err, loc);
-    }
+    openstudio::evalString(t_str);
   }
 
   /// Execute a function by name with 0 parameters and no return value
@@ -880,11 +821,6 @@ class RubyInterpreter
 
   std::map<std::string, std::string> m_types;
 
-  // Used for our rb_protect calls.
-  static VALUE evaluateSimpleImpl(VALUE arg) {
-    return rb_eval_string(StringValuePtr(arg));
-  }
-
   // Returns the swig_type_info for the templated type if it's been registered previously
   // with registerType
   template <typename Type>
@@ -1518,4 +1454,4 @@ class RubyInterpreter
 #  endif
 #endif
 
-#endif  // CLI_RUBYINTERPRETER_HPP
+#endif  // RUBYINTERPRETER_HPP
diff --git a/ruby/module/CMakeLists.txt b/ruby/module/CMakeLists.txt
new file mode 100644
index 00000000000..540de9b7a82
--- /dev/null
+++ b/ruby/module/CMakeLists.txt
@@ -0,0 +1,99 @@
+if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "^(Apple)?Clang$")
+  # using Clang
+  set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -undefined dynamic_lookup")
+elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
+  # using GCC
+  #set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --unresolved-symbols=ignore-all")
+  #set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --unresolved-symbols=ignore-all")
+endif()
+
+
+include_directories(${CMAKE_CURRENT_BINARY_DIR} ${PROJECT_BINARY_DIR} ${PROJECT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR} )
+
+# openstudio ruby bindings to load into normal ruby
+add_library(
+  openstudio_rb
+  MODULE
+  RubyAPI.hpp
+  openstudio_rb.cpp
+)
+set_target_properties(openstudio_rb PROPERTIES PREFIX "")
+set_target_properties(openstudio_rb PROPERTIES OUTPUT_NAME openstudio)
+target_include_directories(openstudio_rb SYSTEM PRIVATE ${RUBY_INCLUDE_DIRS})
+
+# change output directory so the openstudio.pdb file does not conflict between this target and the CLI
+set_target_properties(openstudio_rb PROPERTIES ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_ARCHIVE_OUTPUT_DIRECTORY}/ruby/")
+set_target_properties(openstudio_rb PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/ruby/")
+set_target_properties(openstudio_rb PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/ruby/")
+
+if(APPLE)
+  set_target_properties(openstudio_rb PROPERTIES SUFFIX ".bundle" )
+else()
+  set_target_properties(openstudio_rb PROPERTIES SUFFIX ".so" )
+endif()
+
+if(MSVC)
+  set_target_properties(openstudio_rb PROPERTIES COMPILE_FLAGS "/bigobj /wd4996 /wd5033")
+endif()
+
+add_custom_command(TARGET openstudio_rb
+  POST_BUILD
+  COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/openstudio.rb $<TARGET_FILE_DIR:openstudio_rb>/openstudio.rb
+)
+
+file(GLOB_RECURSE OPENSTUDIO_FILES FOLLOW_SYMLINKS RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/openstudio/" "${CMAKE_CURRENT_SOURCE_DIR}/openstudio/**/*.*")
+foreach(OPENSTUDIO_FILE ${OPENSTUDIO_FILES})
+  add_custom_command(TARGET openstudio_rb
+    POST_BUILD
+    COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/openstudio/${OPENSTUDIO_FILE}" "$<TARGET_FILE_DIR:openstudio_rb>/openstudio/${OPENSTUDIO_FILE}"
+  )
+endforeach()
+
+if( WIN32 )
+  # Dynamically link to openstudiolib.dll on Windows only
+  target_link_libraries(
+    openstudio_rb
+    PUBLIC
+    openstudiolib
+    PRIVATE
+    rubybindings
+    ${ALL_RUBY_BINDING_TARGETS}
+  )
+  target_link_libraries(openstudio_rb PRIVATE ${RUBY_MINGW_STUB_LIB})
+else()
+  # statically link everywhere else
+  target_link_libraries(
+    openstudio_rb
+    PRIVATE
+    openstudio_utilities
+    openstudio_airflow
+    openstudio_model
+    openstudio_energyplus
+    openstudio_epjson
+    openstudio_measure
+    openstudio_osversion
+    openstudio_sdd
+    openstudio_isomodel
+    openstudio_gbxml
+    openstudio_gltf
+    openstudio_radiance
+    rubybindings
+    ${ALL_RUBY_BINDING_TARGETS}
+  )
+endif()
+
+if (APPLE)
+  target_link_libraries(openstudio_rb
+    PRIVATE
+    ${OPENSSL_CRYPTO_LIBRARY}
+    ${OPENSSL_SSL_LIBRARY}
+  )
+endif()
+
+install(TARGETS openstudio_rb
+  EXPORT openstudio
+  DESTINATION Ruby
+  COMPONENT "RubyAPI"
+)
+install(FILES openstudio.rb DESTINATION Ruby COMPONENT "RubyAPI")
+install(DIRECTORY openstudio DESTINATION Ruby COMPONENT "RubyAPI")
diff --git a/ruby/RubyAPI.hpp b/ruby/module/RubyAPI.hpp
similarity index 88%
rename from ruby/RubyAPI.hpp
rename to ruby/module/RubyAPI.hpp
index cae1ef38df3..b88b756b575 100644
--- a/ruby/RubyAPI.hpp
+++ b/ruby/module/RubyAPI.hpp
@@ -30,16 +30,16 @@
 #ifndef RUBYAPI_HPP
 #define RUBYAPI_HPP
 
-  #if (_WIN32 || _MSC_VER)
-    #ifdef openstudio_rb_EXPORTS 
-      #define RUBY_API __declspec(dllexport)
-    #elif defined openstudio_modeleditor_rb_EXPORTS
-      #define RUBY_API __declspec(dllexport)
-    #else
-      #define RUBY_API __declspec(dllimport)
-    #endif
-  #else
-    #define RUBY_API
-  #endif
+#if (_WIN32 || _MSC_VER)
+#  ifdef openstudio_rb_EXPORTS
+#    define RUBY_API __declspec(dllexport)
+#  elif defined openstudio_modeleditor_rb_EXPORTS
+#    define RUBY_API __declspec(dllexport)
+#  else
+#    define RUBY_API __declspec(dllimport)
+#  endif
+#else
+#  define RUBY_API
+#endif
 
 #endif
diff --git a/ruby/openstudio.rb b/ruby/module/openstudio.rb
similarity index 68%
rename from ruby/openstudio.rb
rename to ruby/module/openstudio.rb
index 9d83720ead7..15699e71004 100644
--- a/ruby/openstudio.rb
+++ b/ruby/module/openstudio.rb
@@ -32,27 +32,40 @@
 if /mswin/.match(RUBY_PLATFORM) or /mingw/.match(RUBY_PLATFORM)
 
   if defined?(RubyInstaller::Runtime)
-
-    cur_location = File.dirname(__FILE__)
-
-    build_type = File.basename(cur_location)
-
+    require 'pathname'
+    # This is an absolute path
+    cur_location = Pathname.new(__dir__)
+    build_type = cur_location.basename.to_s
     if build_type == "Debug" or build_type == "Release" or build_type == "RelWithDebugInfo" or build_type == "RelMinSize"
-      # in build dir execution
-      relative_dll_path = File.join(File.dirname(File.dirname(cur_location)), build_type)
-      RubyInstaller::Runtime.add_dll_directory(relative_dll_path)
+      # Multi configuration build dir:
+      # We're at `build/Products/ruby/Release/openstudio.rb`
+      # and we want openstudiolib.dll in `Products/Release`
+      abs_dll_path = cur_location.parent.parent.join(build_type)
+      # First thing is done in there is to foward to DllDirectory, which does
+      # File.expand_path anyways in the initialize method
+      # puts "build_type=#{build_type}, abs_dll_path=#{abs_dll_path}"
+      RubyInstaller::Runtime.add_dll_directory(abs_dll_path)
+    elsif cur_location.parent.basename.to_s == "Products"
+      # This is a build dir execution, but without a multi configuration
+      # generator such as Ninja.
+      # We're at `build/Products/ruby` and we want `build/Products/`
+      abs_dll_path =  cur_location.parent
+      # puts "cur_location.parent.basename=#{cur_location.parent.basename}, abs_dll_path=#{abs_dll_path}"
+      RubyInstaller::Runtime.add_dll_directory(abs_dll_path)
     else
-      # install dir execution
+      # install dir execution. We at `install/Ruby/openstudio.rb'
+      # and we want `install/bin`
       ['bin', 'lib'].each do |p|
-        if File.exists?(File.join(File.dirname(cur_location), p))
-          RubyInstaller::Runtime.add_dll_directory(File.join(File.dirname(cur_location), p))
+        d = cur_location.join('..', p)
+        if d.exist?
+          RubyInstaller::Runtime.add_dll_directory(d)
         end
       end
     end
   end
 else
   # Do something here for Mac OSX environments
-  ENV['PATH'] = "#{File.dirname(__FILE__)}:#{original_path}"
+  ENV['PATH'] = "#{__dir__}:#{original_path}"
 end
 
 # require openstudio.so
@@ -62,4 +75,4 @@
 ENV['PATH'] = original_path
 
 # add this directory to Ruby load path
-$:.unshift(File.expand_path(File.dirname(__FILE__)))
+$:.unshift(__dir__)
diff --git a/ruby/openstudio/energyplus/find_energyplus.rb b/ruby/module/openstudio/energyplus/find_energyplus.rb
similarity index 100%
rename from ruby/openstudio/energyplus/find_energyplus.rb
rename to ruby/module/openstudio/energyplus/find_energyplus.rb
diff --git a/ruby/openstudio/measure/ShowRunnerOutput.rb b/ruby/module/openstudio/measure/ShowRunnerOutput.rb
similarity index 100%
rename from ruby/openstudio/measure/ShowRunnerOutput.rb
rename to ruby/module/openstudio/measure/ShowRunnerOutput.rb
diff --git a/ruby/openstudio/ruleset/ShowRunnerOutput.rb b/ruby/module/openstudio/ruleset/ShowRunnerOutput.rb
similarity index 100%
rename from ruby/openstudio/ruleset/ShowRunnerOutput.rb
rename to ruby/module/openstudio/ruleset/ShowRunnerOutput.rb
diff --git a/ruby/openstudio_rb.cpp b/ruby/module/openstudio_rb.cpp
similarity index 94%
rename from ruby/openstudio_rb.cpp
rename to ruby/module/openstudio_rb.cpp
index fb45f2c763a..3ea4238dc36 100644
--- a/ruby/openstudio_rb.cpp
+++ b/ruby/module/openstudio_rb.cpp
@@ -27,18 +27,16 @@
 *  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 ***********************************************************************************************************************/
 
-#include "init_openstudio.hpp"
+#include "InitRubyBindings.hpp"
 #include <RubyAPI.hpp>
 #include <iostream>
 #include <ruby.h>
 
-extern "C" {
+extern "C"
+{
 
-RUBY_API void Init_openstudio(void) {
-
-  init_openstudio_internal();
-
-}
+  RUBY_API void Init_openstudio(void) {
 
+    openstudio::ruby::initRubyBindings();
+  }
 }
-
diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt
index d01b0e817e7..db25b2c259b 100644
--- a/src/cli/CMakeLists.txt
+++ b/src/cli/CMakeLists.txt
@@ -2,321 +2,55 @@ if (APPLE)
    find_library(COREFOUNDATION_LIBRARY CoreFoundation )
 endif (APPLE)
 
-set(INIT_CALLS "")
-set(INIT_DECLARATIONS "")
-foreach(LIB ${EXTENSION_LIBS_LIST})
-  get_filename_component(LIB_NAME ${LIB} NAME_WE)
-  list (FIND LIBS "${LIB_NAME}" _index)
-  if( ${_index} GREATER -1 )
-    set(INIT_DECLARATIONS "${INIT_DECLARATIONS}  void Init_${LIB_NAME}();\\\n" )
-    set(INIT_CALLS "${INIT_CALLS}  Init_${LIB_NAME}();\\\n" )
-    set(INIT_CALLS "${INIT_CALLS}  rb_provide(\"${LIB_NAME}.so\");\\\n" )
-  endif()
-endforeach()
-
-CONFIGURE_FILE_WITH_CHECKSUM("InitMacros.hxx.in" "${CMAKE_CURRENT_BINARY_DIR}/InitMacros.hxx")
-
-set(MODULE_ROOT "${CONAN_OPENSTUDIO_RUBY_ROOT}")
-if ("${MODULE_ROOT}" STREQUAL "")
-  set(MODULE_ROOT "${CONAN_OPENSTUDIO_RUBY_ROOT_DEBUG}")
-endif()
-
-message("Searching for ruby modules in '${MODULE_ROOT}/lib/**/*.rb'")
-file(GLOB_RECURSE  EXTENSION_RB  FOLLOW_SYMLINKS "${MODULE_ROOT}/lib/**/*.rb")
-
-foreach( _FILE ${EXTENSION_RB} )
-  file(RELATIVE_PATH LOCATION ${MODULE_ROOT}/lib ${_FILE})
-  list(APPEND FILES ${_FILE})
-  list(APPEND EMBEDDED_PATHS ${LOCATION})
-endforeach()
-
-set(GEMFILE_FILES "${OPENSTUDIO_GEMS_DIR}/openstudio-gems.gemspec" "${PROJECT_BINARY_DIR}/openstudio-gems/Gemfile" "${PROJECT_BINARY_DIR}/openstudio-gems/Gemfile.lock")
-file(GLOB_RECURSE RB_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.rb")
-file(GLOB_RECURSE ERB_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.erb")
-file(GLOB_RECURSE JS_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.js")
-file(GLOB_RECURSE CSS_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.css")
-file(GLOB_RECURSE GIF_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.gif")
-file(GLOB_RECURSE PNG_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.png")
-file(GLOB_RECURSE HTML_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.html")
-file(GLOB_RECURSE IDF_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.idf")
-file(GLOB_RECURSE OSM_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.osm")
-file(GLOB_RECURSE EPW_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.epw")
-file(GLOB_RECURSE DDY_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.ddy")
-file(GLOB_RECURSE STAT_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.stat")
-file(GLOB_RECURSE CSV_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.csv")
-file(GLOB_RECURSE JSON_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.json")
-file(GLOB_RECURSE SPEC_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.gemspec")
-file(GLOB_RECURSE GZ_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.gz")
-file(GLOB_RECURSE YML_FILES FOLLOW_SYMLINKS "${OPENSTUDIO_GEMS_DIR}/**/*.yml")
-file(GLOB_RECURSE OS_FILES FOLLOW_SYMLINKS "${CMAKE_CURRENT_SOURCE_DIR}/openstudio/**/*.rb") # DLM: this is temporary, these files should live somewhere else and be included in the shared Ruby lib
-
-foreach( _FILE ${GEMFILE_FILES} )
-  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
-  list(APPEND FILES ${_FILE})
-  list(APPEND EMBEDDED_PATHS ${LOCATION})
-endforeach()
-
-foreach( _FILE ${RB_FILES} )
-  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
-  list(APPEND FILES ${_FILE})
-  list(APPEND EMBEDDED_PATHS ${LOCATION})
-endforeach()
-
-foreach( _FILE ${ERB_FILES} )
-  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
-  list(APPEND FILES ${_FILE})
-  list(APPEND EMBEDDED_PATHS ${LOCATION})
-endforeach()
-
-foreach( _FILE ${JS_FILES} )
-  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
-  list(APPEND FILES ${_FILE})
-  list(APPEND EMBEDDED_PATHS ${LOCATION})
-endforeach()
-
-foreach( _FILE ${CSS_FILES} )
-  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
-  list(APPEND FILES ${_FILE})
-  list(APPEND EMBEDDED_PATHS ${LOCATION})
-endforeach()
-
-foreach( _FILE ${GIF_FILES} )
-  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
-  list(APPEND FILES ${_FILE})
-  list(APPEND EMBEDDED_PATHS ${LOCATION})
-endforeach()
-
-foreach( _FILE ${PNG_FILES} )
-  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
-  list(APPEND FILES ${_FILE})
-  list(APPEND EMBEDDED_PATHS ${LOCATION})
-endforeach()
-
-foreach( _FILE ${HTML_FILES} )
-  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
-  list(APPEND FILES ${_FILE})
-  list(APPEND EMBEDDED_PATHS ${LOCATION})
-endforeach()
-
-foreach( _FILE ${IDF_FILES} )
-  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
-  list(APPEND FILES ${_FILE})
-  list(APPEND EMBEDDED_PATHS ${LOCATION})
-endforeach()
-
-foreach( _FILE ${OSM_FILES} )
-  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
-  list(APPEND FILES ${_FILE})
-  list(APPEND EMBEDDED_PATHS ${LOCATION})
-endforeach()
-
-foreach( _FILE ${EPW_FILES} )
-  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
-  list(APPEND FILES ${_FILE})
-  list(APPEND EMBEDDED_PATHS ${LOCATION})
-endforeach()
-
-foreach( _FILE ${DDY_FILES} )
-  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
-  list(APPEND FILES ${_FILE})
-  list(APPEND EMBEDDED_PATHS ${LOCATION})
-endforeach()
-
-foreach( _FILE ${STAT_FILES} )
-  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
-  list(APPEND FILES ${_FILE})
-  list(APPEND EMBEDDED_PATHS ${LOCATION})
-endforeach()
-
-foreach( _FILE ${CSV_FILES} )
-  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
-  list(APPEND FILES ${_FILE})
-  list(APPEND EMBEDDED_PATHS ${LOCATION})
-endforeach()
-
-foreach( _FILE ${JSON_FILES} )
-  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
-  list(APPEND FILES ${_FILE})
-  list(APPEND EMBEDDED_PATHS ${LOCATION})
-endforeach()
-
-foreach( _FILE ${SPEC_FILES} )
-  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
-  list(APPEND FILES ${_FILE})
-  list(APPEND EMBEDDED_PATHS ${LOCATION})
-endforeach()
-
-foreach( _FILE ${GZ_FILES} )
-  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
-  list(APPEND FILES ${_FILE})
-  list(APPEND EMBEDDED_PATHS ${LOCATION})
-endforeach()
-
-foreach( _FILE ${YML_FILES} )
-  file(RELATIVE_PATH LOCATION "${PROJECT_BINARY_DIR}/openstudio-gems" ${_FILE})
-  list(APPEND FILES ${_FILE})
-  list(APPEND EMBEDDED_PATHS ${LOCATION})
-endforeach()
-
-# DLM: this is temporary, these files should live somewhere else and be included in the shared Ruby lib
-foreach( _FILE ${OS_FILES} )
-  file(RELATIVE_PATH LOCATION "${CMAKE_CURRENT_SOURCE_DIR}" ${_FILE})
-  list(APPEND FILES ${_FILE})
-  list(APPEND EMBEDDED_PATHS ${LOCATION})
-endforeach()
-
-list(APPEND FILES "${CMAKE_CURRENT_SOURCE_DIR}/embedded_help.rb")
-list(APPEND EMBEDDED_PATHS "embedded_help.rb")
-
-list(APPEND FILES "${CMAKE_CURRENT_SOURCE_DIR}/openstudio_cli.rb")
-list(APPEND EMBEDDED_PATHS "openstudio_cli.rb")
-
-list(APPEND FILES "${CMAKE_CURRENT_SOURCE_DIR}/openstudio_init_extended.rb")
-list(APPEND EMBEDDED_PATHS "openstudio_init_extended.rb")
-
-
-list(APPEND FILES "${CMAKE_CURRENT_SOURCE_DIR}/measure_manager.rb")
-list(APPEND EMBEDDED_PATHS "measure_manager.rb")
-
-list(APPEND FILES "${CMAKE_CURRENT_SOURCE_DIR}/measure_manager_server.rb")
-list(APPEND EMBEDDED_PATHS "measure_manager_server.rb")
-
-embed_files("${FILES}" "${EMBEDDED_PATHS}" OUTPUT)
-
-set_source_files_properties(EmbeddedScripting.i
-  PROPERTIES CPLUSPLUS true
-)
-
-include_directories(${CMAKE_CURRENT_BINARY_DIR}
-  ${PROJECT_BINARY_DIR}
-  ${PROJECT_SOURCE_DIR}
-  ${CMAKE_CURRENT_SOURCE_DIR}
-  ${OPENSTUDIO_GEMS_DIR}
+set(CLI_SRC
+  main.cpp
+  RubyCLI.hpp
+  RubyCLI.cpp
+  RunCommand.hpp
+  RunCommand.cpp
+  UpdateCommand.hpp
+  UpdateCommand.cpp
+  MeasureUpdateCommand.hpp
+  MeasureUpdateCommand.cpp
 )
 
-# Must call CMake itself in order to set the SWIG_LIB env var for add_custom_command
-add_custom_command(
-  OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/SWIGRubyRuntime.hxx"
-  COMMAND ${CMAKE_COMMAND} -E env SWIG_LIB="${SWIG_LIB}"
-          "${SWIG_EXECUTABLE}"
-          "-ruby"
-          -external-runtime "${CMAKE_CURRENT_BINARY_DIR}/SWIGRubyRuntime.hxx"
-)
-
-add_custom_command(
-  OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/embedded_scripting_wrap.cxx"
-  COMMAND ${CMAKE_COMMAND} -E env SWIG_LIB="${SWIG_LIB}"
-          "${SWIG_EXECUTABLE}"
-          "-ruby"
-          "-c++"
-          -o "${CMAKE_CURRENT_BINARY_DIR}/embedded_scripting_wrap.cxx"
-          "-fvirtual"
-          "-I${PROJECT_SOURCE_DIR}/src"
-          "-I${PROJECT_BINARY_DIR}/src"
-          "-I${OPENSTUDIO_GEMS_DIR}"
-          "-D_WINDOWS"
-          "-Fmicrosoft"
-          "${CMAKE_CURRENT_SOURCE_DIR}/EmbeddedScripting.i"
-  DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/EmbeddedScripting.i"
-          "${CMAKE_CURRENT_BINARY_DIR}/embedded_files.hxx"
-          "EmbeddedHelp.hpp"
-)
-
-set_source_files_properties(${EMBED_SOURCE_FILES} PROPERTIES HEADER_FILE_ONLY TRUE)
-
-source_group(embedded_files FILES ${OUTPUT})
-
 if(WIN32)
   CONFIGURE_FILE_WITH_CHECKSUM(openstudio.rc.in "${CMAKE_CURRENT_BINARY_DIR}/openstudio.rc")
-endif()
-
-if(WIN32)
 
   # This is really just about RC but instead of spending hours trying to figure how to disable /bigobj just for that part,
   # I removed the definition altogether and didn't hit any issue.
   remove_definitions(/bigobj)
 
   add_executable(openstudio
-    main.cpp
-    "${CMAKE_CURRENT_BINARY_DIR}/SWIGRubyRuntime.hxx"
-    RubyException.hpp
-    RubyInterpreter.hpp
-    "${CMAKE_CURRENT_BINARY_DIR}/embedded_scripting_wrap.cxx"
-    ${OUTPUT}
+    ${CLI_SRC}
     "${CMAKE_CURRENT_BINARY_DIR}/openstudio.rc"
   )
 else()
   add_executable(openstudio
-    main.cpp
-    "${CMAKE_CURRENT_BINARY_DIR}/SWIGRubyRuntime.hxx"
-    RubyException.hpp
-    RubyInterpreter.hpp
-    "${CMAKE_CURRENT_BINARY_DIR}/embedded_scripting_wrap.cxx"
-    ${OUTPUT}
+    ${CLI_SRC}
   )
 endif()
 
-
-# -Wno-deprecated-declaration, /wd4996: suppresses deprecated warning
-# -Wno-register, /wd5033: ISO C++1z does not allow �register� storage class specifier
-if(MSVC)
-  set_target_properties(openstudio PROPERTIES COMPILE_FLAGS "/bigobj /wd4996 /wd5033")
-  set_source_files_properties("${CMAKE_CURRENT_BINARY_DIR}/embedded_scripting_wrap.cxx" PROPERTIES COMPILE_FLAGS "-DRUBY_EMBEDDED" )
-else()
-  set_target_properties(openstudio PROPERTIES COMPILE_FLAGS "-Wno-deprecated-declarations -Wno-register")
-  set_source_files_properties("${CMAKE_CURRENT_BINARY_DIR}/embedded_scripting_wrap.cxx" PROPERTIES COMPILE_FLAGS "-DRUBY_EMBEDDED -Wno-unused-variable" )
-endif()
-
-# We are setting up a set of artificial dependencies here so that
-# cmake knows how to order the libraries properly
-#
-# Better: properly specifying these libs in the conan recipe
-# At the moment this is not possible due to some of them coming from the OS and some
-# being built by this project
-# TODO: Continue to make the conan deps more robust and get this sorted out
-add_library(additional_ruby_libs empty.cpp)
-add_library(ruby_libs empty.cpp)
-target_link_libraries(ruby_libs PUBLIC CONAN_PKG::openstudio_ruby)
-target_link_libraries(ruby_libs PUBLIC additional_ruby_libs)
-
-target_link_libraries(openstudio PRIVATE init_openstudio ruby_libs ${ALL_RUBY_BINDING_TARGETS})
-
-include("${OPENSTUDIO_GEMS_DIR}/export-extensions.cmake")
-
 target_link_libraries(openstudio
- PRIVATE
- openstudio_utilities
- openstudio_airflow
- openstudio_model
- openstudio_energyplus
- openstudio_epjson
- openstudio_measure
- openstudio_osversion
- openstudio_sdd
- openstudio_isomodel
- openstudio_gbxml
- openstudio_gltf
- openstudio_radiance
- ${ruby_extension_libs}
+  PRIVATE
+  openstudiolib
+  openstudio_workflow
+  CONAN_PKG::cli11
 )
 
-if (UNIX AND NOT APPLE)
-  target_link_libraries(additional_ruby_libs dl crypt anl)
-  target_link_libraries(openstudio PRIVATE "icui18n" "icuuc" "gmp")
-elseif(WIN32)
-  target_link_libraries(openstudio PRIVATE wsock32 ws2_32 Dbghelp Shlwapi Iphlpapi)
-  set_target_properties(openstudio PROPERTIES LINK_FLAGS_DEBUG "/NODEFAULTLIB:MSVCRT")
-else()
+if(BUILD_RUBY_BINDINGS)
+  add_dependencies(openstudio rubyengine)
+endif()
+
+if(BUILD_PYTHON_BINDINGS)
+  add_dependencies(openstudio pythonengine)
 endif()
 
 if( APPLE )
   target_link_libraries(openstudio PRIVATE ${COREFOUNDATION_LIBRARY})
 endif()
 
-install(TARGETS openstudio EXPORT openstudio DESTINATION bin COMPONENT "CLI")
-install(FILES openstudio_cli.rb DESTINATION Ruby COMPONENT "RubyAPI")
-install(FILES measure_manager.rb DESTINATION Ruby COMPONENT "RubyAPI")
-install(FILES measure_manager_server.rb DESTINATION Ruby COMPONENT "RubyAPI")
+install(TARGETS openstudio EXPORT openstudio COMPONENT "CLI")
 
 if( BUILD_PAT )
   if( APPLE )
@@ -332,35 +66,98 @@ endif()
 #                        T E S T I N G:   C T E S T S                         #
 ###############################################################################
 
-file(GLOB RUBY_TEST_SRC
-  # find all CLI test
-  "test/test*.rb"
+if(BUILD_TESTING)
 
-  # Also Run the ruby tests with the cli
-  "../../ruby/test/*.rb"
-)
+  add_test(NAME OpenStudioCLI.help
+    COMMAND $<TARGET_FILE:openstudio> --help
+  )
 
-# message("CLI: RUBY_TEST_SRC=${RUBY_TEST_SRC}")
+  add_test(NAME OpenStudioCLI.run_compact_ruby_only_osw
+    COMMAND $<TARGET_FILE:openstudio> run -w compact_ruby_only.osw
+    WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/resources/Examples/compact_osw/"
+  )
 
-# add a test for each unit test
-set(RUBY_TEST_REQUIRES "#include test files")
-foreach(f ${RUBY_TEST_SRC})
+  add_test(NAME OpenStudioCLI.Labs.openstudio_version
+    COMMAND $<TARGET_FILE:openstudio> labs openstudio_version
+  )
 
-  file(READ "${f}" CONTENTS)
-  string(REGEX MATCHALL "def +test_([A-Za-z_0-9 ]+)" FOUND_TESTS ${CONTENTS})
+  add_test(NAME OpenStudioCLI.Labs.energyplus_version
+    COMMAND $<TARGET_FILE:openstudio> labs energyplus_version
+  )
+
+  add_test(NAME OpenStudioCLI.Labs.ruby_version
+    COMMAND $<TARGET_FILE:openstudio> labs ruby_version
+  )
+
+  add_test(NAME OpenStudioCLI.Labs.python_version
+    COMMAND $<TARGET_FILE:openstudio> labs python_version
+  )
+
+  add_test(NAME OpenStudioCLI.Labs.ruby_execute_line
+    COMMAND $<TARGET_FILE:openstudio> labs -e "puts(OpenStudio::Model::Model.new())"
+  )
 
-  foreach(HIT ${FOUND_TESTS})
-    string(REGEX REPLACE "def +test_([A-Za-z_0-9]+)" "\\1" TEST_NAME ${HIT})
-    string(REGEX MATCH "/?([A-Za-z_0-9 ]+)\\.rb" FILE_NAME ${f})
-    string(REGEX REPLACE "/?([A-Za-z_0-9 ]+)\\.rb" "\\1" FILE_NAME ${FILE_NAME})
-    if(BUILD_TESTING)
+  add_test(NAME OpenStudioCLI.Labs.python_execute_line
+    COMMAND $<TARGET_FILE:openstudio> labs -c "print(openstudio.model.Model())"
+  )
 
-        add_test(NAME "CLITest-${FILE_NAME}-${TEST_NAME}"
-          COMMAND "${CMAKE_COMMAND}" -E chdir "${CMAKE_CURRENT_BINARY_DIR}"
-          "$<TARGET_FILE:openstudio>" "${f}" "--name=test_${TEST_NAME}"
-        )
+  add_test(NAME OpenStudioCLI.Labs.execute_ruby_script
+    COMMAND $<TARGET_FILE:openstudio> labs execute_ruby_script ${CMAKE_CURRENT_SOURCE_DIR}/test/execute_ruby_script_with_arguments_test.rb OpenStudioLabs "National Renewable Energy Laboratory"
+  )
+
+  add_test(NAME OpenStudioCLI.Labs.execute_python_script
+    COMMAND $<TARGET_FILE:openstudio> labs execute_python_script ${CMAKE_CURRENT_SOURCE_DIR}/test/execute_python_script_with_arguments_test.py OpenStudioLabs "National Renewable Energy Laboratory"
+  )
+
+  add_test(NAME OpenStudioCLI.Labs.Run_RubyOnly
+    COMMAND $<TARGET_FILE:openstudio> labs run -w compact_ruby_only.osw
+    WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/resources/Examples/compact_osw/"
+  )
+
+  add_test(NAME OpenStudioCLI.Labs.Run_PythonOnly
+    COMMAND $<TARGET_FILE:openstudio> labs run -w compact_python_only.osw
+    WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/resources/Examples/compact_osw/"
+  )
 
-      set_tests_properties("CLITest-${FILE_NAME}-${TEST_NAME}" PROPERTIES TIMEOUT 660 )
-    endif()
+  add_test(NAME OpenStudioCLI.Labs.Run_RubyPythonOnly_TwoMeasure
+    COMMAND $<TARGET_FILE:openstudio> labs run -w compact_python_only_twomeasures.osw
+    WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/resources/Examples/compact_osw/"
+  )
+
+  add_test(NAME OpenStudioCLI.Labs.Run_RubyPython
+    COMMAND $<TARGET_FILE:openstudio> labs run -w compact_ruby_and_python.osw
+    WORKING_DIRECTORY "${PROJECT_BINARY_DIR}/resources/Examples/compact_osw/"
+  )
+
+  file(GLOB RUBY_TEST_SRC
+    # find all CLI test
+    "test/test*.rb"
+
+    # Also Run the ruby tests with the cli
+    "../../ruby/test/*.rb"
+  )
+
+  # message("CLI: RUBY_TEST_SRC=${RUBY_TEST_SRC}")
+
+  # add a test for each unit test
+  set(RUBY_TEST_REQUIRES "#include test files")
+  foreach(f ${RUBY_TEST_SRC})
+
+    file(READ "${f}" CONTENTS)
+    string(REGEX MATCHALL "def +test_([A-Za-z_0-9 ]+)" FOUND_TESTS ${CONTENTS})
+
+    foreach(HIT ${FOUND_TESTS})
+      string(REGEX REPLACE "def +test_([A-Za-z_0-9]+)" "\\1" TEST_NAME ${HIT})
+      string(REGEX MATCH "/?([A-Za-z_0-9 ]+)\\.rb" FILE_NAME ${f})
+      string(REGEX REPLACE "/?([A-Za-z_0-9 ]+)\\.rb" "\\1" FILE_NAME ${FILE_NAME})
+
+
+          add_test(NAME "CLITest-${FILE_NAME}-${TEST_NAME}"
+            COMMAND "${CMAKE_COMMAND}" -E chdir "${CMAKE_CURRENT_BINARY_DIR}"
+            "$<TARGET_FILE:openstudio>" "${f}" "--name=test_${TEST_NAME}"
+          )
+
+        set_tests_properties("CLITest-${FILE_NAME}-${TEST_NAME}" PROPERTIES TIMEOUT 660 )
+    endforeach()
   endforeach()
-endforeach()
+endif()
diff --git a/src/cli/MeasureUpdateCommand.cpp b/src/cli/MeasureUpdateCommand.cpp
new file mode 100644
index 00000000000..8bb227a083f
--- /dev/null
+++ b/src/cli/MeasureUpdateCommand.cpp
@@ -0,0 +1,240 @@
+
+
+/***********************************************************************************************************************
+*  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 "MeasureUpdateCommand.hpp"
+#include "../utilities/core/Filesystem.hpp"
+#include "../utilities/bcl/BCLMeasure.hpp"
+#include "../scriptengine/ScriptEngine.hpp"
+
+#include <fmt/format.h>
+#include <boost/optional.hpp>
+#include <memory>
+#include <stdexcept>
+
+namespace openstudio {
+namespace cli {
+
+  void MeasureUpdateOptions::setupMeasureUpdateOptions(CLI::App* parentApp, ScriptEngineInstance& rubyEngine) {
+    /// Set up a subcommand and capture a shared_ptr to a struct that holds all its options.
+    /// The variables of the struct are bound to the CLI options.
+    /// We use a shared ptr so that the addresses of the variables remain for binding,
+    /// You could return the shared pointer if you wanted to access the values in main.
+
+    auto opt = std::make_shared<MeasureUpdateOptions>();
+
+    auto* measureCommand = parentApp->add_subcommand("measure", "Measure Commands");
+
+    // Positional
+    auto* directoryPathOpt = measureCommand->add_option("DIRECTORY", opt->directoryPath, "Directory containing the measure(s)")->option_text(" ");
+
+    // TODO: I'm trying to preserve historical behavior, but --update, --update_all, --start_server should all be subcommands instead since they are
+    // exclusive! At least start_server should, so that I can make directory required...
+    auto* updateOpt = measureCommand->add_flag("-u,--update", opt->update, "Update the measure.xml")->needs(directoryPathOpt);
+
+    auto* updateAllOpt = measureCommand->add_flag("-t,--update_all", opt->update_all, "Update all measures in a directory")
+                           ->needs(directoryPathOpt)
+                           ->excludes(updateOpt);
+
+    auto* computeArgsOpt =
+      measureCommand->add_option("-a,--compute_arguments", opt->compute_arguments_model, "Specify the FILE path to the workflow to run")
+        ->option_text("MODEL")
+        ->needs(directoryPathOpt)
+        ->excludes(updateOpt, updateAllOpt);
+
+    measureCommand
+      ->add_flag("-r,--run_tests", opt->run_tests, "Run all tests recursively found in a directory, additional arguments are passed to minitest")
+      ->needs(directoryPathOpt)
+      ->excludes(updateOpt, updateAllOpt, computeArgsOpt);
+
+    [[maybe_unused]] auto* startServerOpt = measureCommand->add_option("-s,--start_server", opt->server_port, "Start a measure manager server")
+                                              ->option_text("PORT")
+                                              ->excludes(directoryPathOpt);
+
+    measureCommand->callback([opt, &rubyEngine] { MeasureUpdateOptions::execute(*opt, rubyEngine); });
+  }
+
+  boost::optional<BCLMeasure> getAndUpdateMeasure(const openstudio::path& directoryPath) {
+
+    const auto& directoryPathStr = directoryPath.string();
+
+    // check if measure exists on disk
+    if (!openstudio::filesystem::is_directory(directoryPath)) {
+      fmt::print("Measure '{}' does not exist.\n", directoryPathStr);
+      return boost::none;
+    }
+    if (!openstudio::filesystem::is_regular_file(directoryPath / "measure.xml")) {
+      fmt::print("Measure directory '{}' exists but does not have a measure.xml.\n", directoryPathStr);
+      return boost::none;
+    }
+
+    // load from disk
+    fmt::print("Attempting to load measure '{}'\n", directoryPathStr);
+
+    auto measure_ = openstudio::BCLMeasure::load(directoryPath);
+
+    if (!measure_) {
+      fmt::print("Failed to load measure '{}'\n", directoryPathStr);
+      return boost::none;
+    }
+    fmt::print("Successfully loaded measure '{}'\n", directoryPathStr);
+
+    // see if there are updates, want to make sure to perform both checks so do outside of conditional
+    bool file_updates = measure_->checkForUpdatesFiles();  // checks if any files have been updated
+    bool xml_updates = measure_->checkForUpdatesXML();     // only checks if xml as loaded has been changed since last save
+
+    auto readmeInPath = directoryPath / "README.md.erb";
+    auto readmeOutPath = directoryPath / "README.md";
+
+    bool readme_out_of_date = openstudio::filesystem::is_regular_file(readmeInPath) && !openstudio::filesystem::is_regular_file(readmeOutPath);
+
+    // TODO: try catch like in measure_manager.rb?
+    bool missing_fields = measure_->missingRequiredFields();
+
+    if (file_updates || xml_updates || missing_fields || readme_out_of_date) {
+      fmt::print("Changes detected, updating '{}'\n", directoryPathStr);
+
+      // TODO: the readme.md generation from readme.md.erb requires ruby.
+      fmt::print("Warn: readme.md generation from ERB is not supported yet\n");
+
+      // Save the xml file with changes triggered by checkForUpdatesFiles() / checkForUpdatesXML() above
+      measure_->save();
+
+      // Also need to mimic OSMeasureInfoGetter::getInfo which is actually implemented in ruby itself...
+      // basically we need to compute the arguments of the measures, inspect outputs... so that requires doing something similar to what we do in the
+      // `run` method
+    }
+
+    return measure_;
+  }
+
+  void MeasureUpdateOptions::execute(MeasureUpdateOptions const& opt, ScriptEngineInstance& rubyEngine) {
+    opt.debug_print();
+
+    if (opt.server_port > 0) {
+      // throw std::runtime_error("measure manager server not implemented yet");
+      const auto measureManagerCmd = fmt::format(
+        R"ruby(
+require 'measure_manager_server'
+port = {}
+
+server = WEBrick::HTTPServer.new(:Port => port)
+
+server.mount "/", MeasureManagerServlet
+
+trap("INT") {{
+    server.shutdown
+}}
+
+server.start)ruby",
+        opt.server_port);
+
+      rubyEngine->exec(measureManagerCmd);
+
+    } else if (opt.update) {
+      getAndUpdateMeasure(opt.directoryPath);
+
+    } else if (opt.update_all) {
+      std::vector<openstudio::path> subDirPaths;
+      for (auto const& dir_entry : boost::filesystem::directory_iterator{opt.directoryPath}) {
+        const auto& subDirPath = dir_entry.path();
+
+        if (openstudio::filesystem::is_directory(subDirPath) && openstudio::filesystem::is_regular_file(subDirPath / "measure.xml")) {
+          subDirPaths.emplace_back(subDirPath);
+        }
+      }
+      fmt::print("Found {} measure directories to update\n", subDirPaths.size());
+      for (const auto& subDirPath : subDirPaths) {
+        getAndUpdateMeasure(subDirPath);
+      }
+
+    } else if (!opt.compute_arguments_model.empty()) {
+      // TODO: Same comment as above: we need to call the measure methods same as what we do in OSWorkflow::run
+      throw std::runtime_error("compute_arguments_model not implemented yet");
+    } else if (opt.run_tests) {
+      auto measure_ = getAndUpdateMeasure(opt.directoryPath);
+      if (measure_->measureLanguage() == MeasureLanguage::Ruby) {
+        // TODO: need to capture arguments and pass as ARGV
+
+        throw std::runtime_error("compute_arguments_model not implemented yet");
+
+        // TODO: THIS IS IS INTERESTING! WE CANNOT LOAD THE EMBEDDED GEMS! I suppose we need to do something like embedded_help.rb
+        rubyEngine->exec("OpenStudio::init_rest_of_openstudio()");
+
+        auto runTestCmd = fmt::format(
+          R"ruby(
+# load openstudio_measure_tester gem
+#begin
+  require 'minitest'
+  require 'minitest/reporters'
+
+  # Minitest Reports use a plugin that is normally found by Minitest::load_plugins using Gem.find
+  # until Gem.find is overloaded to find embedded gems, we will manually load the plugin here
+  require 'minitest/minitest_reporter_plugin'
+  Minitest.extensions << 'minitest_reporter'
+
+  require 'openstudio_measure_tester'
+#rescue LoadError
+  #puts "Cannot load 'openstudio_measure_tester'"
+  #return 1
+#end
+
+runner = OpenStudioMeasureTester::Runner.new('{}')
+result = runner.run_all(Dir.pwd)    # <--------------- TODO: this is problematic?
+
+if result != 0
+  $logger.error("Measure tester returned errors")
+  return 1
+end
+)ruby",
+          opt.directoryPath.generic_string());
+        rubyEngine->exec(runTestCmd);
+
+      } else if (measure_->measureLanguage() == MeasureLanguage::Python) {
+      }
+
+      // TODO:
+      throw std::runtime_error("run_tests not implemented yet");
+    }
+  }
+
+  void MeasureUpdateOptions::debug_print() const {
+    fmt::print("\nMeasureUpdateOptions:\n");
+    fmt::print("directoryPath={}\n", this->directoryPath.string());
+    fmt::print("update={}\n", this->update);
+    fmt::print("update_all={}\n", this->update_all);
+    fmt::print("compute_arguments_model={}\n", this->compute_arguments_model.string());
+    fmt::print("run_tests={}\n", this->run_tests);
+    fmt::print("server_port={}\n", this->server_port);
+    fmt::print("\n\n");
+  }
+
+}  // namespace cli
+}  // namespace openstudio
diff --git a/src/cli/MeasureUpdateCommand.hpp b/src/cli/MeasureUpdateCommand.hpp
new file mode 100644
index 00000000000..cfc4dd9c295
--- /dev/null
+++ b/src/cli/MeasureUpdateCommand.hpp
@@ -0,0 +1,66 @@
+/***********************************************************************************************************************
+*  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 CLI_MEASUREUPDATECOMMAND_HPP
+#define CLI_MEASUREUPDATECOMMAND_HPP
+
+#include <CLI/App.hpp>
+#include "../utilities/core/Filesystem.hpp"
+
+namespace openstudio {
+
+class ScriptEngineInstance;
+
+namespace cli {
+
+  struct MeasureUpdateOptions
+  {
+   public:
+    static void setupMeasureUpdateOptions(CLI::App* parentApp, ScriptEngineInstance& rubyEngine);
+
+    static void execute(MeasureUpdateOptions const& opt, ScriptEngineInstance& rubyEngine);
+
+    void debug_print() const;
+
+    bool update = false;
+    bool update_all = false;
+
+    openstudio::path compute_arguments_model;
+    openstudio::path directoryPath;
+
+    bool run_tests = false;
+
+    unsigned server_port = 0;
+  };
+
+}  // namespace cli
+}  // namespace openstudio
+
+#endif  // CLI_MEASUREUPDATECOMMAND_HPP
+
diff --git a/src/cli/RubyCLI.cpp b/src/cli/RubyCLI.cpp
new file mode 100644
index 00000000000..5c99dcf97ba
--- /dev/null
+++ b/src/cli/RubyCLI.cpp
@@ -0,0 +1,34 @@
+#include "./RubyCLI.hpp"
+#include "../scriptengine/ScriptEngine.hpp"
+#include <iostream>
+
+namespace openstudio {
+
+int rubyCLI(ScriptEngineInstance& rubyEngine) {
+  try {
+    rubyEngine->exec(R"(
+       begin
+         require 'openstudio_cli'
+       rescue Exception => e
+         puts
+         puts "Error: #{e.message}"
+         puts "Backtrace:\n\t" + e.backtrace.join("\n\t")
+         raise
+       end
+     )");
+  } catch (const std::exception& e) {
+    rubyEngine->exec(R"(STDOUT.flush)");
+    std::cout << "Exception: " << e.what() << std::endl;  // endl will flush
+    return 1;
+  } catch (...) {
+    rubyEngine->exec(R"(STDOUT.flush)");
+    std::cout << "Unknown Exception" << std::endl;  // endl will flush
+    return 1;
+  }
+
+  rubyEngine->exec(R"(STDOUT.flush)");
+  std::cout << std::flush;
+  return 0;
+}
+
+}  // namespace openstudio
diff --git a/src/cli/RubyCLI.hpp b/src/cli/RubyCLI.hpp
new file mode 100644
index 00000000000..0acbb627e6d
--- /dev/null
+++ b/src/cli/RubyCLI.hpp
@@ -0,0 +1,14 @@
+#ifndef RUBY_CLI_included
+#define RUBY_CLI_included
+
+#include <memory>
+
+namespace openstudio {
+
+class ScriptEngineInstance;
+
+int rubyCLI(ScriptEngineInstance& rubyEngine);
+
+}  // namespace openstudio
+
+#endif  // RUBY_CLI_included
diff --git a/src/cli/RunCommand.cpp b/src/cli/RunCommand.cpp
new file mode 100644
index 00000000000..ee698e0f4ee
--- /dev/null
+++ b/src/cli/RunCommand.cpp
@@ -0,0 +1,141 @@
+/***********************************************************************************************************************
+*  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 "RunCommand.hpp"
+#include "../workflow/RunOptions.hpp"
+#include "../workflow/OSWorkflow.hpp"
+#include "../scriptengine/ScriptEngine.hpp"
+
+#include <fmt/format.h>
+
+#include <memory>
+
+namespace openstudio {
+namespace cli {
+
+  void setupRunFtOptions(CLI::App* app, FtOptions& ftOptions);
+
+  void setupRunOptions(CLI::App* parentApp, ScriptEngineInstance& ruby, ScriptEngineInstance& python) {
+    /// Set up a subcommand and capture a shared_ptr to a struct that holds all its options.
+    /// The variables of the struct are bound to the CLI options.
+    /// We use a shared ptr so that the addresses of the variables remain for binding,
+    /// You could return the shared pointer if you wanted to access the values in main.
+
+    // TODO: modify utilities/filetypes/RunOptions.hpp and use that instead
+    auto opt = std::make_shared<WorkflowRunOptions>();
+
+    auto* const app = parentApp->add_subcommand("run", "Executes an OpenStudio Workflow file");
+
+    app->add_option("-w,--workflow", opt->osw_path, "Specify the FILE path to the workflow to run")->option_text("FILE");
+
+    app->add_flag(
+      "-m,--measures_only",
+      [opt](std::int64_t val) {
+        fmt::print("val={}\n", val);
+        opt->no_simulation = (val == 1);
+      },
+      "Only run the OpenStudio and EnergyPlus measures");
+
+    app->add_flag("-p,--postprocess_only", opt->post_process, "Only run the reporting measures");
+
+    app->add_flag("--export-epJSON", opt->ep_json, "export epJSON file format. The default is IDF");
+
+    app->add_option("-s,--socket", opt->socket_port, "Pipe status messages to a socket on localhost PORT")->option_text("PORT");
+
+    auto* stdout_opt = app->add_flag("--show-stdout", opt->show_stdout, "export epJSON file format. The default is IDF")->group("Stdout Options");
+    app->add_flag("--add-timings", opt->add_timings, "Print the start, end and elapsed times of each state of the simulation.")
+      ->needs(stdout_opt)
+      ->group("Stdout Options");
+    app
+      ->add_flag("--style-stdout", opt->style_stdout,
+                 "Style the stdout output to more clearly show the start and end of each state of the simulation.")
+      ->needs(stdout_opt)
+      ->group("Stdout Options");
+
+    app->add_flag("--debug", opt->debug, "Includes additional outputs for debugging failing workflows and does not clean up the run directory");
+
+    // FT options
+    setupRunFtOptions(app, opt->ft_options);
+
+    app->callback([opt, &ruby, &python] {
+      openstudio::OSWorkflow workflow(*opt, ruby, python);
+      workflow.run();
+    });
+  }
+
+  void setupRunFtOptions(CLI::App* app, FtOptions& ftOptions) {
+    app->add_flag("--runcontrolspecialdays,!--no-runcontrolspecialdays", ftOptions.runcontrolspecialdays, "Include RunControlSpecialDays (Holidays)")
+      ->group(FtOptions::group_name);
+
+    app->add_flag("--set-ip-tabular-output", ftOptions.ip_tabular_output, "Request IP units from E+ Tabular (HTML) Report [Default: False]")
+      ->group(FtOptions::group_name);
+
+    app
+      ->add_flag(
+        "--lifecyclecosts,!--no-lifecyclecosts",
+        [&ftOptions](std::int64_t val) {
+          fmt::print("val={}\n", val);
+          ftOptions.no_lifecyclecosts = (val != 1);
+        },
+        "Include LifeCycleCosts [Default: True]")
+      ->group(FtOptions::group_name);
+
+    app
+      ->add_flag(
+        "--sqlite-output,!--no-sqlite-output",
+        [&ftOptions](std::int64_t val) {
+          fmt::print("val={}\n", val);
+          ftOptions.no_sqlite_output = (val != 1);
+        },
+        "Request Output:SQLite from E+ [Default: True]")
+      ->group(FtOptions::group_name);
+
+    app
+      ->add_flag(
+        "--html-output,!--no-html-output",
+        [&ftOptions](std::int64_t val) {
+          fmt::print("val={}\n", val);
+          ftOptions.no_html_output = (val != 1);
+        },
+        "Request Output:Table:SummaryReports from E+ [Default: True]")
+      ->group(FtOptions::group_name);
+
+    app
+      ->add_flag(
+        "--space-translation,!--no-space-translation",
+        [&ftOptions](std::int64_t val) {
+          fmt::print("val={}\n", val);
+          ftOptions.no_space_translation = (val != 1);
+        },
+        "Add individual E+ Space [Default: True]")
+      ->group(FtOptions::group_name);
+  }
+
+}  // namespace cli
+}  // namespace openstudio
diff --git a/ruby/init_openstudio.hpp b/src/cli/RunCommand.hpp
similarity index 85%
rename from ruby/init_openstudio.hpp
rename to src/cli/RunCommand.hpp
index ba49458a84c..bf133a7403b 100644
--- a/ruby/init_openstudio.hpp
+++ b/src/cli/RunCommand.hpp
@@ -27,13 +27,21 @@
 *  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 ***********************************************************************************************************************/
 
-#include <string>
+#ifndef CLI_RUNCOMMAND_HPP
+#define CLI_RUNCOMMAND_HPP
 
+#include <CLI/App.hpp>
 
-void init_openstudio_internal();
+namespace openstudio {
 
-void init_openstudio_internal_basic();
-void init_openstudio_internal_extended();
+class ScriptEngineInstance;
 
-void evalString(const std::string &t_str);
+namespace cli {
 
+  void setupRunOptions(CLI::App* parentApp, ScriptEngineInstance& ruby, ScriptEngineInstance& python);
+  // void setupRunFtOptions(CLI::App* app, FtOptions& ftOptions);
+
+}  // namespace cli
+}  // namespace openstudio
+
+#endif  // CLI_RUNCOMMAND_HPP
diff --git a/src/cli/UpdateCommand.cpp b/src/cli/UpdateCommand.cpp
new file mode 100644
index 00000000000..ba574bb3780
--- /dev/null
+++ b/src/cli/UpdateCommand.cpp
@@ -0,0 +1,145 @@
+/***********************************************************************************************************************
+*  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 "UpdateCommand.hpp"
+#include "../utilities/core/Filesystem.hpp"
+#include "../utilities/core/FilesystemHelpers.hpp"
+#include "../utilities/core/Logger.hpp"
+#include "../osversion/VersionTranslator.hpp"
+#include "../model/Model.hpp"
+#include "../scriptengine/ScriptEngine.hpp"
+
+#include <fmt/format.h>
+
+#include <memory>
+#include <stdexcept>
+
+namespace openstudio {
+namespace cli {
+
+  void setupMeasureCommand(CLI::App* parentApp) {
+    [[maybe_unused]] auto* measureCommand = parentApp->add_subcommand("measure", "Updates measures and compute arguments");
+  }
+
+  void setupUpdateCommand(CLI::App* parentApp) {
+
+    bool keep = false;
+    [[maybe_unused]] auto* updateCommand = parentApp->add_subcommand("update", "Updates OpenStudio Models to the current version");
+    updateCommand->add_flag("--keep", keep, "Keep original files");
+
+    openstudio::filesystem::path p;
+    updateCommand->add_option("path", p, "Path to OSM or directory containing osms")->required(true);
+
+    updateCommand->callback([&keep, &p] {
+      bool result = runModelUpdateCommand(p, keep);
+      if (!result) {
+        throw std::runtime_error("Failed to update some models");
+      }
+    });
+  }
+
+  bool runModelUpdateCommand(const openstudio::path& p, bool keep) {
+    std::vector<openstudio::path> osmPaths;
+
+    openstudio::osversion::VersionTranslator vt;
+
+    if (openstudio::filesystem::is_directory(p)) {
+      for (auto const& dir_entry : boost::filesystem::directory_iterator{p}) {
+        const auto& thePath = dir_entry.path();
+        if (openstudio::filesystem::is_regular_file(thePath) && thePath.extension() == ".osm") {
+          osmPaths.emplace_back(thePath);
+        }
+      }
+    } else {
+      osmPaths.push_back(p);
+    }
+
+    bool result = true;
+    for (const auto& relPath : osmPaths) {
+      fmt::print("'{}'\n", relPath.string());
+      auto osmPath = openstudio::filesystem::system_complete(relPath);
+      if (keep) {
+        openstudio::path backupPath = osmPath;
+        backupPath.replace_extension(openstudio::toPath(".osm.orig"));
+        openstudio::filesystem::copy_file(osmPath, backupPath, openstudio::filesystem::copy_option::overwrite_if_exists);
+      }
+      if (auto model_ = vt.loadModel(osmPath)) {
+        fmt::print("Updated '{}'\n", osmPath.string());
+        model_->save(osmPath, true);
+      } else {
+        fmt::print("Could not read model at '{}'\n", osmPath.string());
+        result = false;
+      }
+    }
+    return result;
+  }
+
+  void executeRubyScriptCommand(openstudio::path rubyScriptPath, ScriptEngineInstance& rubyEngine, const std::vector<std::string>& arguments) {
+    rubyScriptPath = openstudio::filesystem::system_complete(rubyScriptPath);
+    LOG_FREE(Debug, "executeRubyScriptCommand", "Path for the file to run: " << rubyScriptPath);
+    if (!openstudio::filesystem::is_regular_file(rubyScriptPath)) {
+      throw std::runtime_error(fmt::format("Unable to find the file '{}' on the filesystem", rubyScriptPath.string()));
+    }
+    rubyEngine->exec("OpenStudio::init_rest_of_openstudio()");
+
+    std::string cmd = "ARGV.clear\n";
+    for (const auto& arg : arguments) {
+      cmd += fmt::format("ARGV << \"{}\"\n", arg);
+    }
+    cmd += fmt::format("require '{}'", rubyScriptPath.generic_string());
+    rubyEngine->exec(cmd);
+  }
+
+  void executePythonScriptCommand(openstudio::path pythonScriptPath, ScriptEngineInstance& pythonEngine, const std::vector<std::string>& arguments) {
+    pythonScriptPath = openstudio::filesystem::system_complete(pythonScriptPath);
+    LOG_FREE(Debug, "executePythonScriptCommand", "Path for the file to run: " << pythonScriptPath);
+    if (!openstudio::filesystem::is_regular_file(pythonScriptPath)) {
+      throw std::runtime_error(fmt::format("Unable to find the file '{}' on the filesystem", pythonScriptPath.string()));
+    }
+    // There's probably better to be done, like instantiating the pythonEngine with the argc/argv then calling PyRun_SimpleFile but whatever
+    std::string cmd = fmt::format(R"python(import sys
+sys.argv.clear()
+sys.argv.append("{}")
+)python",
+                                  pythonScriptPath.filename().string());
+    for (const auto& arg : arguments) {
+      cmd += fmt::format("sys.argv.append(\"{}\")\n", arg);
+    }
+    cmd += fmt::format(R"python(
+import importlib
+spec = importlib.util.spec_from_file_location('__main__', r'{}')
+module = importlib.util.module_from_spec(spec)
+spec.loader.exec_module(module)
+)python",
+                       pythonScriptPath.generic_string());
+    fmt::print("{}\n", cmd);
+    pythonEngine->exec(cmd);
+  }
+}  // namespace cli
+}  // namespace openstudio
diff --git a/src/cli/UpdateCommand.hpp b/src/cli/UpdateCommand.hpp
new file mode 100644
index 00000000000..5f31cfbf0cc
--- /dev/null
+++ b/src/cli/UpdateCommand.hpp
@@ -0,0 +1,51 @@
+/***********************************************************************************************************************
+*  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 CLI_UPDATECOMMAND_HPP
+#define CLI_UPDATECOMMAND_HPP
+
+#include <CLI/App.hpp>
+#include "../utilities/core/Filesystem.hpp"
+
+namespace openstudio {
+
+class ScriptEngineInstance;
+
+namespace cli {
+
+  void setupUpdateCommand(CLI::App* parentApp);
+  bool runModelUpdateCommand(const openstudio::path& p, bool keep);
+
+  void executeRubyScriptCommand(openstudio::path rubyScriptPath, ScriptEngineInstance& rubyEngine, const std::vector<std::string>& arguments);
+  void executePythonScriptCommand(openstudio::path pythonScriptPath, ScriptEngineInstance& pythonEngine, const std::vector<std::string>& arguments);
+
+}  // namespace cli
+}  // namespace openstudio
+
+#endif  // CLI_UPDATECOMMAND_HPP
diff --git a/src/cli/main.cpp b/src/cli/main.cpp
index c17243fbd2f..d3086644068 100644
--- a/src/cli/main.cpp
+++ b/src/cli/main.cpp
@@ -1,611 +1,170 @@
-/***********************************************************************************************************************
-*  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 "RubyCLI.hpp"
+#include "UpdateCommand.hpp"
+#include "../scriptengine/ScriptEngine.hpp"
+#include "../workflow/OSWorkflow.hpp"
+#include "../utilities/core/Logger.hpp"
+#include "../utilities/bcl/BCLMeasure.hpp"
 
-#include "RubyInterpreter.hpp"
-#include "GC_Value.hpp"
-#include "InitMacros.hxx"
-#include "../../ruby/init_openstudio.hpp"
-#include <embedded_files.hxx>
+#include "RunCommand.hpp"
+#include "MeasureUpdateCommand.hpp"
 
-#include <iostream>
-
-#ifndef _MSC_VER
-#  include <dlfcn.h>
-#  include <dirent.h>
-#else
-#  include <windows.h>
-#  pragma warning(disable : 4930)
-#  pragma warning(disable : 4101)
-#endif
-
-extern "C"
-{
-  void Init_EmbeddedScripting(void);
-  INIT_DECLARATIONS;
-
-  void Init_encdb();
-
-  //void Init_ascii(); // this is not included in libenc
-  void Init_big5();
-  void Init_cesu_8();
-  void Init_cp949();
-  void Init_emacs_mule();
-  void Init_euc_jp();
-  void Init_euc_kr();
-  void Init_euc_tw();
-  void Init_gb18030();
-  void Init_gb2312();
-  void Init_gbk();
-  void Init_iso_8859_1();
-  void Init_iso_8859_10();
-  void Init_iso_8859_11();
-  void Init_iso_8859_13();
-  void Init_iso_8859_14();
-  void Init_iso_8859_15();
-  void Init_iso_8859_16();
-  void Init_iso_8859_2();
-  void Init_iso_8859_3();
-  void Init_iso_8859_4();
-  void Init_iso_8859_5();
-  void Init_iso_8859_6();
-  void Init_iso_8859_7();
-  void Init_iso_8859_8();
-  void Init_iso_8859_9();
-  void Init_koi8_r();
-  void Init_koi8_u();
-  void Init_shift_jis();
-  //void Init_unicode(); // this is not included in libenc
-  //void Init_us_ascii(); // this is not included in libenc
-  void Init_utf_16be();
-  void Init_utf_16le();
-  void Init_utf_32be();
-  void Init_utf_32le();
-  //void Init_utf_8(); // this is not included in libenc
-  void Init_windows_1250();
-  void Init_windows_1251();
-  void Init_windows_1252();
-  void Init_windows_1253();
-  void Init_windows_1254();
-  void Init_windows_1257();
-  void Init_windows_31j();
-
-  void Init_transdb();
+#include <OpenStudio.hxx>
 
-  void Init_trans_big5();
-  void Init_trans_cesu_8();
-  void Init_trans_chinese();
-  void Init_trans_ebcdic();
-  void Init_trans_emoji();
-  void Init_trans_emoji_iso2022_kddi();
-  void Init_trans_emoji_sjis_docomo();
-  void Init_trans_emoji_sjis_kddi();
-  void Init_trans_emoji_sjis_softbank();
-  void Init_trans_escape();
-  void Init_trans_gb18030();
-  void Init_trans_gbk();
-  void Init_trans_iso2022();
-  void Init_trans_japanese();
-  void Init_trans_japanese_euc();
-  void Init_trans_japanese_sjis();
-  void Init_trans_korean();
-  void Init_trans_single_byte();
-  void Init_trans_utf8_mac();
-  void Init_trans_utf_16_32();
-
-  void Init_bigdecimal();
-  void Init_bigdecimal(void);
-  void Init_continuation(void);
-  void Init_coverage(void);
-  void Init_cparse(void);
-  void Init_date_core(void);
-  void Init_digest(void);
-  void Init_escape(void);
-  void Init_etc(void);
-  void Init_fcntl(void);
-  void Init_fiber(void);
-  void Init_fiddle(void);
-  void Init_generator(void);
-  void Init_md5(void);
-  void Init_monitor(void);
-  void Init_nkf(void);
-  void Init_nonblock(void);
-  void Init_objspace(void);
-  void Init_parser(void);
-  void Init_pathname(void);
-  void Init_psych(void);
-  void Init_ripper(void);
-  void Init_rmd160(void);
-  void Init_sdbm(void);
-  void Init_sha1(void);
-  void Init_sha2(void);
-  void Init_sizeof(void);
-  void Init_socket(void);
-  void Init_stringio(void);
-  void Init_strscan(void);
-  void Init_wait(void);
-  void Init_zlib(void);
-
-  void Init_openssl(void);
-  void Init_ruby_description(void);
-
-#ifndef _WIN32
-  void Init_console(void);
-  void Init_dbm(void);
-  void Init_gdbm(void);
-  void Init_pty(void);
-  void Init_readline(void);
-  void Init_syslog(void);
-#endif
-
-  VALUE init_rest_of_openstudio(...) {
-    init_openstudio_internal_extended();
-    return Qtrue;
-  }
-}
+#include <iostream>
 
-std::vector<std::string> paths;
-static RubyInterpreter rubyInterpreter(paths);  //(paths);
+#include <CLI/CLI.hpp>
 
 int main(int argc, char* argv[]) {
-  ruby_sysinit(&argc, &argv);
-  {
-    RUBY_INIT_STACK;
-    ruby_init();
-
-    swig::GC_VALUE::hash_id = rb_intern("hash");
-    swig::GC_VALUE::lt_id = rb_intern("<");
-    swig::GC_VALUE::gt_id = rb_intern(">");
-    swig::GC_VALUE::eq_id = rb_intern("==");
-    swig::GC_VALUE::le_id = rb_intern("<=");
-    swig::GC_VALUE::ge_id = rb_intern(">=");
-
-    swig::GC_VALUE::pos_id = rb_intern("+@");
-    swig::GC_VALUE::neg_id = rb_intern("-@");
-    swig::GC_VALUE::inv_id = rb_intern("~");
-
-    swig::GC_VALUE::add_id = rb_intern("+");
-    swig::GC_VALUE::sub_id = rb_intern("-");
-    swig::GC_VALUE::mul_id = rb_intern("*");
-    swig::GC_VALUE::div_id = rb_intern("/");
-    swig::GC_VALUE::mod_id = rb_intern("%");
-
-    swig::GC_VALUE::and_id = rb_intern("&");
-    swig::GC_VALUE::or_id = rb_intern("|");
-    swig::GC_VALUE::xor_id = rb_intern("^");
-
-    swig::GC_VALUE::lshift_id = rb_intern("<<");
-    swig::GC_VALUE::rshift_id = rb_intern(">>");
-
-    INIT_CALLS;
-
-    // in case any further init methods try to require files, init this first
-    Init_EmbeddedScripting();
-
-    // Need embedded_help for requiring files out of the embedded system
-    auto embedded_extensions_string = embedded_files::getFileAsString(":/embedded_help.rb");
-
-    try {
-      rubyInterpreter.evalString(embedded_extensions_string);
-    } catch (const std::exception& e) {
-      rubyInterpreter.evalString(R"(STDOUT.flush)");
-      std::cout << "Exception in embedded_help: " << e.what() << std::endl;  // endl will flush
-      return ruby_cleanup(1);
-    } catch (...) {
-      rubyInterpreter.evalString(R"(STDOUT.flush)");
-      std::cout << "Unknown Exception in embedded_help" << std::endl;  // endl will flush
-      return ruby_cleanup(1);
+  int result = 0;
+
+  // ScriptEngineInstance will delay load the engines
+  openstudio::ScriptEngineInstance rubyEngine("rubyengine", argc - 1, argv + 1);
+  openstudio::ScriptEngineInstance pythonEngine("pythonengine", argc - 1, argv + 1);
+
+  if ((argc > 1) && (std::string_view(argv[1]) == "labs")) {
+    CLI::App app{"openstudio"};
+
+    auto* const experimentalApp = app.add_subcommand("labs");
+
+    experimentalApp->add_flag_function(
+      "--verbose",
+      [](auto count) {
+        if (count == 1) {
+          fmt::print("Setting log Level to Debug\n");
+          openstudio::Logger::instance().standardOutLogger().setLogLevel(LogLevel::Debug);
+        } else if (count == 2) {
+          fmt::print("Setting log Level to Trace\n");
+          openstudio::Logger::instance().standardOutLogger().setLogLevel(LogLevel::Trace);
+        }
+      },
+      "Print the full log to STDOUT");
+
+    std::vector<std::string> includeDirs;
+    experimentalApp
+      ->add_option("-I,--include", includeDirs, "Add additional directory to add to front of Ruby $LOAD_PATH (may be used more than once)")
+      ->option_text("DIR");
+
+    std::vector<std::string> executeRubyCmds;
+    CLI::Option* execRubyOption = experimentalApp
+                                    ->add_option("-e,--execute", executeRubyCmds,
+                                                 "Execute one line of Ruby script (may be used more than once). Returns after executing commands.")
+                                    ->option_text("CMD");
+
+    std::vector<std::string> executePythonCmds;
+    CLI::Option* execPythonOption =
+      experimentalApp
+        ->add_option("-c,--pyexecute", executePythonCmds,
+                     "Execute one line of Python script (may be used more than once). Returns after executing commands.")
+        ->option_text("CMD");
+
+    std::vector<std::string> gemPathDirs;
+    app
+      .add_option("--gem_path", gemPathDirs, "Add additional directory to add to front of GEM_PATH environment variable (may be used more than once)")
+      ->option_text("DIR");
+
+    std::string gemHomeDir;
+    experimentalApp->add_option("--gem_home", gemHomeDir, "Set GEM_HOME environment variable")->option_text("DIR");
+
+    std::string gemFile;
+    experimentalApp->add_option("--bundle", gemFile, "Use bundler for GEMFILE'")->option_text("GEMFILE");
+
+    [[maybe_unused]] auto* energyplus_versionCommand =
+      experimentalApp->add_subcommand("energyplus_version", "Returns the EnergyPlus version used by the CLI")->callback([]() {
+        fmt::print("{}+{}\n", energyPlusVersion(), energyPlusBuildSHA());
+      });
+
+    {
+      auto* execute_ruby_scriptCommand = experimentalApp->add_subcommand("execute_ruby_script", "Executes a ruby file");
+      openstudio::filesystem::path rubyScriptPath;
+      execute_ruby_scriptCommand->add_option("path", rubyScriptPath, "Path to ruby file")->required(true);
+      std::vector<std::string> executeRubyScriptCommandArgs;
+      execute_ruby_scriptCommand->add_option("arguments", executeRubyScriptCommandArgs, "Arguments to pass to the ruby file")
+        ->required(false)
+        ->option_text("args");
+      execute_ruby_scriptCommand->callback([&rubyScriptPath, &rubyEngine, &executeRubyScriptCommandArgs] {
+        openstudio::cli::executeRubyScriptCommand(rubyScriptPath, rubyEngine, executeRubyScriptCommandArgs);
+      });
     }
 
-    //// encodings
-    /// Get the symbols from: `DUMPBIN /ARCHIVEMEMBERS  C:\Users\julien\.conan\data\openstudio_ruby\2.7.2\nrel\testing\package\2fa339f0e9f8e459bd56b19a37be69734f2745f4\lib\enc\libenc.lib`
-    Init_encdb();
-    rb_provide("enc/encdb.so");
-    //Init_ascii();
-    //rb_provide("enc/ascii.so");
-    Init_big5();
-    rb_provide("enc/big5.so");
-    Init_cesu_8();
-    rb_provide("enc/cesu_8.so");
-    Init_cp949();
-    rb_provide("enc/cp949.so");
-    Init_emacs_mule();
-    rb_provide("enc/emacs_mule.so");
-    Init_euc_jp();
-    rb_provide("enc/euc_jp.so");
-    Init_euc_kr();
-    rb_provide("enc/euc_kr.so");
-    Init_euc_tw();
-    rb_provide("enc/euc_tw.so");
-    Init_gb18030();
-    rb_provide("enc/gb18030.so");
-    Init_gb2312();
-    rb_provide("enc/gb2312.so");
-    Init_gbk();
-    rb_provide("enc/gbk.so");
-    Init_iso_8859_1();
-    rb_provide("enc/iso_8859_1.so");
-    Init_iso_8859_10();
-    rb_provide("enc/iso_8859_10.so");
-    Init_iso_8859_11();
-    rb_provide("enc/iso_8859_11.so");
-    Init_iso_8859_13();
-    rb_provide("enc/iso_8859_13.so");
-    Init_iso_8859_14();
-    rb_provide("enc/iso_8859_14.so");
-    Init_iso_8859_15();
-    rb_provide("enc/iso_8859_15.so");
-    Init_iso_8859_16();
-    rb_provide("enc/iso_8859_16.so");
-    Init_iso_8859_2();
-    rb_provide("enc/iso_8859_2.so");
-    Init_iso_8859_3();
-    rb_provide("enc/iso_8859_3.so");
-    Init_iso_8859_4();
-    rb_provide("enc/iso_8859_4.so");
-    Init_iso_8859_5();
-    rb_provide("enc/iso_8859_5.so");
-    Init_iso_8859_6();
-    rb_provide("enc/iso_8859_6.so");
-    Init_iso_8859_7();
-    rb_provide("enc/iso_8859_7.so");
-    Init_iso_8859_8();
-    rb_provide("enc/iso_8859_8.so");
-    Init_iso_8859_9();
-    rb_provide("enc/iso_8859_9.so");
-    Init_koi8_r();
-    rb_provide("enc/koi8_r.so");
-    Init_koi8_u();
-    rb_provide("enc/koi8_u.so");
-    Init_shift_jis();
-    rb_provide("enc/shift_jis.so");
-    //Init_unicode();
-    //rb_provide("enc/unicode.so");
-    //Init_us_ascii();
-    //rb_provide("enc/us_ascii.so");
-    Init_utf_16be();
-    rb_provide("enc/utf_16be.so");
-    Init_utf_16le();
-    rb_provide("enc/utf_16le.so");
-    Init_utf_32be();
-    rb_provide("enc/utf_32be.so");
-    Init_utf_32le();
-    rb_provide("enc/utf_32le.so");
-    //Init_utf_8();
-    //rb_provide("enc/utf_8.so");
-    Init_windows_1250();
-    rb_provide("enc/windows_1250.so");
-    Init_windows_1251();
-    rb_provide("enc/windows_1251.so");
-    Init_windows_1252();
-    rb_provide("enc/windows_1252.so");
-    Init_windows_1253();
-    rb_provide("enc/windows_1253.so");
-    Init_windows_1254();
-    rb_provide("enc/windows_1254.so");
-    Init_windows_1257();
-    rb_provide("enc/windows_1257.so");
-    Init_windows_31j();
-    rb_provide("enc/windows_31j.so");
-
-    /// Get the symbols from: `DUMPBIN /ARCHIVEMEMBERS  C:\Users\julien\.conan\data\openstudio_ruby\2.7.2\nrel\testing\package\2fa339f0e9f8e459bd56b19a37be69734f2745f4\lib\enc\libtrans.lib`
-    Init_transdb();
-    rb_provide("enc/trans/transdb.so");
-
-    //Init_trans_big5();
-    //rb_provide("enc/trans/big5.so");
-    Init_trans_big5();
-    rb_provide("enc/trans/big5.so");
-
-    Init_trans_cesu_8();
-    rb_provide("enc/trans/cesu_8.so");
-
-    Init_trans_chinese();
-    rb_provide("enc/trans/chinese.so");
-
-    Init_trans_ebcdic();
-    rb_provide("enc/trans/ebcdic.so");
-
-    Init_trans_emoji();
-    rb_provide("enc/trans/emoji.so");
-
-    Init_trans_emoji_iso2022_kddi();
-    rb_provide("enc/trans/emoji_iso2022_kddi.so");
-
-    Init_trans_emoji_sjis_docomo();
-    rb_provide("enc/trans/emoji_sjis_docomo.so");
-
-    Init_trans_emoji_sjis_kddi();
-    rb_provide("enc/trans/emoji_sjis_kddi.so");
-
-    Init_trans_emoji_sjis_softbank();
-    rb_provide("enc/trans/emoji_sjis_softbank.so");
-
-    Init_trans_escape();
-    rb_provide("enc/trans/escape.so");
-
-    Init_trans_gb18030();
-    rb_provide("enc/trans/gb18030.so");
-
-    Init_trans_gbk();
-    rb_provide("enc/trans/gbk.so");
-
-    Init_trans_iso2022();
-    rb_provide("enc/trans/iso2022.so");
-
-    Init_trans_japanese();
-    rb_provide("enc/trans/japanese.so");
-
-    Init_trans_japanese_euc();
-    rb_provide("enc/trans/japanese_euc.so");
-
-    Init_trans_japanese_sjis();
-    rb_provide("enc/trans/japanese_sjis.so");
-
-    Init_trans_korean();
-    rb_provide("enc/trans/korean.so");
-
-    Init_trans_single_byte();
-    rb_provide("enc/trans/single_byte.so");
-
-    Init_trans_utf8_mac();
-    rb_provide("enc/trans/utf8_mac.so");
-
-    Init_trans_utf_16_32();
-    rb_provide("enc/trans/utf_16_32.so");
-
-    Init_bigdecimal();
-    rb_provide("bigdecimal");
-    rb_provide("bigdecimal.so");
-
-    Init_continuation();
-    rb_provide("continuation");
-    rb_provide("continuation.so");
-
-    Init_coverage();
-    rb_provide("coverage");
-    rb_provide("coverage.so");
-
-    Init_cparse();
-    rb_provide("cparse");
-    rb_provide("racc/cparse");
-    rb_provide("cparse.so");
-    rb_provide("racc/cparse.so");
-
-    Init_date_core();
-    rb_provide("date_core");
-    rb_provide("date_core.so");
-
-    Init_digest();
-    rb_provide("digest");
-    rb_provide("digest.so");
-
-    Init_escape();
-    rb_provide("escape");
-    rb_provide("escape.so");
-
-    Init_etc();
-    rb_provide("etc");
-    rb_provide("etc.so");
-
-    Init_fcntl();
-    rb_provide("fcntl");
-    rb_provide("fcntl.so");
-
-    Init_fiber();
-    rb_provide("fiber");
-    rb_provide("fiber.so");
-
-    Init_fiddle();
-    rb_provide("fiddle");
-    rb_provide("fiddle.so");
-
-    Init_generator();
-    rb_provide("json/ext/generator");
-    rb_provide("json/ext/generator.so");
-
-    Init_md5();
-    rb_provide("md5");
-    rb_provide("digest/md5");
-    rb_provide("md5.so");
-    rb_provide("digest/md5.so");
-
-    Init_monitor();
-    rb_provide("monitor");
-    rb_provide("monitor.so");
-
-    Init_nkf();
-    rb_provide("nkf");
-    rb_provide("nkf.so");
-
-    Init_nonblock();
-    rb_provide("nonblock");
-    rb_provide("nonblock.so");
-    rb_provide("io/nonblock");
-    rb_provide("io/nonblock.so");
-
-    Init_ruby_description();
-
-    Init_objspace();
-    rb_provide("objspace");
-    rb_provide("objspace.so");
-
-    Init_parser();
-    rb_provide("json/ext/parser");
-    rb_provide("json/ext/parser.so");
-
-    Init_pathname();
-    rb_provide("pathname");
-    rb_provide("pathname.so");
-
-    Init_psych();
-    rb_provide("psych");
-    rb_provide("psych.so");
-
-    Init_ripper();
-    rb_provide("ripper");
-    rb_provide("ripper.so");
-
-    Init_rmd160();
-    rb_provide("rmd160");
-    rb_provide("digest/rmd160");
-    rb_provide("rmd160.so");
-    rb_provide("digest/rmd160.so");
-
-    Init_sdbm();
-    rb_provide("sdbm");
-    rb_provide("sdbm.so");
-
-    Init_sha1();
-    rb_provide("sha1");
-    rb_provide("digest/sha1");
-    rb_provide("sha1.so");
-    rb_provide("digest/sha1.so");
-
-    Init_sha2();
-    rb_provide("sha2");
-    rb_provide("digest/sha2");
-    rb_provide("sha2.so");
-    rb_provide("digest/sha2.so");
-
-    Init_sizeof();
-    rb_provide("sizeof");
-    rb_provide("sizeof.so");
-
-    Init_socket();
-    rb_provide("socket");
-    rb_provide("socket.so");
-
-    Init_stringio();
-    rb_provide("stringio");
-    rb_provide("stringio.so");
-
-    Init_strscan();
-    rb_provide("strscan");
-    rb_provide("strscan.so");
-
-    Init_wait();
-    rb_provide("wait");
-    rb_provide("io/wait");
-    rb_provide("wait.so");
-    rb_provide("io/wait.so");
-
-    Init_zlib();
-    rb_provide("zlib");
-    rb_provide("zlib.so");
-
-    Init_openssl();
-    rb_provide("openssl");
-    rb_provide("openssl.so");
-
-    Init_nonblock();
-    rb_provide("io/nonblock");
-    rb_provide("io/nonblock.so");
-
-#ifndef _WIN32
-
-    // DLM: we have Init_console on Windows but crashes when try to init it, fails to load openssl
-    Init_console();
-    rb_provide("console");
-    rb_provide("console.so");
-
-    Init_dbm();
-    rb_provide("dbm");
-    rb_provide("dbm.so");
-
-    Init_gdbm();
-    rb_provide("gdbm");
-    rb_provide("gdbm.so");
-
-    Init_pty();
-    rb_provide("pty");
-    rb_provide("pty.so");
-
-    Init_readline();
-    rb_provide("readline");
-    rb_provide("readline.so");
-
-    Init_syslog();
-    rb_provide("syslog");
-    rb_provide("syslog.so");
-#endif
-
-    // openstudio
-    init_openstudio_internal_basic();
+    {
+      auto* execute_python_scriptCommand = experimentalApp->add_subcommand("execute_python_script", "Executes a python file");
+      openstudio::filesystem::path pythonScriptPath;
+      execute_python_scriptCommand->add_option("path", pythonScriptPath, "Path to python file")->required(true);
+      std::vector<std::string> executePythonScriptCommandArgs;
+      execute_python_scriptCommand->add_option("arguments", executePythonScriptCommandArgs, "Arguments to pass to the python file")
+        ->required(false)
+        ->option_text("args");
+      execute_python_scriptCommand->callback([&pythonScriptPath, &pythonEngine, &executePythonScriptCommandArgs] {
+        openstudio::cli::executePythonScriptCommand(pythonScriptPath, pythonEngine, executePythonScriptCommandArgs);
+      });
+    }
 
-    auto module = rb_define_module("OpenStudio");
-    rb_define_module_function(module, "init_rest_of_openstudio", init_rest_of_openstudio, 0);
-  }
+    [[maybe_unused]] auto* gem_listCommand = experimentalApp->add_subcommand("gem_list", "Lists the set gems available to openstudio");
+    [[maybe_unused]] auto* list_commandsCommand = experimentalApp->add_subcommand("list_commands", "Lists the entire set of available commands");
+    [[maybe_unused]] auto* openstudio_versionCommand =
+      experimentalApp->add_subcommand("openstudio_version", "Returns the OpenStudio version used by the CLI")->callback([]() {
+        fmt::print("{}\n", openStudioLongVersion());
+      });
+    [[maybe_unused]] auto* ruby_versionCommand =
+      experimentalApp->add_subcommand("ruby_version", "Returns the Ruby version used by the CLI")->callback([&rubyEngine]() {
+        rubyEngine->exec("puts RUBY_VERSION");
+      });
+    [[maybe_unused]] auto* python_versionCommand =
+      experimentalApp->add_subcommand("python_version", "Returns the Ruby version used by the CLI")->callback([&pythonEngine]() {
+        pythonEngine->exec("import sys; print(sys.version)");
+      });
+
+    // run command
+    openstudio::cli::setupRunOptions(experimentalApp, rubyEngine, pythonEngine);
+
+    // update (model) command
+    // openstudio::cli::setupUpdateCommand(experimentalApp);
+    {
+      bool keep = false;
+      auto* updateCommand = experimentalApp->add_subcommand("update", "Updates OpenStudio Models to the current version");
+      updateCommand->add_flag("-k,--keep", keep, "Keep original files");
+
+      openstudio::filesystem::path updateOsmPath;
+      updateCommand->add_option("path", updateOsmPath, "Path to OSM or directory containing osms")->required(true);
+
+      updateCommand->callback([&keep, &updateOsmPath] {
+        bool result = openstudio::cli::runModelUpdateCommand(updateOsmPath, keep);
+        if (!result) {
+          throw std::runtime_error("Failed to update some models");
+        }
+      });
+    }
 
-  // DLM: this will interpret any strings passed on the command line as UTF-8
-  // can we be smarter and detect the correct encoding? use wmain on windows to get utf-16?
-  // or we might want to follow ruby and allow '--external-encoding=UTF-8' as an input argument?
-  rb_enc_set_default_external(rb_enc_from_encoding(rb_utf8_encoding()));
+    openstudio::cli::MeasureUpdateOptions::setupMeasureUpdateOptions(experimentalApp, rubyEngine);
 
-  // chop off the first argument which is the exe path/name
-  ruby_set_argv(argc - 1, argv + 1);
+    CLI11_PARSE(app, argc, argv);
 
-  try {
-    rubyInterpreter.evalString(R"(
-       begin
-         require 'openstudio_cli'
-       rescue Exception => e
-         puts
-         puts "Error: #{e.message}"
-         puts "Backtrace:\n\t" + e.backtrace.join("\n\t")
-         raise
-       end
-     )");
-  } catch (const std::exception& e) {
-    rubyInterpreter.evalString(R"(STDOUT.flush)");
-    std::cout << "Exception: " << e.what() << std::endl;  // endl will flush
-    return ruby_cleanup(1);
-  } catch (...) {
-    rubyInterpreter.evalString(R"(STDOUT.flush)");
-    std::cout << "Unknown Exception" << std::endl;  // endl will flush
-    return ruby_cleanup(1);
+    if (*execRubyOption) {
+      fmt::print("--execute Flag received {} times.\n", execRubyOption->count());
+      rubyEngine->exec("OpenStudio::init_rest_of_openstudio()");
+      for (auto& cmd : executeRubyCmds) {
+        fmt::print("{}\n", cmd);
+        rubyEngine->exec(cmd);
+      }
+    }
+    if (*execPythonOption) {
+      fmt::print("--pyexecute Flag received {} times.\n", execPythonOption->count());
+      for (auto& cmd : executePythonCmds) {
+        fmt::print("{}\n", cmd);
+        pythonEngine->exec(cmd);
+      }
+    }
+    // fmt::print("includeDirs={}\n", fmt::join(includeDirs, ","));
+    // fmt::print("gemPathDirs={}\n", fmt::join(gemPathDirs, ","));
+    // fmt::print("gemHomeDir={}\n", gemHomeDir);
+  } else {
+    result = openstudio::rubyCLI(rubyEngine);
   }
-  rubyInterpreter.evalString(R"(STDOUT.flush)");
-  std::cout << std::flush;
-  return ruby_cleanup(0);
-}
 
-extern "C"
-{
-  int rb_hasFile(const char* t_filename) {
-    // TODO Consider expanding this to use the path which we have artificially defined in embedded_help.rb
-    std::string expandedName = std::string(":/ruby/2.7.0/") + std::string(t_filename) + ".rb";
-    return embedded_files::hasFile(expandedName);
-  }
+  // Important to destroy RubyEngine and finalize Ruby at the right time
+  // After the main function returns it is (apparently) too late
+  rubyEngine.reset();
+  pythonEngine.reset();
 
-  int rb_require_embedded(const char* t_filename) {
-    std::string require_script = R"(require ')" + std::string(t_filename) + R"(')";
-    rubyInterpreter.evalString(require_script);
-    return 0;
-  }
+  return result;
 }
diff --git a/src/gbxml/CMakeLists.txt b/src/gbxml/CMakeLists.txt
index 39f83ceb9cd..e312bfb6847 100644
--- a/src/gbxml/CMakeLists.txt
+++ b/src/gbxml/CMakeLists.txt
@@ -22,25 +22,12 @@ set(${target_name}_swig_src
   gbXML.i
 )
 
-file(GLOB_RECURSE XSD_SCHEMA_FILES  FOLLOW_SYMLINKS "${CMAKE_CURRENT_SOURCE_DIR}/resources/*.xsd")
-foreach(_FILE ${XSD_SCHEMA_FILES})
-  file(RELATIVE_PATH LOCATION "${CMAKE_CURRENT_SOURCE_DIR}" ${_FILE})
-  list(APPEND E_FILES ${_FILE})
-  list(APPEND E_PATHS ${LOCATION})
-endforeach()
-
-include("${PROJECT_SOURCE_DIR}/embedded/EmbedFiles.cmake")
-embed_files("${E_FILES}" "${E_PATHS}" EMBEDDED_OUTPUT openstudiogbxml)
-
-# set up groups of source files for Visual Studio
-source_group(embedded FILES ${EMBEDDED_OUTPUT})
-
 set(${target_name}_depends
   ${openstudio_energyplus_depends}
 )
 set(${target_name}_depends ${${target_name}_depends} PARENT_SCOPE)
 
-add_library(${target_name} OBJECT ${${target_name}_src} ${EMBEDDED_OUTPUT})
+add_library(${target_name} OBJECT ${${target_name}_src})
 target_link_libraries(${target_name} PUBLIC ${${target_name}_depends})
 target_compile_definitions(${target_name} PRIVATE openstudio_gbxml_EXPORTS SHARED_OS_LIBS)
 target_include_directories(${target_name} PUBLIC ${PROJECT_BINARY_DIR})
diff --git a/src/lib/CMakeLists.txt b/src/lib/CMakeLists.txt
index 7892dde436b..01453aebc64 100644
--- a/src/lib/CMakeLists.txt
+++ b/src/lib/CMakeLists.txt
@@ -30,8 +30,7 @@ target_link_libraries(
  CONAN_PKG::openssl
  CONAN_PKG::pugixml
  CONAN_PKG::libxml2
- # CONAN_PKG::libxslt
- # CONAN_PKG::tinygltf  # TODO: needed?
+ CONAN_PKG::fmt
 )
 
 target_compile_definitions(openstudiolib INTERFACE "-DSHARED_OS_LIBS")
diff --git a/src/measure/EnergyPlusMeasure.cpp b/src/measure/EnergyPlusMeasure.cpp
index 51177394a1f..f1d96825a02 100644
--- a/src/measure/EnergyPlusMeasure.cpp
+++ b/src/measure/EnergyPlusMeasure.cpp
@@ -38,14 +38,14 @@
 namespace openstudio {
 namespace measure {
 
-  EnergyPlusMeasure::~EnergyPlusMeasure() {}
+  EnergyPlusMeasure::~EnergyPlusMeasure() = default;
 
   std::vector<OSArgument> EnergyPlusMeasure::arguments(const openstudio::Workspace& workspace) const {
-    return OSArgumentVector();
+    return {};
   }
 
   std::vector<OSOutput> EnergyPlusMeasure::outputs() const {
-    return OSOutputVector();
+    return {};
   }
 
   bool EnergyPlusMeasure::run(openstudio::Workspace& workspace, OSRunner& runner, const std::map<std::string, OSArgument>& user_arguments) const {
@@ -53,5 +53,21 @@ namespace measure {
     return true;
   }
 
+  PythonEnergyPlusMeasure::~PythonEnergyPlusMeasure() = default;
+
+  std::vector<OSArgument> PythonEnergyPlusMeasure::arguments(const openstudio::Workspace& workspace) const {
+    return {};
+  }
+
+  std::vector<OSOutput> PythonEnergyPlusMeasure::outputs() const {
+    return {};
+  }
+
+  bool PythonEnergyPlusMeasure::run(openstudio::Workspace& workspace, OSRunner& runner,
+                                    const std::map<std::string, OSArgument>& user_arguments) const {
+    runner.prepareForMeasureRun(*this);
+    return true;
+  }
+
 }  // namespace measure
 }  // namespace openstudio
diff --git a/src/measure/EnergyPlusMeasure.hpp b/src/measure/EnergyPlusMeasure.hpp
index 41c2ae02357..9a7b927b2c9 100644
--- a/src/measure/EnergyPlusMeasure.hpp
+++ b/src/measure/EnergyPlusMeasure.hpp
@@ -79,12 +79,56 @@ namespace measure {
     //@}
 
    protected:
-    EnergyPlusMeasure() {}
+    EnergyPlusMeasure() = default;
 
    private:
     REGISTER_LOGGER("openstudio.measure.EnergyPlusMeasure");
   };
 
+  /** PythonEnergyPlusMeasure is an abstract base class for OSMeasures that operate on PythonEnergyPlus
+ *  Workspaces. */
+  class MEASURE_API PythonEnergyPlusMeasure : public OSMeasure
+  {
+   public:
+    /** @name Constructors and Destructors */
+    //@{
+
+    virtual ~PythonEnergyPlusMeasure();
+
+    //@}
+    /** @name Getters */
+    //@{
+
+    /** Returns the arguments for this script. In interactive applications, an OSRunner presents
+   *  these arguments to the user to produce an OSArgumentMap of user_arguments that it then passes
+   *  to this script's run method. The same basic steps should happen in applications with non-
+   *  interactive scripts, but in that case an entity other than an OSRunner may be in charge of
+   *  collecting user arguments. The base class implementation returns an empty vector. */
+    virtual std::vector<OSArgument> arguments(const openstudio::Workspace& workspace) const;
+
+    /** Returns the outputs for this script. The base class implementation returns an empty vector. */
+    virtual std::vector<OSOutput> outputs() const;
+
+    //@}
+    /** @name Actions */
+    //@{
+
+    /** Run the script on the given workspace with the given runner and user_arguments. The base
+   *  class implementation calls runner.prepareForMeasureRun(*this) and should be called at the
+   *  beginning of derived class implementations of this method. (In C++, that call looks like
+   *  PythonEnergyPlusMeasure::run(workspace, runner, user_arguments). In Ruby that call looks like
+   *  super(workspace, runner, user_arguments). */
+    virtual bool run(openstudio::Workspace& workspace, OSRunner& runner, const std::map<std::string, OSArgument>& user_arguments) const;
+
+    //@}
+
+   protected:
+    PythonEnergyPlusMeasure() = default;
+
+   private:
+    REGISTER_LOGGER("openstudio.measure.PythonEnergyPlusMeasure");
+  };
+
 }  // namespace measure
 }  // namespace openstudio
 
diff --git a/src/measure/Measure.i b/src/measure/Measure.i
index 98901ba6b6d..88f2131a7ad 100644
--- a/src/measure/Measure.i
+++ b/src/measure/Measure.i
@@ -2,7 +2,7 @@
 #define MEASURE_I
 
 #ifdef SWIGPYTHON
-%module openstudiomeasure
+%module(directors="1") openstudiomeasure
 #endif
 
 
@@ -24,6 +24,18 @@
   // Help in overload resolution preferring std::string over char const *
   %ignore openstudio::measure::OSArgument::setValue(char const*);
   %ignore openstudio::measure::OSArgument::setDefaultValue(char const*);
+#elif defined(SWIGRUBY)
+  %ignore openstudio::measure::PythonModelMeasure;
+  %ignore openstudio::measure::PythonEnergyPlusMeasure;
+  %ignore openstudio::measure::PythonReportingMeasure;
+#elif defined(SWIGPYTHON)
+  // Avoid triggering a SWIG warning: 'print' is a python keyword
+  %rename(toString) openstudio::measure::OSArgument::print;
+  %rename(toString) openstudio::measure::OSOutput::print;
+
+  %ignore openstudio::measure::ModelMeasure;
+  %ignore openstudio::measure::EnergyPlusMeasure;
+  %ignore openstudio::measure::ReportingMeasure;
 #endif
 
 %{
@@ -48,10 +60,6 @@
   using namespace openstudio::measure;
 %}
 
-//user scripts
-%template(OSArgumentVector) std::vector<openstudio::measure::OSArgument>;
-%template(OptionalOSArgument) boost::optional<openstudio::measure::OSArgument>;
-%template(OSArgumentMap) std::map<std::string, openstudio::measure::OSArgument>;
 
 %ignore std::vector<openstudio::measure::OSOutput>::vector(size_type);
 %ignore std::vector<openstudio::measure::OSOutput>::resize(size_type);
@@ -59,10 +67,26 @@
 %template(OptionalOSOutput) boost::optional<openstudio::measure::OSOutput>;
 
 %feature("director") OSMeasure;
-%feature("director") ModelMeasure;
-%feature("director") EnergyPlusMeasure;
-%feature("director") ReportingMeasure;
+
+#if defined(SWIGPYTHON)
+  %feature("director") PythonModelMeasure;
+  %feature("director") PythonEnergyPlusMeasure;
+  %feature("director") PythonReportingMeasure;
+  %rename (ModelMeasure) openstudio::measure::PythonModelMeasure;
+  %rename (EnergyPlusMeasure) openstudio::measure::PythonEnergyPlusMeasure;
+  %rename (ReportingMeasure) openstudio::measure::PythonReportingMeasure;
+#else
+  %feature("director") ModelMeasure;
+  %feature("director") EnergyPlusMeasure;
+  %feature("director") ReportingMeasure;
+#endif
 %feature("director") OSRunner;
+%feature("director") OSArgument;
+
+//user scripts
+%template(OSArgumentVector) std::vector<openstudio::measure::OSArgument>;
+%template(OptionalOSArgument) boost::optional<openstudio::measure::OSArgument>;
+%template(OSArgumentMap) std::map<std::string, openstudio::measure::OSArgument>;
 
 %include <measure/OSArgument.hpp>
 %include <measure/OSOutput.hpp>
diff --git a/src/measure/ModelMeasure.cpp b/src/measure/ModelMeasure.cpp
index 833e41aac68..d4dd92cd266 100644
--- a/src/measure/ModelMeasure.cpp
+++ b/src/measure/ModelMeasure.cpp
@@ -36,14 +36,14 @@
 namespace openstudio {
 namespace measure {
 
-  ModelMeasure::~ModelMeasure() {}
+  ModelMeasure::~ModelMeasure() = default;
 
   std::vector<OSArgument> ModelMeasure::arguments(const openstudio::model::Model& model) const {
-    return OSArgumentVector();
+    return {};
   }
 
   std::vector<OSOutput> ModelMeasure::outputs() const {
-    return OSOutputVector();
+    return {};
   }
 
   bool ModelMeasure::run(openstudio::model::Model& model, OSRunner& runner, const std::map<std::string, OSArgument>& user_arguments) const {
@@ -51,5 +51,20 @@ namespace measure {
     return true;
   }
 
+  PythonModelMeasure::~PythonModelMeasure() {}
+
+  std::vector<OSArgument> PythonModelMeasure::arguments(const openstudio::model::Model& model) const {
+    return {};
+  }
+
+  std::vector<OSOutput> PythonModelMeasure::outputs() const {
+    return {};
+  }
+
+  bool PythonModelMeasure::run(openstudio::model::Model& model, OSRunner& runner, const std::map<std::string, OSArgument>& user_arguments) const {
+    runner.prepareForMeasureRun(*this);
+    return true;
+  }
+
 }  // namespace measure
 }  // namespace openstudio
diff --git a/src/measure/ModelMeasure.hpp b/src/measure/ModelMeasure.hpp
index d3096f7b521..9f6309fb449 100644
--- a/src/measure/ModelMeasure.hpp
+++ b/src/measure/ModelMeasure.hpp
@@ -80,7 +80,50 @@ namespace measure {
     //@}
     //
    protected:
-    ModelMeasure() {}
+    ModelMeasure() = default;
+
+   private:
+    REGISTER_LOGGER("openstudio.measure.ModelMeasure");
+  };
+
+  /** PythonModelMeasure is an abstract base class for UserScripts that operate on OpenStudio Models. */
+  class MEASURE_API PythonModelMeasure : public OSMeasure
+  {
+   public:
+    /** @name Constructors and Destructors */
+    //@{
+
+    virtual ~PythonModelMeasure();
+
+    //@}
+    /** @name Getters */
+    //@{
+
+    /** Returns the arguments for this script. In interactive applications, an OSRunner presents
+   *  these arguments to the user to produce an OSArgumentMap of user_arguments that it then passes
+   *  to this script's run method. The same basic steps should happen in applications with non-
+   *  interactive scripts, but in that case an entity other than an OSRunner may be in charge of
+   *  collecting user arguments. The base class implementation returns an empty vector. */
+    virtual std::vector<OSArgument> arguments(const openstudio::model::Model& model) const;
+
+    /** Returns the outputs for this script. The base class implementation returns an empty vector. */
+    virtual std::vector<OSOutput> outputs() const;
+
+    //@}
+    /** @name Actions */
+    //@{
+
+    /** Run the script on the given model with the given runner and user_arguments. The base class
+   *  implementation calls runner.prepareForMeasureRun(*this) and should be called at the
+   *  beginning of derived class implementations of this method. (In C++, that call looks like
+   *  PythonModelMeasure::run(model, runner, user_arguments). In Ruby that call looks like
+   *  super(model, runner, user_arguments). */
+    virtual bool run(openstudio::model::Model& model, OSRunner& runner, const std::map<std::string, OSArgument>& user_arguments) const;
+
+    //@}
+    //
+   protected:
+    PythonModelMeasure() = default;
 
    private:
     REGISTER_LOGGER("openstudio.measure.ModelMeasure");
diff --git a/src/measure/OSArgument.hpp b/src/measure/OSArgument.hpp
index db402fd4525..aff7a7577ba 100644
--- a/src/measure/OSArgument.hpp
+++ b/src/measure/OSArgument.hpp
@@ -97,7 +97,7 @@ OPENSTUDIO_ENUM( OSDomainType,
 
   /** Preserves old name for OSArgumentType. \deprecated */
   // TODO: JM 2018-11-28: Time to remove no?
-  typedef OSArgumentType UserScriptArgumentType;
+  using UserScriptArgumentType = OSArgumentType;
 
   /** OSArgument is an argument to an OSMeasure. **/
   class MEASURE_API OSArgument
@@ -415,7 +415,7 @@ OPENSTUDIO_ENUM( OSDomainType,
     // Note JM 2018-11-28:
     // typedef for the std::variant we will use for value, default value, and domain
     // we add std::monostate to allow the variant to be empty basically
-    typedef std::variant<std::monostate, bool, double, int, std::string, openstudio::path> OSArgumentVariant;
+    using OSArgumentVariant = std::variant<std::monostate, bool, double, int, std::string, openstudio::path>;
 
     bool setStringInternal(OSArgumentVariant& variant, const std::string& value);
 
@@ -444,16 +444,16 @@ OPENSTUDIO_ENUM( OSDomainType,
   };
 
   /** \relates OSArgument */
-  typedef boost::optional<OSArgument> OptionalOSArgument;
+  using OptionalOSArgument = boost::optional<OSArgument>;
 
   /** \relates OSArgument */
-  typedef std::vector<OSArgument> OSArgumentVector;
+  using OSArgumentVector = std::vector<OSArgument>;
 
   /** \relates OSArgument */
-  typedef std::map<std::string, OSArgument> OSArgumentMap;
+  using OSArgumentMap = std::map<std::string, OSArgument>;
 
   /** \relates OSArgument */
-  typedef std::pair<openstudio::path, std::vector<OSArgument>> UserScriptInfo;
+  using UserScriptInfo = std::pair<openstudio::path, std::vector<OSArgument>>;
 
   /** Prints argument data to ostream for debugging purposes. \relates OSArgument */
   MEASURE_API std::ostream& operator<<(std::ostream& os, const OSArgument& arg);
diff --git a/src/measure/OSMeasure.cpp b/src/measure/OSMeasure.cpp
index e53769d71c7..0c47b720dfd 100644
--- a/src/measure/OSMeasure.cpp
+++ b/src/measure/OSMeasure.cpp
@@ -32,7 +32,7 @@
 namespace openstudio {
 namespace measure {
 
-  OSMeasure::~OSMeasure() {}
+  OSMeasure::~OSMeasure() = default;
 
   std::string OSMeasure::name() const {
     return "";
diff --git a/src/measure/ReportingMeasure.cpp b/src/measure/ReportingMeasure.cpp
index 3b6524b9824..cb51d38c88d 100644
--- a/src/measure/ReportingMeasure.cpp
+++ b/src/measure/ReportingMeasure.cpp
@@ -38,14 +38,14 @@
 namespace openstudio {
 namespace measure {
 
-  ReportingMeasure::~ReportingMeasure() {}
+  ReportingMeasure::~ReportingMeasure() = default;
 
   std::vector<OSArgument> ReportingMeasure::arguments(const openstudio::model::Model& model) const {
-    return OSArgumentVector();
+    return {};
   }
 
   std::vector<OSOutput> ReportingMeasure::outputs() const {
-    return OSOutputVector();
+    return {};
   }
 
   bool ReportingMeasure::run(OSRunner& runner, const std::map<std::string, OSArgument>& user_arguments) const {
@@ -54,7 +54,27 @@ namespace measure {
   }
 
   std::vector<IdfObject> ReportingMeasure::energyPlusOutputRequests(OSRunner& runner, const std::map<std::string, OSArgument>& user_arguments) const {
-    return std::vector<IdfObject>();
+    return {};
+  }
+
+  PythonReportingMeasure::~PythonReportingMeasure() = default;
+
+  std::vector<OSArgument> PythonReportingMeasure::arguments(const openstudio::model::Model& model) const {
+    return {};
+  }
+
+  std::vector<OSOutput> PythonReportingMeasure::outputs() const {
+    return {};
+  }
+
+  bool PythonReportingMeasure::run(OSRunner& runner, const std::map<std::string, OSArgument>& user_arguments) const {
+    runner.prepareForMeasureRun(*this);
+    return true;
+  }
+
+  std::vector<IdfObject> PythonReportingMeasure::energyPlusOutputRequests(OSRunner& runner,
+                                                                          const std::map<std::string, OSArgument>& user_arguments) const {
+    return {};
   }
 
 }  // namespace measure
diff --git a/src/measure/ReportingMeasure.hpp b/src/measure/ReportingMeasure.hpp
index 46e741bae1e..9d9d03ee67e 100644
--- a/src/measure/ReportingMeasure.hpp
+++ b/src/measure/ReportingMeasure.hpp
@@ -89,12 +89,62 @@ namespace measure {
     virtual std::vector<IdfObject> energyPlusOutputRequests(OSRunner& runner, const std::map<std::string, OSArgument>& user_arguments) const;
     //@}
    protected:
-    ReportingMeasure() {}
+    ReportingMeasure() = default;
 
    private:
     REGISTER_LOGGER("openstudio.measure.ReportingMeasure");
   };
 
+  /** PythonReportingMeasure is an abstract base class for UserScripts that generate reports. */
+  class MEASURE_API PythonReportingMeasure : public OSMeasure
+  {
+   public:
+    /** @name Constructors and Destructors */
+    //@{
+
+    virtual ~PythonReportingMeasure();
+
+    //@}
+    /** @name Getters */
+    //@{
+
+    /** Returns the arguments for this script. In interactive applications, an OSRunner presents
+   *  these arguments to the user to produce an OSArgumentMap of user_arguments that it then passes
+   *  to this script's run method. The same basic steps should happen in applications with non-
+   *  interactive scripts, but in that case an entity other than an OSRunner may be in charge of
+   *  collecting user arguments. The base class implementation returns an empty vector. */
+    virtual std::vector<OSArgument> arguments(const openstudio::model::Model& model) const;
+
+    /** Returns the outputs for this script. The base class implementation returns an empty vector. */
+    virtual std::vector<OSOutput> outputs() const;
+
+    //@}
+    /** @name Actions */
+    //@{
+
+    /** Run the script with the given runner and user_arguments. The base class implementation calls
+   *  runner.prepareForMeasureRun(*this) and should be called at the beginning of derived class
+   *  implementations of this method. (In C++, that call looks like
+   *  PythonReportingMeasure::run(runner, user_arguments). In Ruby that call looks like
+   *  super(runner, user_arguments). */
+    virtual bool run(OSRunner& runner, const std::map<std::string, OSArgument>& user_arguments) const;
+
+    /** This method is called on all reporting measures immediately before the E+
+   *  simulation. The code that injects these objects into the IDF checks that
+   *  only objects of allowed types are added to prevent changes that impact
+   *  energy use. The injector code also prevents duplicate report requests.
+   *  The default implementation of this method returns an empty vector.
+   *  The runner allows the user to load the last osm/idf and to log messages.
+   * The argument map is available with the arguments for the reporting measure. */
+    virtual std::vector<IdfObject> energyPlusOutputRequests(OSRunner& runner, const std::map<std::string, OSArgument>& user_arguments) const;
+    //@}
+   protected:
+    PythonReportingMeasure() = default;
+
+   private:
+    REGISTER_LOGGER("openstudio.measure.PythonReportingMeasure");
+  };
+
 }  // namespace measure
 }  // namespace openstudio
 
diff --git a/src/model/Model.i b/src/model/Model.i
index 6036133b294..93c30472427 100644
--- a/src/model/Model.i
+++ b/src/model/Model.i
@@ -2,7 +2,7 @@
 #define MODEL_I
 
 #ifdef SWIGPYTHON
-  %module openstudiomodel
+  %module(directors="1") openstudiomodel
 
   // Inject every submodule into the openstudio.model namespace, so we can do like in Ruby
   // `fan = openstudio.model.FanVariableVolume(m)` (and not `openstudio.model.openstudiomodelstraightcomponent.FanVariableVolume(m)`)
diff --git a/src/model/ModelCore.i b/src/model/ModelCore.i
index c3a928e6dc5..afbca89ad6b 100644
--- a/src/model/ModelCore.i
+++ b/src/model/ModelCore.i
@@ -3,7 +3,7 @@
 #define MODEL_CORE_I
 
 #ifdef SWIGPYTHON
-  %module openstudiomodelcore
+  %module(directors="1") openstudiomodelcore
 #endif
 
 %include <model/Model_Common_Include.i>
@@ -192,6 +192,8 @@ class Construction;
 %feature("valuewrapper") Curve;
 %feature("valuewrapper") Construction;
 
+%feature("director") Model;
+
 }
 }
 
diff --git a/src/scriptengine/CMakeLists.txt b/src/scriptengine/CMakeLists.txt
new file mode 100644
index 00000000000..fc9f52faf5e
--- /dev/null
+++ b/src/scriptengine/CMakeLists.txt
@@ -0,0 +1,5 @@
+# Script Engine is a base for scripting in OpenStudio
+# See implementations in ruby/engine and python/engine
+
+add_library(openstudio_scriptengine INTERFACE)
+target_include_directories(openstudio_scriptengine INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
diff --git a/src/scriptengine/ScriptEngine.hpp b/src/scriptengine/ScriptEngine.hpp
new file mode 100644
index 00000000000..e8c4e7e058a
--- /dev/null
+++ b/src/scriptengine/ScriptEngine.hpp
@@ -0,0 +1,141 @@
+#ifndef SCRIPTENGINE_included
+#define SCRIPTENGINE_included
+
+#include "../utilities/core/ApplicationPathHelpers.hpp"
+#include "../utilities/core/DynamicLibrary.hpp"
+#include <any>
+#include <functional>
+#include <map>
+#include <memory>
+#include <stdexcept>
+#include <string>
+#include <string_view>
+#include <typeinfo>
+
+namespace openstudio {
+
+namespace measure {
+  class ModelMeasure;
+}  // namespace measure
+
+class ScriptEngine;
+
+struct ScriptObject
+{
+  std::any object;
+};
+}  // namespace openstudio
+
+using ScriptEngineFactoryType = openstudio::ScriptEngine*(int, char**);
+
+namespace openstudio {
+
+class ScriptEngine
+{
+ public:
+  ScriptEngine([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) {
+    // registerType<openstudio::measure::ModelMeasure*>("openstudio::measure::ModelMeasure *");
+  }
+
+  virtual ~ScriptEngine() = default;
+  ScriptEngine(const ScriptEngine&) = delete;
+  ScriptEngine(ScriptEngine&&) = delete;
+  ScriptEngine& operator=(const ScriptEngine&) = delete;
+  ScriptEngine& operator=(ScriptEngine&&) = delete;
+  ScriptEngine* operator->() {
+    return this;
+  }
+
+  virtual ScriptObject eval(std::string_view sv) = 0;
+
+  // execute string without expecting a return value
+  virtual void exec(std::string_view sv) = 0;
+
+  template <typename T>
+  T getAs(ScriptObject& obj) {
+    void* result = getAs_impl(obj, typeid(T));
+    if (result) {
+      return static_cast<T>(result);
+    } else {
+      throw std::bad_cast();
+    }
+  }
+
+  template <typename T>
+  void registerType(std::string name) {
+    types.emplace(std::cref(typeid(T)), std::move(name));
+  }
+
+ protected:
+  // convert the underlying object to the correct type, then return it as a void *
+  // so the above template function can provide it back to the caller.
+  virtual void* getAs_impl(ScriptObject& obj, const std::type_info&) = 0;
+
+  const std::string& getRegisteredTypeName(const std::type_info& type) {
+    const auto& found_name = types.find(type);
+
+    if (found_name != types.end()) {
+      return found_name->second;
+    }
+
+    throw std::runtime_error("unknown type requested");
+  }
+
+ private:
+  struct Compare
+  {
+    bool operator()(const std::reference_wrapper<const std::type_info>& lhs, const std::reference_wrapper<const std::type_info>& rhs) const {
+      return lhs.get().before(rhs.get());
+    }
+  };
+
+  std::map<std::reference_wrapper<const std::type_info>, std::string, Compare> types;
+};
+
+// The purpose of this class is to delay creating the scripting engine
+// until it is needed, while still initializing with argc and argv
+class ScriptEngineInstance
+{
+ public:
+  ScriptEngineInstance(std::string_view libraryBaseName, int argc, char* argv[]) : libraryName(libraryBaseName), args(argv, argv + argc) {}
+
+  ScriptEngineInstance(const ScriptEngineInstance&) = delete;
+  ScriptEngineInstance(ScriptEngineInstance&&) = delete;
+  ScriptEngineInstance& operator=(const ScriptEngineInstance&) = delete;
+  ScriptEngineInstance& operator=(ScriptEngineInstance&&) = delete;
+
+  openstudio::ScriptEngine& operator->() {
+    if (instance) {
+      return *(instance.get());
+    } else {
+      std::vector<char*> argv;
+      std::transform(args.begin(), args.end(), std::back_inserter(argv), [](const std::string& item) { return const_cast<char*>(item.c_str()); });
+
+      const auto enginePath = getOpenStudioModuleDirectory() / openstudio::getSharedModuleName(libraryName);
+      // DynamicLibrary will perform dlopen on construction and dlclose on destruction
+      // Don't create the DynamicLibrary until it is going to be used
+      engineLib = std::unique_ptr<DynamicLibrary>(new DynamicLibrary(enginePath));
+      const auto factory = engineLib->load_symbol<ScriptEngineFactoryType>("makeScriptEngine");
+      instance = std::unique_ptr<ScriptEngine>(factory(args.size(), argv.data()));
+
+      return *instance;
+    }
+  }
+  explicit operator bool() {
+    return (bool)instance;
+  }
+  void reset() {
+    instance.reset();
+    engineLib.reset();
+  }
+
+ private:
+  std::string libraryName;
+  std::vector<std::string> args;
+  std::unique_ptr<ScriptEngine> instance;
+  std::unique_ptr<DynamicLibrary> engineLib;
+};
+
+}  // namespace openstudio
+
+#endif
diff --git a/src/scriptengine/ScriptEngineAPI.hpp b/src/scriptengine/ScriptEngineAPI.hpp
new file mode 100644
index 00000000000..ed7954c8d01
--- /dev/null
+++ b/src/scriptengine/ScriptEngineAPI.hpp
@@ -0,0 +1,52 @@
+/***********************************************************************************************************************
+*  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 SCRIPTENGINE_SCRIPTENGINEAPI_HPP
+#define SCRIPTENGINE_SCRIPTENGINEAPI_HPP
+
+#if SHARED_OS_LIBS
+#  if (_WIN32 || _MSC_VER)
+#    if defined(openstudio_scriptengine_EXPORTS) || defined(openstudio_EXPORTS)
+#      define SCRIPTENGINE_API __declspec(dllexport)
+#    else
+#      define SCRIPTENGINE_API __declspec(dllimport)
+#    endif
+#  else
+#    if defined(openstudio_scriptengine_EXPORTS) || defined(openstudio_EXPORTS)
+#      define SCRIPTENGINE_API __attribute__((visibility("default")))
+#    else
+#      define SCRIPTENGINE_API
+#    endif
+#  endif
+#else
+#  define SCRIPTENGINE_API
+#  error "This shouldn't happen. ScriptEngine is always supposed to be used in as a Shared Library"
+#endif
+
+#endif  // SCRIPTENGINE_SCRIPTENGINEAPI_HPP
diff --git a/src/utilities/CMakeLists.txt b/src/utilities/CMakeLists.txt
index 672b5d2c657..e74dc9fd967 100644
--- a/src/utilities/CMakeLists.txt
+++ b/src/utilities/CMakeLists.txt
@@ -23,6 +23,9 @@ set(core_src
   core/Containers.hpp
   core/Containers.cpp
   core/Deprecated.hpp
+  core/DynamicLibrary.hpp
+  core/DynamicLibraryPOSIX.hpp
+  core/DynamicLibraryWindows.hpp
   core/Enum.hpp
   core/EnumHelpers.hpp
   core/Exception.hpp
@@ -223,6 +226,13 @@ foreach(_FILE ${XLST_TRANSFORM_FILES})
   list(APPEND E_PATHS ${LOCATION})
 endforeach()
 
+file(GLOB_RECURSE XSD_SCHEMA_FILES  FOLLOW_SYMLINKS "${CMAKE_CURRENT_SOURCE_DIR}/xml/resources/*.xsd")
+foreach(_FILE ${XSD_SCHEMA_FILES})
+  file(RELATIVE_PATH LOCATION "${CMAKE_CURRENT_SOURCE_DIR}" ${_FILE})
+  list(APPEND E_FILES ${_FILE})
+  list(APPEND E_PATHS ${LOCATION})
+endforeach()
+
 include("${PROJECT_SOURCE_DIR}/embedded/EmbedFiles.cmake")
 embed_files("${E_FILES}" "${E_PATHS}" EMBEDDED_OUTPUT openstudio)
 
@@ -452,6 +462,51 @@ target_compile_definitions(${target_name} PRIVATE openstudio_utilities_EXPORTS S
 target_include_directories(${target_name} PUBLIC ${PROJECT_BINARY_DIR})
 target_include_directories(${target_name} PUBLIC ${PROJECT_BINARY_DIR}/src)
 
+include(CheckIncludeFile)
+check_include_file("unistd.h" HAVE_UNISTD_H)
+if(HAVE_UNISTD_H)
+  target_compile_definitions(${target_name} PUBLIC "-DHAVE_UNISTD_H")
+endif()
+
+# To be used by the main cli program, this target
+# is the most minimal thing that provides Path, and Filesystem
+# capabilities, which the cli requires.
+add_library(openstudio_utilities_minimal
+  OBJECT
+  core/ApplicationPathHelpers.hpp
+  ${CMAKE_CURRENT_BINARY_DIR}/core/ApplicationPathHelpers.cxx
+  core/DynamicLibrary.hpp
+  core/DynamicLibraryPOSIX.hpp
+  core/DynamicLibraryWindows.hpp
+  core/Filesystem.hpp
+  core/Filesystem.cpp
+  core/FilesystemHelpers.hpp
+  core/FilesystemHelpers.cpp
+  core/Logger.hpp
+  core/Logger.cpp
+  core/LogMessage.hpp
+  core/LogMessage.cpp
+  core/LogSink.hpp
+  core/LogSink_Impl.hpp
+  core/LogSink.cpp
+  core/Path.hpp
+  core/Path.cpp
+  core/PathHelpers.hpp
+  core/PathHelpers.cpp
+  core/String.hpp
+  core/String.cpp
+  core/StringHelpers.hpp
+  core/StringHelpers.cpp
+  core/StringStreamLogSink.hpp
+  core/StringStreamLogSink_Impl.hpp
+  core/StringStreamLogSink.cpp
+)
+target_link_libraries(openstudio_utilities_minimal PUBLIC ${${target_name}_depends})
+
+if(HAVE_UNISTD_H)
+  target_compile_definitions(openstudio_utilities_minimal PUBLIC "-DHAVE_UNISTD_H")
+endif()
+
 if(BUILD_TESTING)
   CREATE_TEST_TARGETS(${target_name} "${${target_name}_test_src}" openstudiolib)
   add_dependencies(${target_name}_tests openstudio_model_resources)
diff --git a/src/utilities/Utilities.i b/src/utilities/Utilities.i
index 8efc4df1bbd..048a129feba 100644
--- a/src/utilities/Utilities.i
+++ b/src/utilities/Utilities.i
@@ -2,7 +2,7 @@
 #define UTILITIES_UTILITIES_I
 
 #ifdef SWIGPYTHON
-%module openstudioutilities
+%module(directors="1") openstudioutilities
 #endif
 
 
diff --git a/src/utilities/bcl/BCLFileReference.cpp b/src/utilities/bcl/BCLFileReference.cpp
index 7f2eca559e4..24c26926946 100644
--- a/src/utilities/bcl/BCLFileReference.cpp
+++ b/src/utilities/bcl/BCLFileReference.cpp
@@ -52,7 +52,7 @@ BCLFileReference::BCLFileReference(const openstudio::path& measureRootDir, const
     } else if (fileType == "idf") {
       m_softwareProgram = "EnergyPlus";
       //m_softwareProgramVersion = "";
-    } else if (fileType == "rb") {
+    } else if ((fileType == "rb") || (fileType == "py")) {
       m_softwareProgram = "OpenStudio";
       //m_softwareProgramVersion = "";
     }
diff --git a/src/utilities/bcl/BCLMeasure.cpp b/src/utilities/bcl/BCLMeasure.cpp
index 5a5ea0395a5..251b70a4b94 100644
--- a/src/utilities/bcl/BCLMeasure.cpp
+++ b/src/utilities/bcl/BCLMeasure.cpp
@@ -64,9 +64,10 @@ namespace openstudio {
 *  R U L E S  *
 ***************/
 
-static constexpr std::array<std::pair<std::string_view, std::string_view>, 4> rootToUsageTypeMap{{
+static constexpr std::array<std::pair<std::string_view, std::string_view>, 5> rootToUsageTypeMap{{
   // fileName, usageType
   {"measure.rb", "script"},
+  {"measure.py", "script"},
   {"LICENSE.md", "license"},
   {"README.md", "readme"},
   {"README.md.erb", "readmeerb"}
@@ -172,7 +173,8 @@ void BCLMeasure::createDirectory(const openstudio::path& dir) {
 }
 
 BCLMeasure::BCLMeasure(const std::string& name, const std::string& className, const openstudio::path& dir, const std::string& taxonomyTag,
-                       MeasureType measureType, const std::string& description, const std::string& modelerDescription)
+                       MeasureType measureType, const std::string& description, const std::string& modelerDescription,
+                       MeasureLanguage measureLanguage)
   : m_directory(openstudio::filesystem::system_complete(dir)), m_bclXML(BCLXMLType::MeasureXML) {
 
   // Avoid potential problems, since toPath("/path/to/dir/") != toPath("/path/to/dir")
@@ -189,9 +191,11 @@ BCLMeasure::BCLMeasure(const std::string& name, const std::string& className, co
   // read in template files
   std::string measureTemplate;
   std::string licenseTemplate = ":/templates/common/LICENSE.md";
-  std::string readmeTemplate = ":/templates/common/README.md.erb";
+  std::string readmeTemplate;
+  if (measureLanguage == MeasureLanguage::Ruby) {
+    readmeTemplate = ":/templates/common/README.md.erb";
+  }
   std::string docTemplate = ":/templates/common/docs/.gitkeep";
-  ;
   std::string testTemplate;
 
   std::string templateClassName;
@@ -208,8 +212,13 @@ BCLMeasure::BCLMeasure(const std::string& name, const std::string& className, co
   openstudio::path testEPWRelativePath;
   openstudio::path resourceFileRelativePath;
   if (measureType == MeasureType::ModelMeasure) {
-    measureTemplate = ":/templates/ModelMeasure/measure.rb";
-    testTemplate = ":/templates/ModelMeasure/tests/model_measure_test.rb";
+    if (measureLanguage == MeasureLanguage::Ruby) {
+      measureTemplate = ":/templates/ModelMeasure/measure.rb";
+      testTemplate = ":/templates/ModelMeasure/tests/model_measure_test.rb";
+    } else if (measureLanguage == MeasureLanguage::Python) {
+      measureTemplate = ":/templates/ModelMeasure/measure.py";
+      testTemplate = ":/templates/ModelMeasure/tests/test_model_measure.py";
+    }
     testOSM = ":/templates/ModelMeasure/tests/example_model.osm";
     templateClassName = "ModelMeasureName";
 
@@ -224,8 +233,13 @@ BCLMeasure::BCLMeasure(const std::string& name, const std::string& className, co
     arguments.push_back(arg);
 
   } else if (measureType == MeasureType::EnergyPlusMeasure) {
-    measureTemplate = ":/templates/EnergyPlusMeasure/measure.rb";
-    testTemplate = ":/templates/EnergyPlusMeasure/tests/energyplus_measure_test.rb";
+    if (measureLanguage == MeasureLanguage::Ruby) {
+      measureTemplate = ":/templates/EnergyPlusMeasure/measure.rb";
+      testTemplate = ":/templates/EnergyPlusMeasure/tests/energyplus_measure_test.rb";
+    } else if (measureLanguage == MeasureLanguage::Python) {
+      measureTemplate = ":/templates/EnergyPlusMeasure/measure.py";
+      testTemplate = ":/templates/EnergyPlusMeasure/tests/test_energyplus_measure.py";
+    }
     templateClassName = "EnergyPlusMeasureName";
 
     std::string argName("zone_name");
@@ -242,11 +256,16 @@ BCLMeasure::BCLMeasure(const std::string& name, const std::string& className, co
     templateClassName = "UtilityMeasureName";
 
   } else if (measureType == MeasureType::ReportingMeasure) {
-    measureTemplate = ":/templates/ReportingMeasure/measure.rb";
-    testTemplate = ":/templates/ReportingMeasure/tests/reporting_measure_test.rb";
+    if (measureLanguage == MeasureLanguage::Ruby) {
+      measureTemplate = ":/templates/ReportingMeasure/measure.rb";
+      testTemplate = ":/templates/ReportingMeasure/tests/reporting_measure_test.rb";
+      resourceFile = ":/templates/ReportingMeasure/resources/report.html.in";
+    } else if (measureLanguage == MeasureLanguage::Python) {
+      measureTemplate = ":/templates/ReportingMeasure/measure.py";
+      testTemplate = ":/templates/ReportingMeasure/tests/test_reporting_measure.py";
+    }
     testOSM = ":/templates/ReportingMeasure/tests/example_model.osm";
     testEPW = ":/templates/ReportingMeasure/tests/USA_CO_Golden-NREL.724666_TMY3.epw";
-    resourceFile = ":/templates/ReportingMeasure/resources/report.html.in";
     templateClassName = "ReportingMeasureName";
 
     testOSMRelativePath = toPath("tests/example_model.osm");
@@ -322,11 +341,17 @@ BCLMeasure::BCLMeasure(const std::string& name, const std::string& className, co
     m_bclXML.addFile(fileRef);
   };
 
-  writeFile(toPath("measure.rb"), "script", measureString, true);
+  if (measureLanguage == MeasureLanguage::Ruby) {
+    writeFile(toPath("measure.rb"), "script", measureString, true);
+    writeFile(measureTestRelativeDir / toPath(lowerClassName + "_test.rb"), "test", testString);
+    writeFile(toPath("README.md.erb"), "readmeerb", readmeString);
+  } else if (measureLanguage == MeasureLanguage::Python) {
+    writeFile(toPath("measure.py"), "script", measureString, true);
+    writeFile(measureTestRelativeDir / toPath("test_" + lowerClassName + ".py"), "test", testString);
+  }
   writeFile(toPath("LICENSE.md"), "license", licenseString);
-  writeFile(toPath("README.md.erb"), "readmeerb", readmeString);
   writeFile(measureDocRelativeDir / toPath(".gitkeep"), "doc", docString);
-  writeFile(measureTestRelativeDir / toPath(lowerClassName + "_test.rb"), "test", testString);
+
   if (!testOSMString.empty()) {
     writeFile(testOSMRelativePath, "test", testOSMString);
   }
@@ -348,6 +373,7 @@ BCLMeasure::BCLMeasure(const std::string& name, const std::string& className, co
   m_bclXML.setOutputs(outputs);
   m_bclXML.addTag(taxonomyTag);
   this->setMeasureType(measureType);
+  this->setMeasureLanguage(measureLanguage);
 
   // reset the checksum to trigger update even if nothing has changed
   m_bclXML.resetXMLChecksum();
@@ -376,7 +402,16 @@ BCLMeasure::BCLMeasure(const openstudio::path& dir) : m_directory(openstudio::fi
   } else if (measureTypes.size() > 1) {
     LOG_AND_THROW("'" << toString(dir) << "' has multiple copies of required attribute \"Measure Type\"");
   } else if (measureTypes[0].valueType() != AttributeValueType::String) {
-    LOG_AND_THROW("'" << toString(dir) << "' has multiple copies of required attribute \"Measure Type\"");
+    LOG_AND_THROW("'" << toString(dir) << "' has wrong type for required attribute \"Measure Type\"");
+  }
+
+  std::vector<Attribute> measureLanguages = m_bclXML.getAttributes("Measure Language");
+  if (measureLanguages.empty()) {
+    // No-op
+  } else if (measureLanguages.size() > 1) {
+    LOG_AND_THROW("'" << toString(dir) << "' has multiple copies of required attribute \"Measure Language\"");
+  } else if (measureLanguages[0].valueType() != AttributeValueType::String) {
+    LOG_AND_THROW("'" << toString(dir) << "' has wrong type for required attribute \"Measure Language\"");
   }
 
   if (m_bclXML.xmlChecksum().empty()) {
@@ -683,6 +718,23 @@ void BCLMeasure::setMeasureType(const MeasureType& measureType) {
   m_bclXML.addAttribute(attribute);
 }
 
+MeasureLanguage BCLMeasure::measureLanguage() const {
+  std::vector<Attribute> measureLanguages = m_bclXML.getAttributes("Measure Language");
+  if (measureLanguages.empty()) {
+    // This was added at 3.5.0, when Python measures came in, so fall back to Ruby if not there
+    return MeasureLanguage::Ruby;
+  }
+  OS_ASSERT(measureLanguages.size() == 1);
+  return MeasureLanguage(measureLanguages[0].valueAsString());
+}
+
+void BCLMeasure::setMeasureLanguage(const MeasureLanguage& measureLanguage) {
+  const std::string attributeName("Measure Language");
+  Attribute attribute(attributeName, measureLanguage.valueName());
+  m_bclXML.removeAttributes(attributeName);
+  m_bclXML.addAttribute(attribute);
+}
+
 std::vector<std::string> BCLMeasure::intendedSoftwareTools() const {
   std::vector<std::string> result;
   std::vector<Attribute> attributes = m_bclXML.getAttributes("Intended Software Tool");
@@ -710,6 +762,18 @@ boost::optional<openstudio::path> BCLMeasure::primaryRubyScriptPath() const {
   return boost::none;
 }
 
+boost::optional<openstudio::path> BCLMeasure::primaryPythonScriptPath() const {
+  openstudio::path result = m_directory / toPath("measure.py");
+  if (exists(result) && is_regular_file(result)) {
+    return result;
+  }
+  return boost::none;
+}
+
+boost::optional<openstudio::path> BCLMeasure::primaryScriptPath() const {
+  return (measureLanguage() == MeasureLanguage::Ruby) ? primaryRubyScriptPath() : primaryPythonScriptPath();
+}
+
 FileReferenceType BCLMeasure::inputFileType() const {
   FileReferenceType result = FileReferenceType::Unknown;
   MeasureType measureType = this->measureType();
@@ -750,9 +814,14 @@ void BCLMeasure::incrementVersionId() {
   m_bclXML.incrementVersionId();
 }
 
-bool BCLMeasure::updateMeasureScript(const MeasureType& oldMeasureType, const MeasureType& newMeasureType, const std::string& oldClassName,
-                                     const std::string& newClassName, const std::string& name, const std::string& description,
-                                     const std::string& modelerDescription) {
+bool BCLMeasure::updateMeasureScript(const MeasureType& oldMeasureType, const MeasureType& newMeasureType, const MeasureLanguage& oldMeasureLanguage,
+                                     const MeasureLanguage& newMeasureLanguage, const std::string& oldClassName, const std::string& newClassName,
+                                     const std::string& name, const std::string& description, const std::string& modelerDescription) {
+  if (oldMeasureLanguage != newMeasureLanguage) {
+    LOG_AND_THROW("Cannot update a measure after a Language Change from " << oldMeasureLanguage.valueName() << " to "
+                                                                          << newMeasureLanguage.valueName());
+  }
+  // TODO: deal with Python
   boost::optional<openstudio::path> path = primaryRubyScriptPath();
   if (path && exists(*path)) {
 
@@ -961,7 +1030,7 @@ bool BCLMeasure::checkForUpdatesFiles() {
     openstudio::path absoluteFilePath = m_directory / relativeFilePath;
     if (openstudio::filesystem::exists(absoluteFilePath)) {
       bool thisResult = addWithUsageTypeIfNotExisting(relativeFilePath, std::string(usageType));
-      if (thisResult && (fileName == "measure.rb")) {
+      if (thisResult && ((fileName == "measure.rb") || (fileName == "measure.py"))) {
         // we don't know what the actual version this was created for, we also don't know minimum version
         filesToAdd.back().setSoftwareProgramVersion(openStudioVersion());
       }
diff --git a/src/utilities/bcl/BCLMeasure.hpp b/src/utilities/bcl/BCLMeasure.hpp
index 9546ec9a689..e1e05a15da1 100644
--- a/src/utilities/bcl/BCLMeasure.hpp
+++ b/src/utilities/bcl/BCLMeasure.hpp
@@ -72,6 +72,11 @@ OPENSTUDIO_ENUM( MeasureBadgeType,
   ((OSMeasure))
 );
 
+OPENSTUDIO_ENUM( MeasureLanguage,
+  ((Ruby))
+  ((Python))
+);
+
 // clang-format on
 
 /** BCLMeasure is a class for managing the contents of a BCL Measure directory including the xml description file.
@@ -86,7 +91,8 @@ class UTILITIES_API BCLMeasure
   /// Will throw exception unless dir is a directory that does not exist or an empty directory.
   /// A template measure will be created, including a blank user script.
   BCLMeasure(const std::string& name, const std::string& className, const openstudio::path& dir, const std::string& taxonomyTag,
-             MeasureType measureType, const std::string& description, const std::string& modelerDescription);
+             MeasureType measureType, const std::string& description, const std::string& modelerDescription,
+             MeasureLanguage measureLanguage = MeasureLanguage::Ruby);
 
   /// Constructor for downloaded measures, path is to directory containing "measure.xml" file
   /// Will throw exception if directory does not exist or does not represent a valid measure.
@@ -201,9 +207,13 @@ class UTILITIES_API BCLMeasure
   /// Returns value of the "Measure Type" attribute
   MeasureType measureType() const;
 
+  MeasureLanguage measureLanguage() const;
+
   /// Sets value of the "Measure Type" attribute
   void setMeasureType(const MeasureType& measureType);
 
+  void setMeasureLanguage(const MeasureLanguage& measureLanguage);
+
   /// Returns values of any "Intended Software Tool" attributes
   std::vector<std::string> intendedSoftwareTools() const;
 
@@ -214,6 +224,11 @@ class UTILITIES_API BCLMeasure
     *  a regular file. Otherwise returns boost::none. */
   boost::optional<openstudio::path> primaryRubyScriptPath() const;
 
+  boost::optional<openstudio::path> primaryPythonScriptPath() const;
+
+  /** Returns the path of the primary script path, based on MeasureLanguage **/
+  boost::optional<openstudio::path> primaryScriptPath() const;
+
   /// Returns the input file type for this measure
   FileReferenceType inputFileType() const;
 
@@ -274,9 +289,9 @@ class UTILITIES_API BCLMeasure
 
   /// Update's the ruby measure script to have this new information
   // cppcheck-suppress functionConst
-  bool updateMeasureScript(const MeasureType& oldMeasureType, const MeasureType& newMeasureType, const std::string& oldClassName,
-                           const std::string& newClassName, const std::string& name, const std::string& description,
-                           const std::string& modelerDescription);
+  bool updateMeasureScript(const MeasureType& oldMeasureType, const MeasureType& newMeasureType, const MeasureLanguage& oldMeasureLanguage,
+                           const MeasureLanguage& newMeasureLanguage, const std::string& oldClassName, const std::string& newClassName,
+                           const std::string& name, const std::string& description, const std::string& modelerDescription);
 
   /// Update's the ruby measure tests to have this new information
   // cppcheck-suppress functionConst
diff --git a/src/utilities/bcl/LocalBCL.i b/src/utilities/bcl/LocalBCL.i
index 85eaaa21aab..476609df443 100644
--- a/src/utilities/bcl/LocalBCL.i
+++ b/src/utilities/bcl/LocalBCL.i
@@ -86,6 +86,7 @@
 %template(PugixmldocPtr) std::shared_ptr<pugi::xml_document>;
 
 %template(OptionalMeasureType) boost::optional<openstudio::MeasureType>;
+%template(OptionalMeasureLanguage) boost::optional<openstudio::MeasureLanguage>;
 
 %include <utilities/bcl/BCLFileReference.hpp>
 %include <utilities/bcl/BCLComponent.hpp>
diff --git a/src/utilities/bcl/templates/EnergyPlusMeasure/measure.py b/src/utilities/bcl/templates/EnergyPlusMeasure/measure.py
new file mode 100644
index 00000000000..c22e0446a1f
--- /dev/null
+++ b/src/utilities/bcl/templates/EnergyPlusMeasure/measure.py
@@ -0,0 +1,112 @@
+"""insert your copyright here.
+
+# see the URL below for information on how to write OpenStudio measures
+# http://nrel.github.io/OpenStudio-user-documentation/reference/measure_writing_guide/
+"""
+
+import openstudio
+
+
+class EnergyPlusMeasureName(openstudio.measure.EnergyPlusMeasure):
+    """An EnergyPlusMeasure."""
+
+    def name(self):
+        """Returns the human readable name.
+
+        Measure name should be the title case of the class name.
+        The measure name is the first contact a user has with the measure;
+        it is also shared throughout the measure workflow, visible in the OpenStudio Application,
+        PAT, Server Management Consoles, and in output reports.
+        As such, measure names should clearly describe the measure's function,
+        while remaining general in nature
+        """
+        return "NAME_TEXT"
+
+    def description(self):
+        """Human readable description.
+
+        The measure description is intended for a general audience and should not assume
+        that the reader is familiar with the design and construction practices suggested by the measure.
+        """
+        return "DESCRIPTION_TEXT"
+
+    def modeler_description(self):
+        """Human readable description of modeling approach.
+
+        The modeler description is intended for the energy modeler using the measure.
+        It should explain the measure's intent, and include any requirements about
+        how the baseline model must be set up, major assumptions made by the measure,
+        and relevant citations or references to applicable modeling resources
+        """
+        return "MODELER_DESCRIPTION_TEXT"
+
+    def arguments(self, workspace: openstudio.Workspace):
+        """Prepares user arguments for the measure.
+
+        Measure arguments define which -- if any -- input parameters the user may set before running the measure.
+        """
+        args = openstudio.measure.OSArgumentVector()
+
+        zone_name = openstudio.measure.OSArgument.makeStringArgument("zone_name", True)
+        zone_name.setDisplayName("New zone name")
+        zone_name.setDescription("This name will be used as the name of the new zone.")
+        args.append(zone_name)
+
+        return args
+
+    def run(
+        self,
+        workspace: openstudio.Workspace,
+        runner: openstudio.measure.OSRunner,
+        user_arguments: openstudio.measure.OSArgumentMap,
+    ):
+        """Defines what happens when the measure is run."""
+        super().run(workspace, runner, user_arguments)  # Do **NOT** remove this line
+
+        if not (runner.validateUserArguments(self.arguments(workspace), user_arguments)):
+            return False
+
+        # assign the user inputs to variables
+        zone_name = runner.getStringArgumentValue("zone_name", user_arguments)
+
+        # check the zone_name for reasonableness
+        if not zone_name:
+            runner.registerError("Empty zone name was entered.")
+            return False
+
+        # get all thermal zones in the starting workspace
+        zones = workspace.getObjectsByType("Zone")
+        # report initial condition of workspace
+        runner.registerInitialCondition(f"The building started with {len(zones)} zones.")
+
+        # add a new zone to the model with the new name
+        # http://apps1.eere.energy.gov/buildings/energyplus/pdfs/inputoutputreference.pdf#nameddest=Zone
+        new_zone_string = f"""
+  Zone,
+    {zone_name},             !- Name
+    0,                       !- Direction of Relative North {{deg}}
+    0,                       !- X Origin {{m}}
+    0,                       !- Y Origin {{m}}
+    0,                       !- Z Origin {{m}}
+    1,                       !- Type
+    1,                       !- Multiplier
+    autocalculate,           !- Ceiling Height {{m}}
+    autocalculate;           !- Volume {{m3}}
+          """
+
+        idfObject_: openstudio.OptionalIdfObject = openstudio.IdfObject.load(new_zone_string)
+        idfObject: openstudio.IdfObject = idfObject_.get()
+        wsObject: openstudio.OptionalWorkspaceObject = workspace.addObject(idfObject)
+        new_zone: openstudio.WorkspaceObject = wsObject.get()
+
+        runner.registerInfo(f"A zone named '{new_zone.nameString()}' was added.")
+
+        # report final condition of model
+        finishing_zones = workspace.getObjectsByType("Zone")
+        runner.registerFinalCondition(f"The building finished with {len(finishing_zones)} zones.")
+
+        return True
+
+
+# register the measure to be used by the application
+EnergyPlusMeasureName().registerWithApplication()
diff --git a/src/utilities/bcl/templates/EnergyPlusMeasure/measure.rb b/src/utilities/bcl/templates/EnergyPlusMeasure/measure.rb
index 4d16f83a87e..75d70d268ec 100644
--- a/src/utilities/bcl/templates/EnergyPlusMeasure/measure.rb
+++ b/src/utilities/bcl/templates/EnergyPlusMeasure/measure.rb
@@ -36,7 +36,7 @@ def arguments(workspace)
 
   # define what happens when the measure is run
   def run(workspace, runner, user_arguments)
-    super(workspace, runner, user_arguments)
+    super(workspace, runner, user_arguments)  # Do **NOT** remove this line
 
     # use the built-in error checking
     if !runner.validateUserArguments(arguments(workspace), user_arguments)
diff --git a/src/utilities/bcl/templates/ModelMeasure/measure.py b/src/utilities/bcl/templates/ModelMeasure/measure.py
new file mode 100644
index 00000000000..083b0009a9b
--- /dev/null
+++ b/src/utilities/bcl/templates/ModelMeasure/measure.py
@@ -0,0 +1,95 @@
+"""insert your copyright here.
+
+# see the URL below for information on how to write OpenStudio measures
+# http://nrel.github.io/OpenStudio-user-documentation/reference/measure_writing_guide/
+"""
+
+import openstudio
+import typing
+
+
+class ModelMeasureName(openstudio.measure.ModelMeasure):
+    """A ModelMeasure."""
+
+    def name(self):
+        """Returns the human readable name.
+
+        Measure name should be the title case of the class name.
+        The measure name is the first contact a user has with the measure;
+        it is also shared throughout the measure workflow, visible in the OpenStudio Application,
+        PAT, Server Management Consoles, and in output reports.
+        As such, measure names should clearly describe the measure's function,
+        while remaining general in nature
+        """
+        return "NAME_TEXT"
+
+    def description(self):
+        """Human readable description.
+
+        The measure description is intended for a general audience and should not assume
+        that the reader is familiar with the design and construction practices suggested by the measure.
+        """
+        return "DESCRIPTION_TEXT"
+
+    def modeler_description(self):
+        """Human readable description of modeling approach.
+
+        The modeler description is intended for the energy modeler using the measure.
+        It should explain the measure's intent, and include any requirements about
+        how the baseline model must be set up, major assumptions made by the measure,
+        and relevant citations or references to applicable modeling resources
+        """
+        return "MODELER_DESCRIPTION_TEXT"
+
+    def arguments(self, model: typing.Optional[openstudio.model.Model] = None):
+        """Prepares user arguments for the measure.
+
+        Measure arguments define which -- if any -- input parameters the user may set before running the measure.
+        """
+        args = openstudio.measure.OSArgumentVector()
+
+        example_arg = openstudio.measure.OSArgument.makeStringArgument("space_name", True)
+        example_arg.setDisplayName("New space name")
+        example_arg.setDescription("This name will be used as the name of the new space.")
+        args.append(example_arg)
+
+        return args
+
+    def run(
+        self,
+        model: openstudio.model.Model,
+        runner: openstudio.measure.OSRunner,
+        user_arguments: openstudio.measure.OSArgumentMap,
+    ):
+        """Defines what happens when the measure is run."""
+        super().run(model, runner, user_arguments)  # Do **NOT** remove this line
+
+        if not (runner.validateUserArguments(self.arguments(model), user_arguments)):
+            return False
+
+        # assign the user inputs to variables
+        space_name = runner.getStringArgumentValue("space_name", user_arguments)
+
+        # check the example_arg for reasonableness
+        if not space_name:
+            runner.registerError("Empty space name was entered.")
+            return False
+
+        # report initial condition of model
+        runner.registerInitialCondition(f"The building started with {len(model.getSpaces())} spaces.")
+
+        # add a new space to the model
+        new_space = openstudio.model.Space(model)
+        new_space.setName(space_name)
+
+        # echo the new space's name back to the user
+        runner.registerInfo(f"Space {new_space.nameString()} was added.")
+
+        # report final condition of model
+        runner.registerFinalCondition(f"The building finished with {len(model.getSpaces())} spaces.")
+
+        return True
+
+
+# register the measure to be used by the application
+ModelMeasureName().registerWithApplication()
diff --git a/src/utilities/bcl/templates/ModelMeasure/measure.rb b/src/utilities/bcl/templates/ModelMeasure/measure.rb
index d5e31b571ca..3afb27feb19 100644
--- a/src/utilities/bcl/templates/ModelMeasure/measure.rb
+++ b/src/utilities/bcl/templates/ModelMeasure/measure.rb
@@ -36,7 +36,7 @@ def arguments(model)
 
   # define what happens when the measure is run
   def run(model, runner, user_arguments)
-    super(model, runner, user_arguments)
+    super(model, runner, user_arguments)  # Do **NOT** remove this line
 
     # use the built-in error checking
     if !runner.validateUserArguments(arguments(model), user_arguments)
diff --git a/src/utilities/bcl/templates/ReportingMeasure/measure.py b/src/utilities/bcl/templates/ReportingMeasure/measure.py
new file mode 100644
index 00000000000..50230cacfce
--- /dev/null
+++ b/src/utilities/bcl/templates/ReportingMeasure/measure.py
@@ -0,0 +1,383 @@
+"""insert your copyright here.
+
+# see the URL below for information on how to write OpenStudio measures
+# http://nrel.github.io/OpenStudio-user-documentation/reference/measure_writing_guide/
+"""
+
+import openstudio
+from pathlib import Path
+
+
+class ReportingMeasureName(openstudio.measure.ReportingMeasure):
+    """An ReportingMeasure."""
+
+    def name(self):
+        """Returns the human readable name.
+
+        Measure name should be the title case of the class name.
+        The measure name is the first contact a user has with the measure;
+        it is also shared throughout the measure workflow, visible in the OpenStudio Application,
+        PAT, Server Management Consoles, and in output reports.
+        As such, measure names should clearly describe the measure's function,
+        while remaining general in nature
+        """
+        return "NAME_TEXT"
+
+    def description(self):
+        """Human readable description.
+
+        The measure description is intended for a general audience and should not assume
+        that the reader is familiar with the design and construction practices suggested by the measure.
+        """
+        return "DESCRIPTION_TEXT"
+
+    def modeler_description(self):
+        """Human readable description of modeling approach.
+
+        The modeler description is intended for the energy modeler using the measure.
+        It should explain the measure's intent, and include any requirements about
+        how the baseline model must be set up, major assumptions made by the measure,
+        and relevant citations or references to applicable modeling resources
+        """
+        return "MODELER_DESCRIPTION_TEXT"
+
+    def arguments(self, model: openstudio.model.Model):
+        """Prepares user arguments for the measure.
+
+        Measure arguments define which -- if any -- input parameters the user may set before running the measure.
+        """
+        args = openstudio.measure.OSArgumentVector()
+
+        report_drybulb_temp = openstudio.measure.OSArgument.makeBoolArgument("report_drybulb_temp", True)
+        report_drybulb_temp.setDisplayName("Add output variables for Drybulb Temperature")
+        report_drybulb_temp.setDescription('Will add drybulb temp and report min/mix value in html.')
+        report_drybulb_temp.setValue(True)
+        args.append(report_drybulb_temp)
+
+        return args
+
+    def outputs(self):
+        """Define the outputs that the measure will create."""
+        outs = openstudio.measure.OSOutputVector()
+
+        # this measure does not produce machine readable outputs with registerValue, return an empty list
+
+        return outs
+
+    def energyPlusOutputRequests(
+        self,
+        runner: openstudio.measure.OSRunner,
+        user_arguments: openstudio.measure.OSArgumentMap
+    ):
+        """Returns a vector of IdfObject's to request EnergyPlus objects needed by the run method."""
+        super().energyPlusOutputRequests(runner, user_arguments)  # Do **NOT** remove this line
+
+        result = openstudio.IdfObjectVector()
+
+        # To use the built-in error checking we need the model...
+        # get the last model and sql file
+        model = runner.lastOpenStudioModel()
+        if not model.is_initialized():
+            runner.registerError('Cannot find last model.')
+            return False
+
+        model = model.get()
+
+        # use the built-in error checking
+        if not runner.validateUserArguments(self.arguments(model), user_arguments):
+            return False
+
+        if runner.getBoolArgumentValue('report_drybulb_temp', user_arguments):
+            request = openstudio.IdfObject.load(
+                'Output:Variable, , Site Outdoor Air Drybulb Temperature, Hourly;').get()
+            result.append(request)
+
+        return result
+
+    def run(
+        self,
+        runner: openstudio.measure.OSRunner,
+        user_arguments: openstudio.measure.OSArgumentMap,
+    ):
+        """Defines what happens when the measure is run."""
+        super().run(runner, user_arguments)
+
+        # get the last model and sql file
+        model = runner.lastOpenStudioModel()
+        if not model.is_initialized():
+            runner.registerError('Cannot find last model.')
+            return False
+        model = model.get()
+
+        # use the built-in error checking (need model)
+        if not runner.validateUserArguments(self.arguments(model), user_arguments):
+            return False
+
+        # get measure arguments
+        report_drybulb_temp = runner.getBoolArgumentValue('report_drybulb_temp', user_arguments)
+
+        # load sql file
+        sql_file = runner.lastEnergyPlusSqlFile()
+        if not sql_file.is_initialized():
+            runner.registerError('Cannot find last sql file.')
+            return False
+
+        sql_file = sql_file.get()
+        model.setSqlFile(sql_file)
+
+        style_str = """/* Style the titles */
+      #pagetitle {
+        text-align: center;
+        font-size: 32px;
+        font-weight: 700;
+        color: #E2231A; /* #1b1e24; */
+        font-variant: small-caps;
+        border-top: none;
+      }
+
+      h1 {
+        font-size: 26px;
+        font-weight: 700;
+        color: #3e8ddd; /* #1b1e24; */
+        font-variant: small-caps;
+        border-top: 1px solid #000;
+
+      }
+
+      h2 {
+        font-size: 20px;
+        color: #428BCA;
+      }
+"""
+        output = f"""
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <!-- Required meta tags -->
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+
+    <title>ReportingMeasure</title>
+    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
+
+    <script type="text/javascript" src="http://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
+    <script type="text/javascript" src="http://cdnjs.cloudflare.com/ajax/libs/d3/7.6.1/d3.min.js"></script>
+
+    <style>
+      {style_str}
+    </style>
+  </head>
+  <body>
+
+    <div id="content" class="container-fluid">
+      <div class="row">
+
+        <div id="topofpage" class="col-md-12" role="main">
+          <h1 id="pagetitle">Reporting Measure Example</h1>
+        </div>  <!-- End of first main -->
+      </div> <!-- End of first row (toc) -->
+
+      <div class="row">
+        <h2>Basic HTML</h2>
+        <p>Measure Name = {self.name()}<br/>
+           Building Name = {model.getBuilding().nameString()}<br/>
+           Floor Area = {model.getBuilding().floorArea():.0f} m<sup>2</sup><br/>
+           Net Site Energy = {sql_file.netSiteEnergy().get():.2f} GJ<br/>
+        </p>
+      </div>
+
+      <div class="row">
+        <h2>Example Table</h2>
+        <table class="table table-sm table-hover table-striped">
+          <thead>
+            <tr>
+              <th scope="col">#</th>
+              <th scope="col">Value</th>
+              <th scope="col">Units</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr>
+              <th scope="row">Measure Name</th>
+              <td>{self.name()}</td>
+              <td></td>
+            </tr>
+            <tr>
+              <th scope="row">Building Name</th>
+              <td>{model.getBuilding().nameString()}</td>
+              <td></td>
+            </tr>
+            <tr>
+              <th scope="row">Floor Area</th>
+              <td>{model.getBuilding().floorArea():.0f}</td>
+              <td>m<sup>2</sup></td>
+            </tr>
+            <tr>
+              <th scope="row">Net Site Energy</th>
+              <td>{sql_file.netSiteEnergy().get():.2f}</td>
+              <td>GJ</td>
+            </tr>
+"""
+
+        if report_drybulb_temp:
+            # get the weather file run period (as opposed to design day run period)
+            ann_env_pd = None
+            for env_pd in sql_file.availableEnvPeriods():
+                env_type = sql_file.environmentType(env_pd)
+                if not env_type.is_initialized():
+                    continue
+                if env_type.get() == openstudio.EnvironmentType('WeatherRunPeriod'):
+                    ann_env_pd = env_pd
+                    break
+            if ann_env_pd is None:
+                runner.registerWarning('No annual environment period found.')
+            else:
+                # get desired variable
+                key_value = 'Environment'
+                time_step = 'Hourly'  # "Zone Timestep", "Hourly", "HVAC System Timestep"
+                variable_name = 'Site Outdoor Air Drybulb Temperature'
+
+                # key value would go at the end if we used it.
+                output_timeseries = sql_file.timeSeries(ann_env_pd, time_step, variable_name, key_value)
+                if not output_timeseries.is_initialized():
+                    runner.registerWarning('Timeseries not found.')
+                else:
+                    runner.registerInfo('Found timeseries.')
+
+                    output_timeseries = output_timeseries.get()
+                    values = output_timeseries.values()
+                    vals = [values[i] for i in range(values.size())]
+                    times = output_timeseries.dateTimes()
+
+                    data_lines = []
+
+                    def format_dt(dt):
+                        d = dt.date()
+                        t = dt.time()
+                        # return datetime.datetime(d.year(), d.monthOfYear().value(), d.dayOfMonth(), t.hours(), t.minutes(), t.seconds(), tzinfo=datetime.timezone.utc)
+                        return f"new Date({d.year()}, {d.monthOfYear().value()}, {d.dayOfMonth()}, {t.hours()}, {t.minutes()}, {t.seconds()})"
+
+                    for i, v in enumerate(vals):
+                        data_lines.append(f"{{date: {format_dt(times[i])}, value: {v}}}")
+                    date_lines_str = ",\n        ".join(data_lines)
+
+                    output += f"""
+            <tr>
+              <th scope="row">Minimum Site Outdoor Air Drybulb Temperature</th>
+              <td>{min(vals):.1f}</td>
+              <td>C</td>
+            </tr>
+            <tr>
+              <th scope="row">Maximum Site Outdoor Air Drybulb Temperature</th>
+              <td>{min(vals):.1f}</td>
+              <td>C</td>
+            </tr>
+         </tbody>
+        </table>
+      </div> <!-- End table row -->
+
+      <div class="row">
+        <h2>Site Outdoor Air Drybulb Temperature</h2>
+        <div id="chartArea" class="col-xs-12 col-lg-6"></div>
+      </div>
+    </div> <!-- End main container fluid -->
+
+    <script>
+
+    var data = [
+        {date_lines_str}
+    ];
+"""
+                    output += """
+    // var data = [
+    //     { date: new Date(2013, 17, 1), value: 1 },
+    //     { date: new Date(2013, 17, 2), value: 2 },
+    //     { date: new Date(2013, 17, 3), value: 3 },
+    // ];
+
+    // set the dimensions and margins of the graph
+    var margin = {top: 10, right: 30, bottom: 30, left: 60};
+    // var width = 460 - margin.left - margin.right;
+    var width = document.getElementById('chartArea').clientWidth;
+    // var height = 400 - margin.top - margin.bottom;
+    // golden rectangle ratio.
+    var height = width / 3.236;
+
+    // append the svg object to the body of the page
+    var svg = d3.select("#chartArea")
+      .append("svg")
+        .attr("width", width + margin.left + margin.right)
+        .attr("height", height + margin.top + margin.bottom)
+      .append("g")
+        .attr("transform",
+              "translate(" + margin.left + "," + margin.top + ")");
+
+    //We will build a basic function to handle window resizing.
+    function resize() {
+        width = document.getElementById('chartArea').clientWidth;
+        height = width / 3.236;
+        d3.select('#chartArea svg')
+          .attr('width', width)
+          .attr('height', height);
+    }
+
+    window.onresize = resize;
+
+    //Call our resize function if the window size is changed.
+
+    // Add X axis --> it is a date format
+    var x = d3.scaleTime()
+      .domain(d3.extent(data, function(d) { return d.date; }))
+      .range([ 0, width ]);
+    svg.append("g")
+      .attr("transform", "translate(0," + height + ")")
+      .call(d3.axisBottom(x));
+
+    // Add Y axis
+    var y = d3.scaleLinear()
+      .domain([0, d3.max(data, function(d) { return +d.value; })])
+      .range([ height, 0 ]);
+    svg.append("g")
+      .call(d3.axisLeft(y));
+
+    // Add the line
+    svg.append("path")
+      .datum(data)
+      .attr("fill", "none")
+      .attr("stroke", "steelblue")
+      .attr("stroke-width", 1.5)
+      .attr("d", d3.line()
+        .x(function(d) { return x(d.date) })
+        .y(function(d) { return y(d.value) })
+        )
+
+    </script>
+  </body>
+</html>
+"""
+        else:
+            output += """
+
+         </tbody>
+        </table>
+      </div> <!-- End table row -->
+
+    </div> <!-- End main container fluid -->
+
+  </body>
+</html>"""
+
+        # write html file: any file named 'report*.*' in the current working directory
+        # will be copied to the ./reports/ folder as 'reports/<measure_class_name>_<filename>.html'
+        html_out_path = Path('./report.html').absolute()
+        with open(html_out_path, 'w') as f:
+            f.write(output)
+
+        # Close the sql file
+        sql_file.close()
+
+        return True
+
+
+# register the measure to be used by the application
+ReportingMeasureName().registerWithApplication()
diff --git a/src/utilities/bcl/templates/ReportingMeasure/measure.rb b/src/utilities/bcl/templates/ReportingMeasure/measure.rb
index e40fcaf9066..ece24e3fd1d 100644
--- a/src/utilities/bcl/templates/ReportingMeasure/measure.rb
+++ b/src/utilities/bcl/templates/ReportingMeasure/measure.rb
@@ -49,7 +49,7 @@ def outputs
   # return a vector of IdfObject's to request EnergyPlus objects needed by the run method
   # Warning: Do not change the name of this method to be snake_case. The method must be lowerCamelCase.
   def energyPlusOutputRequests(runner, user_arguments)
-    super(runner, user_arguments)
+    super(runner, user_arguments)  # Do **NOT** remove this line
 
     result = OpenStudio::IdfObjectVector.new
 
diff --git a/src/utilities/core/ApplicationPathHelpers.cxx.in b/src/utilities/core/ApplicationPathHelpers.cxx.in
index 6003a75ae3b..fbb01556dd0 100644
--- a/src/utilities/core/ApplicationPathHelpers.cxx.in
+++ b/src/utilities/core/ApplicationPathHelpers.cxx.in
@@ -33,6 +33,7 @@
 
 #include <OpenStudio.hxx>
 
+#include <fmt/format.h>
 #include <sstream>
 #include <boost/regex.hpp>
 #include <boost/algorithm/string.hpp>
@@ -467,4 +468,16 @@ namespace openstudio {
     return openstudio::path();
   }
 
+  std::string_view getSharedModuleSuffix() {
+    return "${CMAKE_SHARED_MODULE_SUFFIX}";
+  }
+  
+  std::string_view getSharedModulePrefix() {
+    return "${CMAKE_SHARED_MODULE_PREFIX}";
+  }
+
+  std::string getSharedModuleName(std::string_view basename) {
+    return fmt::format("{}{}{}", getSharedModulePrefix(), basename, getSharedModuleSuffix());
+  }
+
 } // openstudio
diff --git a/src/utilities/core/ApplicationPathHelpers.hpp b/src/utilities/core/ApplicationPathHelpers.hpp
index aff99a35cd3..bb15623dc5e 100644
--- a/src/utilities/core/ApplicationPathHelpers.hpp
+++ b/src/utilities/core/ApplicationPathHelpers.hpp
@@ -75,6 +75,18 @@ UTILITIES_API openstudio::path getRadianceDirectory();
 /// \returns The path to the Perl executable if it exists.
 UTILITIES_API openstudio::path getPerlExecutable();
 
+/// \returns The platform specific suffix for shared modules.
+/// "dll" on Windows, ".so" on Linux, etc.
+UTILITIES_API std::string_view getSharedModuleSuffix();
+
+/// \returns The platform specific file prefix for shared modules.
+/// "lib" on Linux.
+UTILITIES_API std::string_view getSharedModulePrefix();
+
+/// \returns The platform specific shared module name,
+/// concatenates the prefix and suffix to the given basename
+UTILITIES_API std::string getSharedModuleName(std::string_view basename);
+
 }  // namespace openstudio
 
 #endif  //UTILITIES_CORE_PATHHELPERS_HPP
diff --git a/src/utilities/core/DynamicLibrary.hpp b/src/utilities/core/DynamicLibrary.hpp
new file mode 100644
index 00000000000..1afe3292ca6
--- /dev/null
+++ b/src/utilities/core/DynamicLibrary.hpp
@@ -0,0 +1,5 @@
+#if defined(_MSC_VER)
+#  include "DynamicLibraryWindows.hpp"
+#elif defined(HAVE_UNISTD_H)
+#  include "DynamicLibraryPOSIX.hpp"
+#endif
diff --git a/src/utilities/core/DynamicLibraryPOSIX.hpp b/src/utilities/core/DynamicLibraryPOSIX.hpp
new file mode 100644
index 00000000000..2415968eacb
--- /dev/null
+++ b/src/utilities/core/DynamicLibraryPOSIX.hpp
@@ -0,0 +1,47 @@
+#ifndef dynamiclibrary_posix_hpp_INCLUDED
+#define dynamiclibrary_posix_hpp_INCLUDED
+
+#include "Path.hpp"
+
+#include <stdexcept>
+#include <string>
+
+#include <dlfcn.h>
+
+#include <fmt/format.h>
+
+namespace openstudio {
+
+struct DynamicLibrary
+{
+  template <typename Signature>
+  [[nodiscard]] Signature* load_symbol(const std::string& name) {
+    // reinterpret_cast is necessary here
+    const auto symbol = reinterpret_cast<Signature*>(dlsym(m_handle.get(), name.c_str()));  // NOLINT
+
+    if (symbol == nullptr) {
+      throw std::runtime_error(fmt::format("Unable to load symbol: '{}', reason: '{}'", name, dlerror()));
+    }
+
+    return symbol;
+  }
+
+  explicit DynamicLibrary(openstudio::path location)
+    : m_location{std::move(location)}, m_handle{dlopen(m_location.c_str(), RTLD_LAZY | RTLD_LOCAL), m_handle_deleter} {
+    if (!m_handle) {
+      throw std::runtime_error(fmt::format("Unable to load library '{}', reason: '{}'", m_location.string(), dlerror()));
+    }
+  }
+
+  static void m_handle_deleter(void* h) {
+    if (h) {
+      dlclose(h);
+    }
+  }
+
+  openstudio::path m_location{};
+  std::unique_ptr<void, decltype(&m_handle_deleter)> m_handle{nullptr, m_handle_deleter};
+};
+}  // namespace openstudio
+
+#endif  // dynamiclibrary_posix_hpp_INCLUDED
diff --git a/src/utilities/core/DynamicLibraryWindows.hpp b/src/utilities/core/DynamicLibraryWindows.hpp
new file mode 100644
index 00000000000..be9f7391da1
--- /dev/null
+++ b/src/utilities/core/DynamicLibraryWindows.hpp
@@ -0,0 +1,113 @@
+#ifndef dynamiclibrary_windows_hpp_INCLUDED
+#define dynamiclibrary_windows_hpp_INCLUDED
+
+#include "Path.hpp"
+
+#include <stdexcept>
+#include <filesystem>
+#include <string>
+
+#if !defined(WIN32_LEAN_AND_MEAN)
+#  define WIN32_LEAN_AND_MEAN
+#  include <windows.h>
+#  undef WIN32_LEAN_AND_MEAN
+#else
+#  include <windows.h>
+#endif
+
+#include <fmt/format.h>
+
+namespace openstudio {
+
+struct DynamicLibrary
+{
+
+  template <typename T>
+  static [[nodiscard]] std::wstring to_wstring(const T& t_str) {
+    return std::wstring(t_str.begin(), t_str.end());
+  }
+
+  template <typename T>
+  static [[nodiscard]] std::string to_string(const T& t_str) {
+    return std::string(t_str.begin(), t_str.end());
+  }
+
+#if defined(_UNICODE) || defined(UNICODE)
+  template <typename T>
+  static std::wstring to_proper_string(const T& t_str) {
+    return to_wstring(t_str);
+  }
+#else
+  template <typename T>
+  static std::string [[nodiscard]] to_proper_string(const T& t_str) {
+    return to_string(t_str);
+  }
+#endif
+
+  static std::string [[nodiscard]] get_error_message(DWORD t_err) {
+    typedef LPTSTR StringType;
+
+#if defined(_UNICODE) || defined(UNICODE)
+    std::wstring retval = L"Unknown Error";
+#else
+    std::string retval = "Unknown Error";
+#endif
+    StringType lpMsgBuf = nullptr;
+
+    // TODO this reinterpret_cast is suspicious (implementation borrowed from ChaiScript)
+    if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, t_err,
+                      MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), reinterpret_cast<StringType>(&lpMsgBuf), 0, nullptr)
+          != 0
+        && lpMsgBuf) {
+      retval = lpMsgBuf;
+      LocalFree(lpMsgBuf);
+    }
+
+    return to_string(retval);
+  }
+
+  template <typename Signature>
+  [[nodiscard]] Signature* load_symbol(const std::string& name) {
+    // reinterpret_cast is necessary here
+    const auto symbol = reinterpret_cast<Signature*>(GetProcAddress(m_handle, name.c_str()));
+
+    if (symbol == nullptr) {
+      throw std::runtime_error(fmt::format("Unable to load symbol: '{}', reason: '{}'", name, get_error_message(GetLastError())));
+    }
+
+    return symbol;
+  }
+
+  explicit DynamicLibrary(openstudio::path location)
+    : m_location{std::move(location)}, m_handle{LoadLibrary(to_proper_string(m_location.string()).c_str())} {
+    if (!m_handle) {
+      throw std::runtime_error(fmt::format("Unable to load library '{}', reason: '{}'", m_location.string(), get_error_message(GetLastError())));
+    }
+  }
+
+  DynamicLibrary() = delete;
+  DynamicLibrary(DynamicLibrary&& other) noexcept : m_handle{other.m_handle} {
+    other.m_handle = HMODULE{};
+  }
+  DynamicLibrary(const DynamicLibrary&) = delete;
+  DynamicLibrary& operator=(DynamicLibrary&& other) noexcept {
+    if (m_handle) {
+      FreeLibrary(m_handle);
+    }
+    m_handle = std::exchange(other.m_handle, HMODULE{});
+    return *this;
+  }
+  DynamicLibrary& operator=(const DynamicLibrary&) = delete;
+
+  ~DynamicLibrary() noexcept {
+    if (m_handle) {
+      FreeLibrary(m_handle);
+    }
+  }
+
+  openstudio::path m_location{};
+  HMODULE m_handle{};
+};
+
+}  // namespace openstudio
+#endif  // dynamiclibrary_windows_hpp_INCLUDED
diff --git a/src/utilities/filetypes/RunOptions_Impl.hpp b/src/utilities/filetypes/RunOptions_Impl.hpp
index 1f96a35d806..2ad2a122d82 100644
--- a/src/utilities/filetypes/RunOptions_Impl.hpp
+++ b/src/utilities/filetypes/RunOptions_Impl.hpp
@@ -43,6 +43,18 @@
 namespace openstudio {
 namespace detail {
 
+  class FtOptions
+  {
+   public:
+    bool runcontrolspecialdays = true;
+    bool ip_tabular_output = false;
+    bool no_lifecyclecosts = false;
+    bool no_sqlite_output = false;
+    bool no_html_output = false;
+    bool no_variable_dictionary = false;
+    bool no_space_translation = false;
+  };
+
   class UTILITIES_API RunOptions_Impl
   {
    public:
diff --git a/src/utilities/filetypes/WorkflowStepResult.cpp b/src/utilities/filetypes/WorkflowStepResult.cpp
index 73d166c2c0c..d5fa6477c94 100644
--- a/src/utilities/filetypes/WorkflowStepResult.cpp
+++ b/src/utilities/filetypes/WorkflowStepResult.cpp
@@ -34,6 +34,8 @@
 
 #include <json/json.h>
 
+#include <utility>
+
 namespace openstudio {
 namespace detail {
 
@@ -129,7 +131,7 @@ namespace detail {
     m_units.reset();
   }
 
-  WorkflowStepResult_Impl::WorkflowStepResult_Impl() {}
+  WorkflowStepResult_Impl::WorkflowStepResult_Impl() = default;
 
   std::string WorkflowStepResult_Impl::string() const {
     Json::Value value(Json::objectValue);
@@ -196,7 +198,7 @@ namespace detail {
       value["step_final_condition"] = stepFinalCondition().get();
     }
 
-    if (complete || (stepErrors().size() > 0)) {
+    if (complete || (!stepErrors().empty())) {
       Json::Value errors(Json::arrayValue);
       for (const auto& stepError : stepErrors()) {
         errors.append(stepError);
@@ -204,7 +206,7 @@ namespace detail {
       value["step_errors"] = errors;
     }
 
-    if (complete || (stepWarnings().size() > 0)) {
+    if (complete || (!stepWarnings().empty())) {
       Json::Value warnings(Json::arrayValue);
       for (const auto& stepWarning : stepWarnings()) {
         warnings.append(stepWarning);
@@ -212,7 +214,7 @@ namespace detail {
       value["step_warnings"] = warnings;
     }
 
-    if (complete || (stepInfo().size() > 0)) {
+    if (complete || (!stepInfo().empty())) {
       Json::Value info(Json::arrayValue);
       for (const auto& stepI : stepInfo()) {
         info.append(stepI);
@@ -220,7 +222,7 @@ namespace detail {
       value["step_info"] = info;
     }
 
-    if (complete || (stepValues().size() > 0)) {
+    if (complete || (!stepValues().empty())) {
       Json::Value values(Json::arrayValue);
       for (const auto& stepValue : stepValues()) {
 
@@ -240,7 +242,7 @@ namespace detail {
       value["step_values"] = values;
     }
 
-    if (complete || (stepFiles().size() > 0)) {
+    if (complete || (!stepFiles().empty())) {
       Json::Value files(Json::arrayValue);
       for (const auto& stepFile : stepFiles()) {
         files.append(toString(stepFile));
@@ -379,7 +381,7 @@ namespace detail {
     LOG(Debug, "WorkflowStepResult::errors is deprecated, use stepErrors instead");
     std::vector<LogMessage> result;
     for (const auto& stepError : stepErrors()) {
-      result.push_back(LogMessage(Error, "", stepError));
+      result.emplace_back(Error, "", stepError);
     }
     return result;
   }
@@ -388,7 +390,7 @@ namespace detail {
     LOG(Debug, "WorkflowStepResult::warnings is deprecated, use stepWarnings instead");
     std::vector<LogMessage> result;
     for (const auto& stepWarning : stepWarnings()) {
-      result.push_back(LogMessage(Warn, "", stepWarning));
+      result.emplace_back(Warn, "", stepWarning);
     }
     return result;
   }
@@ -397,7 +399,7 @@ namespace detail {
     LOG(Debug, "WorkflowStepResult::info is deprecated, use stepInfo instead");
     std::vector<LogMessage> result;
     for (const auto& info : stepInfo()) {
-      result.push_back(LogMessage(Info, "", info));
+      result.emplace_back(Info, "", info);
     }
     return result;
   }
@@ -781,7 +783,7 @@ WorkflowStepResult::WorkflowStepResult() : m_impl(std::shared_ptr<detail::Workfl
 
 WorkflowStepResult::WorkflowStepResult(const WorkflowStepResult& other) : m_impl(other.m_impl) {}
 
-WorkflowStepResult::WorkflowStepResult(std::shared_ptr<detail::WorkflowStepResult_Impl> impl) : m_impl(impl) {}
+WorkflowStepResult::WorkflowStepResult(std::shared_ptr<detail::WorkflowStepResult_Impl> impl) : m_impl(std::move(impl)) {}
 
 boost::optional<WorkflowStepResult> WorkflowStepResult::fromString(const std::string& s) {
   // We let it fail with a warning message
diff --git a/src/utilities/idf/Idf.i b/src/utilities/idf/Idf.i
index 7016f3a8fea..6b0cf76d846 100644
--- a/src/utilities/idf/Idf.i
+++ b/src/utilities/idf/Idf.i
@@ -149,6 +149,7 @@
 %include <utilities/idf/WorkspaceObjectOrder.hpp>
 %include <utilities/idf/WorkspaceExtensibleGroup.hpp>
 %include <utilities/idf/WorkspaceObject.hpp>
+%feature("director") Workspace;
 %include <utilities/idf/Workspace.hpp>
 
 %feature("director") IdfObjectWatcher;
diff --git a/src/utilities/idf/Workspace.hpp b/src/utilities/idf/Workspace.hpp
index 31be5a03aea..68e0f0be089 100644
--- a/src/utilities/idf/Workspace.hpp
+++ b/src/utilities/idf/Workspace.hpp
@@ -117,7 +117,7 @@ class UTILITIES_API Workspace
    *  If the Workspace so constructed is not valid at the specified StrictnessLevel, all of the
    *  newly created objects are removed, and the constructor returns an empty Workspace with
    *  StrictnessLevel None. Problems may be diagnosed by calling idfFile.validityReport(level). */
-  Workspace(const IdfFile& idfFile, StrictnessLevel level = StrictnessLevel::Minimal);
+  Workspace(const IdfFile& idfFile, StrictnessLevel level = openstudio::StrictnessLevel::Minimal);
 
   /** Copy constructor, shares data with other Workspace. */
   Workspace(const Workspace& other);
@@ -140,7 +140,8 @@ class UTILITIES_API Workspace
   /** Clone just the objects referenced by handles into a new Workspace. All non-object data is
    *  also cloned. If keepHandles, then new handles will not be assigned to the cloned objects.
    *  Virtual implementation, and similar usage to clone. */
-  Workspace cloneSubset(const std::vector<Handle>& handles, bool keepHandles = false, StrictnessLevel level = StrictnessLevel::Draft) const;
+  Workspace cloneSubset(const std::vector<Handle>& handles, bool keepHandles = false,
+                        StrictnessLevel level = openstudio::StrictnessLevel::Draft) const;
 
   /** Swaps underlying data between this workspace and other. Throws if other and this
    *  are not of the same type (must both be plain Workspaces, model::Models, or
diff --git a/src/utilities/xml/XMLValidator.cpp b/src/utilities/xml/XMLValidator.cpp
index 25bba0640c5..9b88ba4db7d 100644
--- a/src/utilities/xml/XMLValidator.cpp
+++ b/src/utilities/xml/XMLValidator.cpp
@@ -49,7 +49,6 @@
 #include <libxml/xpathInternals.h>  // BAD_CAST
 
 #include <src/utilities/embedded_files.hxx>
-#include <src/gbxml/embedded_files.hxx>
 
 #include <fmt/format.h>
 
@@ -501,7 +500,7 @@ XMLValidator XMLValidator::gbxmlValidator() {
     LOG_AND_THROW("Failed to create a temporary directory for extracting the embedded path");
   }
   bool quiet = true;
-  ::openstudiogbxml::embedded_files::extractFile(":/resources/GreenBuildingXML_Ver6.01.xsd", openstudio::toString(tmpDir), quiet);
+  ::openstudio::embedded_files::extractFile(":/xml/resources/GreenBuildingXML_Ver6.01.xsd", openstudio::toString(tmpDir), quiet);
   return XMLValidator(tmpDir / "GreenBuildingXML_Ver6.01.xsd");
 }
 
diff --git a/src/gbxml/resources/GreenBuildingXML_Ver6.01.xsd b/src/utilities/xml/resources/GreenBuildingXML_Ver6.01.xsd
similarity index 100%
rename from src/gbxml/resources/GreenBuildingXML_Ver6.01.xsd
rename to src/utilities/xml/resources/GreenBuildingXML_Ver6.01.xsd
diff --git a/src/workflow/CMakeLists.txt b/src/workflow/CMakeLists.txt
new file mode 100644
index 00000000000..fe9a00be78f
--- /dev/null
+++ b/src/workflow/CMakeLists.txt
@@ -0,0 +1,7 @@
+add_library(openstudio_workflow
+  OBJECT
+  OSWorkflow.hpp
+  OSWorkflow.cpp
+)
+
+target_link_libraries(openstudio_workflow PRIVATE openstudiolib)
diff --git a/src/workflow/OSWorkflow.cpp b/src/workflow/OSWorkflow.cpp
new file mode 100644
index 00000000000..f57b09cd522
--- /dev/null
+++ b/src/workflow/OSWorkflow.cpp
@@ -0,0 +1,373 @@
+#include "./OSWorkflow.hpp"
+#include "../utilities/filetypes/WorkflowStep.hpp"
+#include "../utilities/bcl/BCLMeasure.hpp"
+
+#include "../osversion/VersionTranslator.hpp"
+#include "../measure/OSMeasure.hpp"
+#include "../measure/ModelMeasure.hpp"
+#include "../measure/EnergyPlusMeasure.hpp"
+#include "../measure/ReportingMeasure.hpp"
+#include "../measure/OSArgument.hpp"
+#include "../measure/OSRunner.hpp"
+#include "../model/Model.hpp"
+#include "../model/Model_Impl.hpp"
+#include "../utilities/idf/Workspace.hpp"
+#include "../utilities/data/Variant.hpp"
+#include "../energyplus/ForwardTranslator.hpp"
+#include "utilities/core/Logger.hpp"
+
+#include <fmt/format.h>
+#include <fmt/ostream.h>
+#include <stdexcept>
+
+namespace openstudio {
+
+OSWorkflow::OSWorkflow(const filesystem::path& oswPath, ScriptEngineInstance& ruby, ScriptEngineInstance& python)
+  :
+#if USE_RUBY_ENGINE
+    rubyEngine(ruby),
+#endif
+#if USE_PYTHON_ENGINE
+    pythonEngine(python),
+#endif
+    workflowJSON(oswPath) {
+}
+
+OSWorkflow::OSWorkflow(WorkflowRunOptions t_runOptions, ScriptEngineInstance& ruby, ScriptEngineInstance& python)
+  :
+#if USE_RUBY_ENGINE
+    rubyEngine(ruby),
+#endif
+#if USE_PYTHON_ENGINE
+    pythonEngine(python),
+#endif
+    workflowJSON(t_runOptions.osw_path),
+    runOptions(std::move(t_runOptions)) {
+}
+
+void OSWorkflow::applyArguments(measure::OSArgumentMap& argumentMap, const std::string& argumentName, const openstudio::Variant& argumentValue) {
+  fmt::print("Info: Setting argument value '{}' to '{}'\n", argumentName, argumentValue);
+
+  // if (!argumentValue.hasValue()) {
+  //   fmt::print("Warn: Value for argument '{}' not set in argument list therefore will use default\n", argumentName);
+  //   return;
+  // }
+  if (!argumentMap.contains(argumentName)) {
+    throw std::runtime_error(fmt::format("Could not find argument '{}' in argument_map\n", argumentName));
+  }
+
+  auto& v = argumentMap.at(argumentName);
+
+  bool value_set = false;
+  VariantType variantType = argumentValue.variantType();
+
+  if (variantType == VariantType::String) {
+    value_set = v.setValue(argumentValue.valueAsString());
+  } else if (variantType == VariantType::Double) {
+    value_set = v.setValue(argumentValue.valueAsDouble());
+  } else if (variantType == VariantType::Integer) {
+    value_set = v.setValue(argumentValue.valueAsInteger());
+  } else if (variantType == VariantType::Boolean) {
+    value_set = v.setValue(argumentValue.valueAsBoolean());
+  }
+
+  if (!value_set) {
+    throw std::runtime_error(fmt::format("Could not set argument '{}' to value '{}': passed argument has type '{}' while '{}' was expected",
+                                         argumentName, argumentValue, argumentValue.variantType().valueName(), v.type().valueName()));
+  }
+}
+
+void OSWorkflow::run() {
+  if (runOptions.debug) {
+    openstudio::Logger::instance().standardOutLogger().setLogLevel(Debug);
+  }
+
+#if USE_RUBY_ENGINE
+  rubyEngine->exec("puts 'Hello from Ruby'");
+  rubyEngine->registerType<openstudio::measure::ModelMeasure*>("openstudio::measure::ModelMeasure *");
+  rubyEngine->registerType<openstudio::measure::EnergyPlusMeasure*>("openstudio::measure::EnergyPlusMeasure *");
+  rubyEngine->registerType<openstudio::measure::ReportingMeasure*>("openstudio::measure::ReportingMeasure *");
+  // -1: gotta init the rest of OpenStudio in ruby, so that Ruleset and co are defined
+  rubyEngine->exec("OpenStudio::init_rest_of_openstudio()");
+#endif
+#if USE_PYTHON_ENGINE
+  pythonEngine->exec("print('Hello from Python')");
+  pythonEngine->registerType<openstudio::measure::PythonModelMeasure*>("openstudio::measure::PythonModelMeasure *");
+  pythonEngine->registerType<openstudio::measure::PythonEnergyPlusMeasure*>("openstudio::measure::PythonEnergyPlusMeasure *");
+  pythonEngine->registerType<openstudio::measure::PythonReportingMeasure*>("openstudio::measure::PythonReportingMeasure *");
+#endif
+
+  // bool no_simulation = false;
+  // bool post_process = false;
+  // bool ep_json = false;
+  // bool show_stdout = false;
+  // bool add_timings = false;
+  // bool style_stdout = false;
+  // unsigned socket_port = 0;
+
+  //// O. Need to apply measure steps IN ORDER. (eg: OpenStudio Measures before Eplus measures etc)
+  //// https://github.com/NREL/OpenStudio-workflow-gem/blob/develop/lib/openstudio/workflow/util/measure.rb
+
+  // 1. Instantiate seed model
+
+  auto runInitialization = [this]() -> openstudio::model::Model {
+    fmt::print("Debug: Finding and loading the seed file\n");
+    auto seedPath_ = workflowJSON.seedFile();
+    if (!seedPath_) {
+      return openstudio::model::Model{};
+    }
+
+    auto modelFullPath_ = workflowJSON.findFile(seedPath_.get());
+    if (!modelFullPath_) {
+      throw std::runtime_error(fmt::format("Seed model {} specified in OSW cannot be found", seedPath_.get()));
+    }
+    openstudio::osversion::VersionTranslator vt;
+    auto m_ = vt.loadModel(modelFullPath_.get());
+    if (!m_) {
+      throw std::runtime_error(fmt::format("Failed to load OSM file {}\n", openstudio::toString(seedPath_.get())));
+    }
+    return m_.get();
+  };
+
+  model::Model model = runInitialization();
+  boost::optional<openstudio::Workspace> workspace_;
+
+  // 2. determine ruby or python
+  // 3. import measure.(py|rb)
+  // 4. instantiate measure
+  // 5. run measure
+
+  // TODO: need to add the ReportingMeasure to the Mix
+  // TODO: need to run the idf through EnergyPlus
+  // TODO: need to create subdirectories for steps
+  // TODO: need to do some cleanup, like removing old sim files, maybe reports, etc
+  // TODO: need to implement some of the options/flags like --postprocess_only
+  // TODO: need to merge workflowJSON flags with flags from command line (eg: FT options)
+  // TODO: need to modify utilities/RunOptions.hpp instead of duplicating some of that work in workflow
+
+  for (openstudio::MeasureType stepType : {openstudio::MeasureType::ModelMeasure, openstudio::MeasureType::EnergyPlusMeasure}) {
+    const auto modelSteps = workflowJSON.getMeasureSteps(stepType);
+    if (stepType == MeasureType::EnergyPlusMeasure) {
+      openstudio::energyplus::ForwardTranslator ft;
+      auto& ftOptions = runOptions.ft_options;
+      ft.setKeepRunControlSpecialDays(ftOptions.runcontrolspecialdays);
+      ft.setIPTabularOutput(ftOptions.ip_tabular_output);
+      ft.setExcludeLCCObjects(ftOptions.no_lifecyclecosts);
+      ft.setExcludeSQliteOutputReport(ftOptions.no_sqlite_output);
+      ft.setExcludeHTMLOutputReport(ftOptions.no_html_output);
+      ft.setExcludeSpaceTranslation(ftOptions.no_space_translation);
+      workspace_ = ft.translateModel(model);
+    }
+
+    for (const auto& step : modelSteps) {
+      unsigned stepIndex = workflowJSON.currentStepIndex();
+      fmt::print("\n\nRunning step {}\n", stepIndex);
+
+      //  measure_run_dir = File.join(run_dir, "#{step_index.to_s.rjust(3,'0')}_#{measure_dir_name}")
+      //  logger.debug "Creating run directory for measure in #{measure_run_dir}"
+      //  FileUtils.mkdir_p measure_run_dir
+      //  Dir.chdir measure_run_dir
+
+      const auto measureDirName = step.measureDirName();
+      const auto measureDirPath_ = workflowJSON.findMeasure(measureDirName);
+      if (!measureDirPath_) {
+        fmt::print("Could not find measure '{}'\n", measureDirName);
+        continue;
+      }
+      BCLMeasure bclMeasure(measureDirPath_.get());
+
+      const auto scriptPath_ = bclMeasure.primaryScriptPath();
+      if (!scriptPath_) {
+        fmt::print("Could not find primaryScriptPath '{}'\n", measureDirName);
+        continue;
+      }
+      fmt::print("Found {} at primaryScriptPath: '{}'\n", measureDirName, openstudio::toString(scriptPath_.get()));
+      const std::string className = bclMeasure.className();
+      const auto measureType = bclMeasure.measureType();
+      const MeasureLanguage measureLanguage = bclMeasure.measureLanguage();
+
+      // TODO: will add a Logger later
+      fmt::print("Class Name: {}\n", className);
+      fmt::print("Measure Script Path: {}\n", openstudio::toString(scriptPath_.get()));
+      fmt::print("Measure Type: {}\n", bclMeasure.measureType().valueName());
+      fmt::print("Measure Language: {}\n", measureLanguage.valueName());
+
+      //openstudio::measure::ModelMeasure* modelMeasurePtr = nullptr;
+      // TODO: probably want to do that ultimately, then static_cast appropriately
+      ScriptEngineInstance* thisEngine;
+      ScriptObject measureScriptObject;
+      openstudio::measure::OSMeasure* measurePtr = nullptr;
+
+      auto getArguments = [&model, &workspace_, &measureType, &scriptPath_, &step,
+                           &measureLanguage](openstudio::measure::OSMeasure* measurePtr) -> measure::OSArgumentMap {
+        if (!measurePtr) {
+          throw std::runtime_error(fmt::format("Could not load measure at '{}'", openstudio::toString(scriptPath_.get())));
+        }
+        // Initialize arguments which may be model dependent, don't allow arguments method access to real model in case it changes something
+        std::vector<measure::OSArgument> arguments;
+
+        fmt::print("measure->name()= '{}'\n", measurePtr->name());
+
+        if (measureType == MeasureType::ModelMeasure) {
+          // For computing arguments
+          auto modelClone = model.clone(true).cast<model::Model>();
+          if (measureLanguage == MeasureLanguage::Ruby) {
+            arguments = static_cast<openstudio::measure::ModelMeasure*>(measurePtr)->arguments(modelClone);  // NOLINT
+          } else if (measureLanguage == MeasureLanguage::Python) {
+            arguments = static_cast<openstudio::measure::PythonModelMeasure*>(measurePtr)->arguments(modelClone);  // NOLINT
+          }
+        } else if (measureType == MeasureType::EnergyPlusMeasure) {
+          auto workspaceClone = workspace_->clone(true).cast<openstudio::Workspace>();
+          if (measureLanguage == MeasureLanguage::Ruby) {
+            arguments = static_cast<openstudio::measure::EnergyPlusMeasure*>(measurePtr)->arguments(workspaceClone);  // NOLINT
+          } else if (measureLanguage == MeasureLanguage::Python) {
+            arguments = static_cast<openstudio::measure::PythonEnergyPlusMeasure*>(measurePtr)->arguments(workspaceClone);  // NOLINT
+          }
+        } else if (measureType == MeasureType::ReportingMeasure) {
+          auto modelClone = model.clone(true).cast<model::Model>();
+          if (measureLanguage == MeasureLanguage::Ruby) {
+            arguments = static_cast<openstudio::measure::ReportingMeasure*>(measurePtr)->arguments(modelClone);  // NOLINT
+          } else if (measureLanguage == MeasureLanguage::Python) {
+            arguments = static_cast<openstudio::measure::PythonReportingMeasure*>(measurePtr)->arguments(modelClone);  // NOLINT
+          }
+        }
+
+        measure::OSArgumentMap argumentMap;
+        if (!arguments.empty()) {
+          for (const auto& argument : arguments) {
+            fmt::print("Argument: {}\n", argument.name());
+            argumentMap[argument.name()] = argument.clone();
+          }
+
+          //  logger.debug "Iterating over arguments for workflow item '#{measure_dir_name}'"
+          auto stepArgs = step.arguments();
+          fmt::print("Current step has {} arguments\n", stepArgs.size());
+          // handle skip first
+          if (!stepArgs.empty() && stepArgs.contains("__SKIP__")) {
+            // TODO: handling of SKIP is incomplete here, will need to increment the runner and co, and not process the measure
+
+          } else {
+            // TODO: I've copied workflow-gem here, but it's wrong. It's trying to issue a warning if an argument is not set, so it should loop on
+            // argumentMap instead...
+            for (auto const& [argumentName, argumentValue] : stepArgs) {
+              applyArguments(argumentMap, argumentName, argumentValue);
+            }
+          }
+        }
+
+        return argumentMap;
+      };
+
+      if (measureLanguage == MeasureLanguage::Ruby) {
+        // TODO: probably need to do path formatting properly for windows
+#if USE_RUBY_ENGINE
+        auto importCmd = fmt::format("require '{}'", openstudio::toString(scriptPath_.get()));
+        rubyEngine->exec(importCmd);
+        measureScriptObject = rubyEngine->eval(fmt::format("{}.new()", className));
+        thisEngine = &rubyEngine;
+#else
+        throw std::runtime_error("Cannot run a Ruby measure when RubyEngine isn't enabled");
+#endif
+      } else if (measureLanguage == MeasureLanguage::Python) {
+#if USE_PYTHON_ENGINE
+        // place measureDirPath in sys.path; do from measure import MeasureName
+        // I think this can't work without a "as xxx" otherwise we'll repeatedly try to import a module named 'measure'
+        // pythonEngine->pyimport("measure", openstudio::toString(measureDirPath.get()));
+        //         auto importCmd = fmt::format(R"python(
+        // import sys
+        // sys.path.insert(0, r'{}')
+        // force_reload = 'measure' in sys.modules
+        // import measure
+        // if force_reload:
+        //     # print("force reload measure")
+        //     import importlib
+        //     importlib.reload(measure)
+        // )python",
+        //                                      scriptPath_->parent_path().generic_string());
+        auto importCmd = fmt::format(R"python(
+import importlib
+spec = importlib.util.spec_from_file_location('{}', r'{}')
+module = importlib.util.module_from_spec(spec)
+spec.loader.exec_module(module)
+)python",
+                                     className, scriptPath_->generic_string());
+        // fmt::print("\nimportCmd:\n{}\n", importCmd);
+        pythonEngine->exec(importCmd);
+        // measureScriptObject = pythonEngine->eval(fmt::format("measure.{}()", className));
+        measureScriptObject = pythonEngine->eval(fmt::format("module.{}()", className));
+
+        thisEngine = &pythonEngine;
+#else
+        throw std::runtime_error("Cannot run a Python measure when PythonEngine isn't enabled");
+#endif
+      }
+
+      // This pointer will only be valid for as long as the above PythonMeasure is in scope
+      // After that, dereferencing the measure pointer will crash the program
+      if (measureType == MeasureType::ModelMeasure) {
+        if (measureLanguage == MeasureLanguage::Ruby) {
+          measurePtr = (*thisEngine)->getAs<openstudio::measure::ModelMeasure*>(measureScriptObject);
+        } else if (measureLanguage == MeasureLanguage::Python) {
+          measurePtr = (*thisEngine)->getAs<openstudio::measure::PythonModelMeasure*>(measureScriptObject);
+        }
+      } else if (measureType == MeasureType::EnergyPlusMeasure) {
+        if (measureLanguage == MeasureLanguage::Ruby) {
+          measurePtr = (*thisEngine)->getAs<openstudio::measure::EnergyPlusMeasure*>(measureScriptObject);
+        } else if (measureLanguage == MeasureLanguage::Python) {
+          measurePtr = (*thisEngine)->getAs<openstudio::measure::PythonEnergyPlusMeasure*>(measureScriptObject);
+        }
+      } else if (measureType == MeasureType::ReportingMeasure) {
+        if (measureLanguage == MeasureLanguage::Ruby) {
+          measurePtr = (*thisEngine)->getAs<openstudio::measure::ReportingMeasure*>(measureScriptObject);
+        } else if (measureLanguage == MeasureLanguage::Python) {
+          measurePtr = (*thisEngine)->getAs<openstudio::measure::PythonReportingMeasure*>(measureScriptObject);
+        }
+      }
+
+      const auto argmap = getArguments(measurePtr);
+      // There is a bug. I can run one measure but not two. The one measure can be either python or ruby
+      // I think it might have to do with the operations that must be done to the runner to reset state. maybe?
+      if (measureType == MeasureType::ModelMeasure) {
+        if (measureLanguage == MeasureLanguage::Ruby) {
+          static_cast<openstudio::measure::ModelMeasure*>(measurePtr)->run(model, runner, argmap);
+        } else if (measureLanguage == MeasureLanguage::Python) {
+          static_cast<openstudio::measure::PythonModelMeasure*>(measurePtr)->run(model, runner, argmap);
+        }
+      } else if (measureType == MeasureType::EnergyPlusMeasure) {
+        if (measureLanguage == MeasureLanguage::Ruby) {
+          static_cast<openstudio::measure::EnergyPlusMeasure*>(measurePtr)->run(workspace_.get(), runner, argmap);
+        } else if (measureLanguage == MeasureLanguage::Python) {
+          static_cast<openstudio::measure::PythonEnergyPlusMeasure*>(measurePtr)->run(workspace_.get(), runner, argmap);
+        }
+      } else if (measureType == MeasureType::ReportingMeasure) {
+        if (measureLanguage == MeasureLanguage::Ruby) {
+          static_cast<openstudio::measure::ReportingMeasure*>(measurePtr)->run(runner, argmap);
+        } else if (measureLanguage == MeasureLanguage::Python) {
+          static_cast<openstudio::measure::PythonReportingMeasure*>(measurePtr)->run(runner, argmap);
+        }
+      }
+
+      WorkflowStepResult result = runner.result();
+      if (auto stepResult_ = result.stepResult()) {
+        fmt::print("Step Result: {}\n", stepResult_->valueName());
+      }
+      // incrementStep must be called after run
+      runner.incrementStep();
+      if (auto errors = result.stepErrors(); !errors.empty()) {
+        throw std::runtime_error(fmt::format("Measure {} reported an error with [{}]\n", measureDirName, fmt::join(errors, "\n")));
+      }
+
+      if (measureType == MeasureType::ModelMeasure) {
+        if (auto weatherFile_ = model.weatherFile()) {
+          if (auto p_ = weatherFile_->path()) {
+            // Probably a workflowJSON.findFile() call...
+            // m_epwPath_ = p_;
+          } else {
+            fmt::print("Weather file object found in model but no path is given\n");
+          }
+        }
+      }
+    }  // End for (const auto& step : modelSteps)
+  }
+}
+}  // namespace openstudio
diff --git a/src/workflow/OSWorkflow.hpp b/src/workflow/OSWorkflow.hpp
new file mode 100644
index 00000000000..24ca51c6082
--- /dev/null
+++ b/src/workflow/OSWorkflow.hpp
@@ -0,0 +1,48 @@
+#ifndef WORKFLOW_OSWORKFLOW_HPP
+#define WORKFLOW_OSWORKFLOW_HPP
+
+#include "RunOptions.hpp"
+#include "../measure/OSRunner.hpp"
+#include "../scriptengine/ScriptEngine.hpp"
+#include "../utilities/core/Filesystem.hpp"
+#include "../utilities/filetypes/WorkflowJSON.hpp"
+#include <memory>
+
+#define USE_RUBY_ENGINE 1
+#define USE_PYTHON_ENGINE 1
+
+namespace openstudio {
+
+class Variant;
+
+namespace measure {
+  class OSArgument;
+  using OSArgumentMap = std::map<std::string, OSArgument>;
+}  // namespace measure
+
+class OSWorkflow
+{
+ public:
+  OSWorkflow(const filesystem::path& oswPath, ScriptEngineInstance& ruby, ScriptEngineInstance& python);
+  OSWorkflow(WorkflowRunOptions t_runOptions, ScriptEngineInstance& ruby, ScriptEngineInstance& python);
+
+  void run();
+
+ private:
+  // TODO: add a Logger
+#if USE_RUBY_ENGINE
+  ScriptEngineInstance& rubyEngine;
+#endif
+#if USE_PYTHON_ENGINE
+  ScriptEngineInstance& pythonEngine;
+#endif
+  WorkflowJSON workflowJSON;
+  measure::OSRunner runner{workflowJSON};
+  WorkflowRunOptions runOptions;
+
+  static void applyArguments(measure::OSArgumentMap& argumentMap, const std::string& argumentName, const openstudio::Variant& argumentValue);
+};
+
+}  // namespace openstudio
+
+#endif  // WORKFLOW_OSWORKFLOW_HPP
diff --git a/src/workflow/RunOptions.cpp b/src/workflow/RunOptions.cpp
new file mode 100644
index 00000000000..6185b3fd36c
--- /dev/null
+++ b/src/workflow/RunOptions.cpp
@@ -0,0 +1,64 @@
+/***********************************************************************************************************************
+*  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 "RunOptions.hpp"
+
+#include <fmt/format.h>
+
+namespace openstudio {
+
+void WorkflowRunOptions::debug_print() const {
+  fmt::print("\nRunOptions:\n");
+  fmt::print("osw_path={}\n", this->osw_path.string());
+  fmt::print("debug={}\n", this->debug);
+  fmt::print("no_simulation={}\n", this->no_simulation);
+  fmt::print("post_process={}\n", this->post_process);
+  fmt::print("ep_json={}\n", this->ep_json);
+  fmt::print("show_stdout={}\n", this->show_stdout);
+  fmt::print("add_timings={}\n", this->add_timings);
+  fmt::print("style_stdout={}\n", this->style_stdout);
+
+  fmt::print("socket_port={}\n", this->socket_port);
+
+  this->ft_options.debug_print();
+
+  fmt::print("\n\n");
+}
+
+void FtOptions::debug_print() const {
+  fmt::print("FtOptions:\n");
+  fmt::print("  * runcontrolspecialdays={}\n", runcontrolspecialdays);
+  fmt::print("  * ip_tabular_output={}\n", ip_tabular_output);
+  fmt::print("  * no_lifecyclecosts={}\n", no_lifecyclecosts);
+  fmt::print("  * no_sqlite_output={}\n", no_sqlite_output);
+  fmt::print("  * no_html_output={}\n", no_html_output);
+  fmt::print("  * no_space_translation={}\n", no_space_translation);
+}
+
+}  // namespace openstudio
diff --git a/src/workflow/RunOptions.hpp b/src/workflow/RunOptions.hpp
new file mode 100644
index 00000000000..a739db69e16
--- /dev/null
+++ b/src/workflow/RunOptions.hpp
@@ -0,0 +1,73 @@
+/***********************************************************************************************************************
+*  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 WORKFLOW_RUNOPTIONS_HPP
+#define WORKFLOW_RUNOPTIONS_HPP
+
+#include "../utilities/core/Filesystem.hpp"
+
+namespace openstudio {
+
+struct FtOptions
+{
+ public:
+  bool runcontrolspecialdays = true;
+  bool ip_tabular_output = false;
+  bool no_lifecyclecosts = false;
+  bool no_sqlite_output = false;
+  bool no_html_output = false;
+  bool no_variable_dictionary = false;
+  bool no_space_translation = false;
+
+  void debug_print() const;
+
+  static constexpr auto group_name = "Forward Translator Options";
+};
+
+struct WorkflowRunOptions
+{
+ public:
+  void debug_print() const;
+
+  bool debug = false;
+  bool no_simulation = false;
+  bool post_process = false;
+  bool ep_json = false;
+  bool show_stdout = false;
+  bool add_timings = false;
+  bool style_stdout = false;
+  unsigned socket_port = 0;
+  openstudio::path osw_path = "./workflow.osw";
+
+  FtOptions ft_options;
+};
+
+}  // namespace openstudio
+
+#endif  // WORKFLOW_RUNOPTIONS_HPP


Test patch:
diff --git a/resources/Examples/compact_osw/measures/IncreaseRoofRValuePython/tests/output/test_output.osm b/resources/Examples/compact_osw/measures/IncreaseRoofRValuePython/tests/output/test_output.osm
new file mode 100644
index 00000000000..397f7bf863a
--- /dev/null
+++ b/resources/Examples/compact_osw/measures/IncreaseRoofRValuePython/tests/output/test_output.osm
@@ -0,0 +1,3264 @@
+
+OS:Version,
+  {7ffd4a1f-f95b-417d-b99d-b1b51334cd78}, !- Handle
+  3.4.0;                                  !- Version Identifier
+
+OS:SimulationControl,
+  {9a896f84-b9c2-4780-a1be-9b7c8e00538a}, !- Handle
+  Yes,                                    !- Do Zone Sizing Calculation
+  Yes,                                    !- Do System Sizing Calculation
+  ,                                       !- Do Plant Sizing Calculation
+  No,                                     !- Run Simulation for Sizing Periods
+  Yes;                                    !- Run Simulation for Weather File Run Periods
+
+OS:Timestep,
+  {d22041d6-cf71-4595-8c09-afcd04a86f5d}, !- Handle
+  6;                                      !- Number of Timesteps per Hour
+
+OS:RunPeriod,
+  {86e936f4-a45b-42e3-907b-d9b98b3900ab}, !- Handle
+  Run Period 1,                           !- Name
+  1,                                      !- Begin Month
+  1,                                      !- Begin Day of Month
+  12,                                     !- End Month
+  31,                                     !- End Day of Month
+  No,                                     !- Use Weather File Holidays and Special Days
+  No,                                     !- Use Weather File Daylight Saving Period
+  No,                                     !- Apply Weekend Holiday Rule
+  Yes,                                    !- Use Weather File Rain Indicators
+  Yes,                                    !- Use Weather File Snow Indicators
+  1;                                      !- Number of Times Runperiod to be Repeated
+
+OS:SurfaceConvectionAlgorithm:Inside,
+  {9472425d-bad4-42db-98a8-6d1bc4d23877}, !- Handle
+  TARP;                                   !- Algorithm
+
+OS:SurfaceConvectionAlgorithm:Outside,
+  {1553c3c3-7a22-4848-be1a-669832a34da0}, !- Handle
+  DOE-2;                                  !- Algorithm
+
+OS:HeatBalanceAlgorithm,
+  {3344b7bd-56dd-46b6-9ba6-1743956b6341}, !- Handle
+  ConductionTransferFunction,             !- Algorithm
+  200;                                    !- Surface Temperature Upper Limit {C}
+
+OS:ZoneAirHeatBalanceAlgorithm,
+  {e12e59dd-5dbe-4bab-bf05-52ff9f065ec3}, !- Handle
+  ThirdOrderBackwardDifference;           !- Algorithm
+
+OS:ConvergenceLimits,
+  {de57accf-a280-42d3-802f-307d34636a87}, !- Handle
+  1;                                      !- Minimum System Timestep {minutes}
+
+OS:ShadowCalculation,
+  {48d2246d-2dca-42f5-95da-16f9fbe490f1}, !- Handle
+  PolygonClipping,                        !- Shading Calculation Method
+  ,                                       !- Shading Calculation Update Frequency Method
+  20,                                     !- Shading Calculation Update Frequency
+  15000,                                  !- Maximum Figures in Shadow Overlap Calculations
+  ,                                       !- Polygon Clipping Algorithm
+  512,                                    !- Pixel Counting Resolution
+  ,                                       !- Sky Diffuse Modeling Algorithm
+  No,                                     !- Output External Shading Calculation Results
+  No,                                     !- Disable Self-Shading Within Shading Zone Groups
+  No;                                     !- Disable Self-Shading From Shading Zone Groups to Other Zones
+
+OS:Site,
+  {d89b94b3-477a-464f-b8da-c37a32d592af}, !- Handle
+  Site 1,                                 !- Name
+  41.77,                                  !- Latitude {deg}
+  -87.75,                                 !- Longitude {deg}
+  -6,                                     !- Time Zone {hr}
+  190,                                    !- Elevation {m}
+  ;                                       !- Terrain
+
+OS:Site:GroundTemperature:BuildingSurface,
+  {2f470957-3e0c-4a99-8688-840234112a61}, !- Handle
+  19.527,                                 !- January Ground Temperature {C}
+  19.502,                                 !- February Ground Temperature {C}
+  19.536,                                 !- March Ground Temperature {C}
+  19.598,                                 !- April Ground Temperature {C}
+  20.002,                                 !- May Ground Temperature {C}
+  21.64,                                  !- June Ground Temperature {C}
+  22.225,                                 !- July Ground Temperature {C}
+  22.375,                                 !- August Ground Temperature {C}
+  21.449,                                 !- September Ground Temperature {C}
+  20.121,                                 !- October Ground Temperature {C}
+  19.802,                                 !- November Ground Temperature {C}
+  19.633;                                 !- December Ground Temperature {C}
+
+OS:Site:GroundTemperature:Deep,
+  {015efc36-7214-4cdc-98aa-c7f21e0eecf0}, !- Handle
+  19.527,                                 !- January Deep Ground Temperature {C}
+  19.502,                                 !- February Deep Ground Temperature {C}
+  19.536,                                 !- March Deep Ground Temperature {C}
+  19.598,                                 !- April Deep Ground Temperature {C}
+  20.002,                                 !- May Deep Ground Temperature {C}
+  21.64,                                  !- June Deep Ground Temperature {C}
+  22.225,                                 !- July Deep Ground Temperature {C}
+  22.375,                                 !- August Deep Ground Temperature {C}
+  21.449,                                 !- September Deep Ground Temperature {C}
+  20.121,                                 !- October Deep Ground Temperature {C}
+  19.802,                                 !- November Deep Ground Temperature {C}
+  19.633;                                 !- December Deep Ground Temperature {C}
+
+OS:Site:WaterMainsTemperature,
+  {1f12ba2c-8d86-471e-b25e-7e1949b5c868}, !- Handle
+  Correlation,                            !- Calculation Method
+  ,                                       !- Temperature Schedule Name
+  9.69,                                   !- Annual Average Outdoor Air Temperature {C}
+  28.1;                                   !- Maximum Difference In Monthly Average Outdoor Air Temperatures {deltaC}
+
+OS:SizingPeriod:DesignDay,
+  {2ffe3efa-9e94-4ee5-aed8-2a97b7df557c}, !- Handle
+  Sizing Period Design Day 1,             !- Name
+  -20.6,                                  !- Maximum Dry-Bulb Temperature {C}
+  0,                                      !- Daily Dry-Bulb Temperature Range {deltaC}
+  99063,                                  !- Barometric Pressure {Pa}
+  4.9,                                    !- Wind Speed {m/s}
+  270,                                    !- Wind Direction {deg}
+  0,                                      !- Sky Clearness
+  No,                                     !- Rain Indicator
+  No,                                     !- Snow Indicator
+  21,                                     !- Day of Month
+  1,                                      !- Month
+  WinterDesignDay,                        !- Day Type
+  No,                                     !- Daylight Saving Time Indicator
+  WetBulb,                                !- Humidity Condition Type
+  ,                                       !- Humidity Condition Day Schedule Name
+  -20.6,                                  !- Wetbulb or DewPoint at Maximum Dry-Bulb {C}
+  ,                                       !- Humidity Ratio at Maximum Dry-Bulb {kgWater/kgDryAir}
+  ,                                       !- Enthalpy at Maximum Dry-Bulb {J/kg}
+  ;                                       !- Dry-Bulb Temperature Range Modifier Type
+
+OS:SizingPeriod:DesignDay,
+  {3a6accd4-27de-4b8f-834f-04cd9b055c53}, !- Handle
+  Sizing Period Design Day 2,             !- Name
+  33.2,                                   !- Maximum Dry-Bulb Temperature {C}
+  10.7,                                   !- Daily Dry-Bulb Temperature Range {deltaC}
+  99063,                                  !- Barometric Pressure {Pa}
+  5.3,                                    !- Wind Speed {m/s}
+  230,                                    !- Wind Direction {deg}
+  1,                                      !- Sky Clearness
+  No,                                     !- Rain Indicator
+  No,                                     !- Snow Indicator
+  21,                                     !- Day of Month
+  7,                                      !- Month
+  SummerDesignDay,                        !- Day Type
+  No,                                     !- Daylight Saving Time Indicator
+  WetBulb,                                !- Humidity Condition Type
+  ,                                       !- Humidity Condition Day Schedule Name
+  23.8,                                   !- Wetbulb or DewPoint at Maximum Dry-Bulb {C}
+  ,                                       !- Humidity Ratio at Maximum Dry-Bulb {kgWater/kgDryAir}
+  ,                                       !- Enthalpy at Maximum Dry-Bulb {J/kg}
+  ;                                       !- Dry-Bulb Temperature Range Modifier Type
+
+OS:ScheduleTypeLimits,
+  {9fb242af-866e-43b2-9e36-20f2ec391279}, !- Handle
+  Temperature,                            !- Name
+  -60,                                    !- Lower Limit Value
+  200,                                    !- Upper Limit Value
+  CONTINUOUS,                             !- Numeric Type
+  Temperature;                            !- Unit Type
+
+OS:DefaultScheduleSet,
+  {c0328153-1299-4213-b6f7-8540652d4880}, !- Handle
+  Default Schedules,                      !- Name
+  ,                                       !- Hours of Operation Schedule Name
+  {09edddef-4b83-4a34-8026-9bc707dcadda}, !- Number of People Schedule Name
+  {bd24cb41-52de-40d3-b570-e7f98ced69d6}, !- People Activity Level Schedule Name
+  {122868d6-b02c-4d77-bd09-0dd6db8c2b67}, !- Lighting Schedule Name
+  {d102ddc0-b56d-4d45-94f9-32a83bc51fd9}, !- Electric Equipment Schedule Name
+  {6c55330b-333e-4352-9f6e-8552d1fd84b9}, !- Gas Equipment Schedule Name
+  {a82adb05-983a-45da-8edf-1631bd981ef3}, !- Hot Water Equipment Schedule Name
+  {3894555f-e927-4bc3-a4b5-6e5425e9f9da}, !- Infiltration Schedule Name
+  ,                                       !- Steam Equipment Schedule Name
+  ;                                       !- Other Equipment Schedule Name
+
+OS:Schedule:Ruleset,
+  {09edddef-4b83-4a34-8026-9bc707dcadda}, !- Handle
+  Medium Office Number of People Schedule, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  {dfe36e30-58a0-478e-89fd-92bda8ee06cc}, !- Default Day Schedule Name
+  {177934c3-4fb3-44d7-9b4d-6db3528c3ae3}; !- Summer Design Day Schedule Name
+
+OS:Schedule:Day,
+  {dfe36e30-58a0-478e-89fd-92bda8ee06cc}, !- Handle
+  Medium Office Number of People All Other Days Schedule, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  24,                                     !- Hour 1
+  0,                                      !- Minute 1
+  0;                                      !- Value Until Time 1
+
+OS:Schedule:Day,
+  {0fb3d1ba-b689-4e52-8c8b-e8b3f621ea9d}, !- Handle
+  Medium Office Number of People Summer Design Day Schedule, !- Name
+  ,                                       !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  6,                                      !- Hour 1
+  0,                                      !- Minute 1
+  0,                                      !- Value Until Time 1
+  22,                                     !- Hour 2
+  0,                                      !- Minute 2
+  1,                                      !- Value Until Time 2
+  24,                                     !- Hour 3
+  0,                                      !- Minute 3
+  0.05;                                   !- Value Until Time 3
+
+OS:Schedule:Day,
+  {177934c3-4fb3-44d7-9b4d-6db3528c3ae3}, !- Handle
+  Medium Office Number of People Summer Design Day Schedule 1, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  6,                                      !- Hour 1
+  0,                                      !- Minute 1
+  0,                                      !- Value Until Time 1
+  22,                                     !- Hour 2
+  0,                                      !- Minute 2
+  1,                                      !- Value Until Time 2
+  24,                                     !- Hour 3
+  0,                                      !- Minute 3
+  0.05;                                   !- Value Until Time 3
+
+OS:Schedule:Rule,
+  {30388701-766d-4883-8cdb-eca7fd72619b}, !- Handle
+  Medium Office Number of People Schedule Weekdays Rule, !- Name
+  {09edddef-4b83-4a34-8026-9bc707dcadda}, !- Schedule Ruleset Name
+  1,                                      !- Rule Order
+  {68303b3e-a9d3-4a87-b03b-795f455a8484}, !- Day Schedule Name
+  ,                                       !- Apply Sunday
+  Yes,                                    !- Apply Monday
+  Yes,                                    !- Apply Tuesday
+  Yes,                                    !- Apply Wednesday
+  Yes,                                    !- Apply Thursday
+  Yes;                                    !- Apply Friday
+
+OS:Schedule:Day,
+  {68303b3e-a9d3-4a87-b03b-795f455a8484}, !- Handle
+  Medium Office Number of People Weekdays Schedule, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  6,                                      !- Hour 1
+  0,                                      !- Minute 1
+  0,                                      !- Value Until Time 1
+  7,                                      !- Hour 2
+  0,                                      !- Minute 2
+  0.1,                                    !- Value Until Time 2
+  8,                                      !- Hour 3
+  0,                                      !- Minute 3
+  0.2,                                    !- Value Until Time 3
+  12,                                     !- Hour 4
+  0,                                      !- Minute 4
+  0.95,                                   !- Value Until Time 4
+  13,                                     !- Hour 5
+  0,                                      !- Minute 5
+  0.5,                                    !- Value Until Time 5
+  17,                                     !- Hour 6
+  0,                                      !- Minute 6
+  0.95,                                   !- Value Until Time 6
+  18,                                     !- Hour 7
+  0,                                      !- Minute 7
+  0.7,                                    !- Value Until Time 7
+  20,                                     !- Hour 8
+  0,                                      !- Minute 8
+  0.4,                                    !- Value Until Time 8
+  22,                                     !- Hour 9
+  0,                                      !- Minute 9
+  0.1,                                    !- Value Until Time 9
+  24,                                     !- Hour 10
+  0,                                      !- Minute 10
+  0.05;                                   !- Value Until Time 10
+
+OS:Schedule:Rule,
+  {d873e9d0-87e6-4712-bde1-608a5b3f2d32}, !- Handle
+  Medium Office Number of People Schedule Saturday Rule, !- Name
+  {09edddef-4b83-4a34-8026-9bc707dcadda}, !- Schedule Ruleset Name
+  0,                                      !- Rule Order
+  {53b696be-ba1d-40d8-b248-f3a2bcd5136d}, !- Day Schedule Name
+  ,                                       !- Apply Sunday
+  ,                                       !- Apply Monday
+  ,                                       !- Apply Tuesday
+  ,                                       !- Apply Wednesday
+  ,                                       !- Apply Thursday
+  ,                                       !- Apply Friday
+  Yes;                                    !- Apply Saturday
+
+OS:Schedule:Day,
+  {53b696be-ba1d-40d8-b248-f3a2bcd5136d}, !- Handle
+  Medium Office Number of People Saturday Schedule, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  6,                                      !- Hour 1
+  0,                                      !- Minute 1
+  0,                                      !- Value Until Time 1
+  8,                                      !- Hour 2
+  0,                                      !- Minute 2
+  0.1,                                    !- Value Until Time 2
+  14,                                     !- Hour 3
+  0,                                      !- Minute 3
+  0.5,                                    !- Value Until Time 3
+  17,                                     !- Hour 4
+  0,                                      !- Minute 4
+  0.1,                                    !- Value Until Time 4
+  24,                                     !- Hour 5
+  0,                                      !- Minute 5
+  0;                                      !- Value Until Time 5
+
+OS:ScheduleTypeLimits,
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Handle
+  Fractional,                             !- Name
+  0,                                      !- Lower Limit Value
+  1,                                      !- Upper Limit Value
+  Continuous;                             !- Numeric Type
+
+OS:Schedule:Ruleset,
+  {bd24cb41-52de-40d3-b570-e7f98ced69d6}, !- Handle
+  Medium Office People Activity Level Schedule, !- Name
+  {7d358452-d08d-4751-b35d-b9c5eba66112}, !- Schedule Type Limits Name
+  {b21c9819-8aa3-4d76-b55d-21faa6846386}; !- Default Day Schedule Name
+
+OS:Schedule:Day,
+  {b21c9819-8aa3-4d76-b55d-21faa6846386}, !- Handle
+  Medium Office People Activity Level All Days Schedule, !- Name
+  {7d358452-d08d-4751-b35d-b9c5eba66112}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  24,                                     !- Hour 1
+  0,                                      !- Minute 1
+  120;                                    !- Value Until Time 1
+
+OS:ScheduleTypeLimits,
+  {7d358452-d08d-4751-b35d-b9c5eba66112}, !- Handle
+  ActivityLevel,                          !- Name
+  0,                                      !- Lower Limit Value
+  ,                                       !- Upper Limit Value
+  Continuous,                             !- Numeric Type
+  ActivityLevel;                          !- Unit Type
+
+OS:Schedule:Ruleset,
+  {122868d6-b02c-4d77-bd09-0dd6db8c2b67}, !- Handle
+  Medium Office Lighting Schedule,        !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  {e3d94a45-c6b3-440b-ab6c-c146e36ecb93}, !- Default Day Schedule Name
+  {57dc33b0-de28-4c77-874e-cd5ed14d3cc1}, !- Summer Design Day Schedule Name
+  {31068acd-f5d5-42f4-a85d-4b90b98faac2}; !- Winter Design Day Schedule Name
+
+OS:Schedule:Day,
+  {e3d94a45-c6b3-440b-ab6c-c146e36ecb93}, !- Handle
+  Medium Office Lighting All Other Days Schedule, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  24,                                     !- Hour 1
+  0,                                      !- Minute 1
+  0.05;                                   !- Value Until Time 1
+
+OS:Schedule:Day,
+  {6558d9a6-7545-438f-970e-4906ad8fc905}, !- Handle
+  Medium Office Lighting Summer Design Day Schedule, !- Name
+  ,                                       !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  6,                                      !- Hour 1
+  0,                                      !- Minute 1
+  1,                                      !- Value Until Time 1
+  24,                                     !- Hour 2
+  0,                                      !- Minute 2
+  0;                                      !- Value Until Time 2
+
+OS:Schedule:Day,
+  {57dc33b0-de28-4c77-874e-cd5ed14d3cc1}, !- Handle
+  Medium Office Lighting Summer Design Day Schedule 1, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  6,                                      !- Hour 1
+  0,                                      !- Minute 1
+  1,                                      !- Value Until Time 1
+  24,                                     !- Hour 2
+  0,                                      !- Minute 2
+  0;                                      !- Value Until Time 2
+
+OS:Schedule:Day,
+  {05748907-0b5f-4d66-8523-9fe35ec1a381}, !- Handle
+  Medium Office Lighting Winter Design Day Schedule, !- Name
+  ,                                       !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  24,                                     !- Hour 1
+  0,                                      !- Minute 1
+  0;                                      !- Value Until Time 1
+
+OS:Schedule:Day,
+  {31068acd-f5d5-42f4-a85d-4b90b98faac2}, !- Handle
+  Medium Office Lighting Winter Design Day Schedule 1, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  24,                                     !- Hour 1
+  0,                                      !- Minute 1
+  0;                                      !- Value Until Time 1
+
+OS:Schedule:Rule,
+  {c0edeede-0d30-493a-9d3d-872bd1ed86df}, !- Handle
+  Medium Office Lighting Schedule Weekdays Rule, !- Name
+  {122868d6-b02c-4d77-bd09-0dd6db8c2b67}, !- Schedule Ruleset Name
+  1,                                      !- Rule Order
+  {cae50817-efde-4cf0-ba42-f26c88540d23}, !- Day Schedule Name
+  ,                                       !- Apply Sunday
+  Yes,                                    !- Apply Monday
+  Yes,                                    !- Apply Tuesday
+  Yes,                                    !- Apply Wednesday
+  Yes,                                    !- Apply Thursday
+  Yes;                                    !- Apply Friday
+
+OS:Schedule:Day,
+  {cae50817-efde-4cf0-ba42-f26c88540d23}, !- Handle
+  Medium Office Lighting Weekdays Schedule, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  5,                                      !- Hour 1
+  0,                                      !- Minute 1
+  0.05,                                   !- Value Until Time 1
+  7,                                      !- Hour 2
+  0,                                      !- Minute 2
+  0.1,                                    !- Value Until Time 2
+  8,                                      !- Hour 3
+  0,                                      !- Minute 3
+  0.3,                                    !- Value Until Time 3
+  17,                                     !- Hour 4
+  0,                                      !- Minute 4
+  0.9,                                    !- Value Until Time 4
+  18,                                     !- Hour 5
+  0,                                      !- Minute 5
+  0.7,                                    !- Value Until Time 5
+  20,                                     !- Hour 6
+  0,                                      !- Minute 6
+  0.5,                                    !- Value Until Time 6
+  22,                                     !- Hour 7
+  0,                                      !- Minute 7
+  0.3,                                    !- Value Until Time 7
+  23,                                     !- Hour 8
+  0,                                      !- Minute 8
+  0.1,                                    !- Value Until Time 8
+  24,                                     !- Hour 9
+  0,                                      !- Minute 9
+  0.05;                                   !- Value Until Time 9
+
+OS:Schedule:Rule,
+  {55995440-6cf2-4eb4-afe7-892dfbcf2e6d}, !- Handle
+  Medium Office Lighting Schedule Saturday Rule, !- Name
+  {122868d6-b02c-4d77-bd09-0dd6db8c2b67}, !- Schedule Ruleset Name
+  0,                                      !- Rule Order
+  {3068fcd0-3de6-4ab5-9958-f52595185b68}, !- Day Schedule Name
+  ,                                       !- Apply Sunday
+  ,                                       !- Apply Monday
+  ,                                       !- Apply Tuesday
+  ,                                       !- Apply Wednesday
+  ,                                       !- Apply Thursday
+  ,                                       !- Apply Friday
+  Yes;                                    !- Apply Saturday
+
+OS:Schedule:Day,
+  {3068fcd0-3de6-4ab5-9958-f52595185b68}, !- Handle
+  Medium Office Lighting Saturday Schedule, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  6,                                      !- Hour 1
+  0,                                      !- Minute 1
+  0.05,                                   !- Value Until Time 1
+  8,                                      !- Hour 2
+  0,                                      !- Minute 2
+  0.1,                                    !- Value Until Time 2
+  14,                                     !- Hour 3
+  0,                                      !- Minute 3
+  0.5,                                    !- Value Until Time 3
+  17,                                     !- Hour 4
+  0,                                      !- Minute 4
+  0.15,                                   !- Value Until Time 4
+  24,                                     !- Hour 5
+  0,                                      !- Minute 5
+  0.05;                                   !- Value Until Time 5
+
+OS:Schedule:Ruleset,
+  {d102ddc0-b56d-4d45-94f9-32a83bc51fd9}, !- Handle
+  Medium Office Electric Equipment Schedule, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  {f8a6b858-ff3a-4ff3-b885-4cc396824377}, !- Default Day Schedule Name
+  {7975b42d-c261-47f9-bfb1-a1fc40c31aa7}, !- Summer Design Day Schedule Name
+  {6c760e67-9c0f-4b49-887e-f7a80e2c91d1}; !- Winter Design Day Schedule Name
+
+OS:Schedule:Day,
+  {f8a6b858-ff3a-4ff3-b885-4cc396824377}, !- Handle
+  Medium Office Electric Equipment All Other Days Schedule, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  24,                                     !- Hour 1
+  0,                                      !- Minute 1
+  0.3;                                    !- Value Until Time 1
+
+OS:Schedule:Day,
+  {5e1ad041-d4d7-4294-bd31-f31d4e13d3b4}, !- Handle
+  Medium Office Electric Equipment Summer Design Day Schedule, !- Name
+  ,                                       !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  6,                                      !- Hour 1
+  0,                                      !- Minute 1
+  1,                                      !- Value Until Time 1
+  24,                                     !- Hour 2
+  0,                                      !- Minute 2
+  0;                                      !- Value Until Time 2
+
+OS:Schedule:Day,
+  {7975b42d-c261-47f9-bfb1-a1fc40c31aa7}, !- Handle
+  Medium Office Electric Equipment Summer Design Day Schedule 1, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  6,                                      !- Hour 1
+  0,                                      !- Minute 1
+  1,                                      !- Value Until Time 1
+  24,                                     !- Hour 2
+  0,                                      !- Minute 2
+  0;                                      !- Value Until Time 2
+
+OS:Schedule:Day,
+  {640a05f8-19c0-42dd-8000-a476cb8093a9}, !- Handle
+  Medium Office Electric Equipment Winter Design Day Schedule, !- Name
+  ,                                       !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  24,                                     !- Hour 1
+  0,                                      !- Minute 1
+  0;                                      !- Value Until Time 1
+
+OS:Schedule:Day,
+  {6c760e67-9c0f-4b49-887e-f7a80e2c91d1}, !- Handle
+  Medium Office Electric Equipment Winter Design Day Schedule 1, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  24,                                     !- Hour 1
+  0,                                      !- Minute 1
+  0;                                      !- Value Until Time 1
+
+OS:Schedule:Rule,
+  {7bf44e7e-795a-4f74-bd27-764bbaba91ba}, !- Handle
+  Medium Office Electric Equipment Schedule Weekdays Rule, !- Name
+  {d102ddc0-b56d-4d45-94f9-32a83bc51fd9}, !- Schedule Ruleset Name
+  1,                                      !- Rule Order
+  {99958113-9c44-414a-9de6-cad10e8bcb18}, !- Day Schedule Name
+  ,                                       !- Apply Sunday
+  Yes,                                    !- Apply Monday
+  Yes,                                    !- Apply Tuesday
+  Yes,                                    !- Apply Wednesday
+  Yes,                                    !- Apply Thursday
+  Yes;                                    !- Apply Friday
+
+OS:Schedule:Day,
+  {99958113-9c44-414a-9de6-cad10e8bcb18}, !- Handle
+  Medium Office Electric Equipment Weekdays Schedule, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  8,                                      !- Hour 1
+  0,                                      !- Minute 1
+  0.4,                                    !- Value Until Time 1
+  12,                                     !- Hour 2
+  0,                                      !- Minute 2
+  0.9,                                    !- Value Until Time 2
+  13,                                     !- Hour 3
+  0,                                      !- Minute 3
+  0.8,                                    !- Value Until Time 3
+  17,                                     !- Hour 4
+  0,                                      !- Minute 4
+  0.9,                                    !- Value Until Time 4
+  18,                                     !- Hour 5
+  0,                                      !- Minute 5
+  0.8,                                    !- Value Until Time 5
+  20,                                     !- Hour 6
+  0,                                      !- Minute 6
+  0.6,                                    !- Value Until Time 6
+  22,                                     !- Hour 7
+  0,                                      !- Minute 7
+  0.5,                                    !- Value Until Time 7
+  24,                                     !- Hour 8
+  0,                                      !- Minute 8
+  0.4;                                    !- Value Until Time 8
+
+OS:Schedule:Rule,
+  {56d35191-69bf-4ded-ab7c-474a88957f2c}, !- Handle
+  Medium Office Electric Equipment Schedule Saturday Rule, !- Name
+  {d102ddc0-b56d-4d45-94f9-32a83bc51fd9}, !- Schedule Ruleset Name
+  0,                                      !- Rule Order
+  {3722c63b-6d22-4203-abf6-88a71b667471}, !- Day Schedule Name
+  ,                                       !- Apply Sunday
+  ,                                       !- Apply Monday
+  ,                                       !- Apply Tuesday
+  ,                                       !- Apply Wednesday
+  ,                                       !- Apply Thursday
+  ,                                       !- Apply Friday
+  Yes;                                    !- Apply Saturday
+
+OS:Schedule:Day,
+  {3722c63b-6d22-4203-abf6-88a71b667471}, !- Handle
+  Medium Office Electric Equipment Saturday Schedule, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  6,                                      !- Hour 1
+  0,                                      !- Minute 1
+  0.3,                                    !- Value Until Time 1
+  8,                                      !- Hour 2
+  0,                                      !- Minute 2
+  0.4,                                    !- Value Until Time 2
+  14,                                     !- Hour 3
+  0,                                      !- Minute 3
+  0.5,                                    !- Value Until Time 3
+  17,                                     !- Hour 4
+  0,                                      !- Minute 4
+  0.35,                                   !- Value Until Time 4
+  24,                                     !- Hour 5
+  0,                                      !- Minute 5
+  0.3;                                    !- Value Until Time 5
+
+OS:Schedule:Ruleset,
+  {6c55330b-333e-4352-9f6e-8552d1fd84b9}, !- Handle
+  Medium Office Gas Equipment Schedule,   !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  {d271259f-6bf7-4619-9a0c-fd388f87e7bf}, !- Default Day Schedule Name
+  {ebd9c6b5-cbdc-4200-8ccc-98a5c0874655}, !- Summer Design Day Schedule Name
+  {5177fb7b-187b-4d70-8a1e-49f8367981a4}; !- Winter Design Day Schedule Name
+
+OS:Schedule:Day,
+  {d271259f-6bf7-4619-9a0c-fd388f87e7bf}, !- Handle
+  Medium Office Gas Equipment All Other Days Schedule, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  24,                                     !- Hour 1
+  0,                                      !- Minute 1
+  0.3;                                    !- Value Until Time 1
+
+OS:Schedule:Day,
+  {489afd76-7a9f-416a-9fa3-189bff07f36f}, !- Handle
+  Medium Office Gas Equipment Summer Design Day Schedule, !- Name
+  ,                                       !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  6,                                      !- Hour 1
+  0,                                      !- Minute 1
+  1,                                      !- Value Until Time 1
+  24,                                     !- Hour 2
+  0,                                      !- Minute 2
+  0;                                      !- Value Until Time 2
+
+OS:Schedule:Day,
+  {ebd9c6b5-cbdc-4200-8ccc-98a5c0874655}, !- Handle
+  Medium Office Gas Equipment Summer Design Day Schedule 1, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  6,                                      !- Hour 1
+  0,                                      !- Minute 1
+  1,                                      !- Value Until Time 1
+  24,                                     !- Hour 2
+  0,                                      !- Minute 2
+  0;                                      !- Value Until Time 2
+
+OS:Schedule:Day,
+  {365ed231-a0a3-45e9-b217-f1b75e1a35b1}, !- Handle
+  Medium Office Gas Equipment Winter Design Day Schedule, !- Name
+  ,                                       !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  24,                                     !- Hour 1
+  0,                                      !- Minute 1
+  0;                                      !- Value Until Time 1
+
+OS:Schedule:Day,
+  {5177fb7b-187b-4d70-8a1e-49f8367981a4}, !- Handle
+  Medium Office Gas Equipment Winter Design Day Schedule 1, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  24,                                     !- Hour 1
+  0,                                      !- Minute 1
+  0;                                      !- Value Until Time 1
+
+OS:Schedule:Rule,
+  {918e3034-565e-48c2-8680-6b937f428f0e}, !- Handle
+  Medium Office Gas Equipment Schedule Weekdays Rule, !- Name
+  {6c55330b-333e-4352-9f6e-8552d1fd84b9}, !- Schedule Ruleset Name
+  1,                                      !- Rule Order
+  {82804dc7-a9e2-4272-9089-2db00f46edcd}, !- Day Schedule Name
+  ,                                       !- Apply Sunday
+  Yes,                                    !- Apply Monday
+  Yes,                                    !- Apply Tuesday
+  Yes,                                    !- Apply Wednesday
+  Yes,                                    !- Apply Thursday
+  Yes;                                    !- Apply Friday
+
+OS:Schedule:Day,
+  {82804dc7-a9e2-4272-9089-2db00f46edcd}, !- Handle
+  Medium Office Gas Equipment Weekdays Schedule, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  8,                                      !- Hour 1
+  0,                                      !- Minute 1
+  0.4,                                    !- Value Until Time 1
+  12,                                     !- Hour 2
+  0,                                      !- Minute 2
+  0.9,                                    !- Value Until Time 2
+  13,                                     !- Hour 3
+  0,                                      !- Minute 3
+  0.8,                                    !- Value Until Time 3
+  17,                                     !- Hour 4
+  0,                                      !- Minute 4
+  0.9,                                    !- Value Until Time 4
+  18,                                     !- Hour 5
+  0,                                      !- Minute 5
+  0.8,                                    !- Value Until Time 5
+  20,                                     !- Hour 6
+  0,                                      !- Minute 6
+  0.6,                                    !- Value Until Time 6
+  22,                                     !- Hour 7
+  0,                                      !- Minute 7
+  0.5,                                    !- Value Until Time 7
+  24,                                     !- Hour 8
+  0,                                      !- Minute 8
+  0.4;                                    !- Value Until Time 8
+
+OS:Schedule:Rule,
+  {d3bdb1bc-9269-487d-bd77-0378e268f5e7}, !- Handle
+  Medium Office Gas Equipment Schedule Saturday Rule, !- Name
+  {6c55330b-333e-4352-9f6e-8552d1fd84b9}, !- Schedule Ruleset Name
+  0,                                      !- Rule Order
+  {e7d88faa-282b-4831-8215-52b485c8b248}, !- Day Schedule Name
+  ,                                       !- Apply Sunday
+  ,                                       !- Apply Monday
+  ,                                       !- Apply Tuesday
+  ,                                       !- Apply Wednesday
+  ,                                       !- Apply Thursday
+  ,                                       !- Apply Friday
+  Yes;                                    !- Apply Saturday
+
+OS:Schedule:Day,
+  {e7d88faa-282b-4831-8215-52b485c8b248}, !- Handle
+  Medium Office Gas Equipment Saturday Schedule, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  6,                                      !- Hour 1
+  0,                                      !- Minute 1
+  0.3,                                    !- Value Until Time 1
+  8,                                      !- Hour 2
+  0,                                      !- Minute 2
+  0.4,                                    !- Value Until Time 2
+  14,                                     !- Hour 3
+  0,                                      !- Minute 3
+  0.5,                                    !- Value Until Time 3
+  17,                                     !- Hour 4
+  0,                                      !- Minute 4
+  0.35,                                   !- Value Until Time 4
+  24,                                     !- Hour 5
+  0,                                      !- Minute 5
+  0.3;                                    !- Value Until Time 5
+
+OS:Schedule:Ruleset,
+  {a82adb05-983a-45da-8edf-1631bd981ef3}, !- Handle
+  Medium Office Hot Water Equipment Schedule, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  {f4d8ca36-6e05-429c-82d6-90bec348b5e5}, !- Default Day Schedule Name
+  {07036ab6-a0ee-4077-a800-025faf243238}, !- Summer Design Day Schedule Name
+  {0aa26ced-1869-48ee-a06e-d5a802449edc}; !- Winter Design Day Schedule Name
+
+OS:Schedule:Day,
+  {f4d8ca36-6e05-429c-82d6-90bec348b5e5}, !- Handle
+  Medium Office Hot Water Equipment Default Schedule, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  5,                                      !- Hour 1
+  0,                                      !- Minute 1
+  0.04,                                   !- Value Until Time 1
+  6,                                      !- Hour 2
+  0,                                      !- Minute 2
+  0.07,                                   !- Value Until Time 2
+  11,                                     !- Hour 3
+  0,                                      !- Minute 3
+  0.04,                                   !- Value Until Time 3
+  13,                                     !- Hour 4
+  0,                                      !- Minute 4
+  0.06,                                   !- Value Until Time 4
+  14,                                     !- Hour 5
+  0,                                      !- Minute 5
+  0.09,                                   !- Value Until Time 5
+  15,                                     !- Hour 6
+  0,                                      !- Minute 6
+  0.06,                                   !- Value Until Time 6
+  21,                                     !- Hour 7
+  0,                                      !- Minute 7
+  0.04,                                   !- Value Until Time 7
+  22,                                     !- Hour 8
+  0,                                      !- Minute 8
+  0.07,                                   !- Value Until Time 8
+  24,                                     !- Hour 9
+  0,                                      !- Minute 9
+  0.04;                                   !- Value Until Time 9
+
+OS:Schedule:Day,
+  {dbfd9190-b157-43a7-9d33-ac85938ede7a}, !- Handle
+  Medium Office Hot Water Equipment Summer Design Day Schedule, !- Name
+  ,                                       !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  5,                                      !- Hour 1
+  0,                                      !- Minute 1
+  0.05,                                   !- Value Until Time 1
+  6,                                      !- Hour 2
+  0,                                      !- Minute 2
+  0.08,                                   !- Value Until Time 2
+  7,                                      !- Hour 3
+  0,                                      !- Minute 3
+  0.07,                                   !- Value Until Time 3
+  8,                                      !- Hour 4
+  0,                                      !- Minute 4
+  0.19,                                   !- Value Until Time 4
+  9,                                      !- Hour 5
+  0,                                      !- Minute 5
+  0.35,                                   !- Value Until Time 5
+  10,                                     !- Hour 6
+  0,                                      !- Minute 6
+  0.38,                                   !- Value Until Time 6
+  11,                                     !- Hour 7
+  0,                                      !- Minute 7
+  0.39,                                   !- Value Until Time 7
+  12,                                     !- Hour 8
+  0,                                      !- Minute 8
+  0.47,                                   !- Value Until Time 8
+  13,                                     !- Hour 9
+  0,                                      !- Minute 9
+  0.57,                                   !- Value Until Time 9
+  14,                                     !- Hour 10
+  0,                                      !- Minute 10
+  0.54,                                   !- Value Until Time 10
+  15,                                     !- Hour 11
+  0,                                      !- Minute 11
+  0.34,                                   !- Value Until Time 11
+  16,                                     !- Hour 12
+  0,                                      !- Minute 12
+  0.33,                                   !- Value Until Time 12
+  17,                                     !- Hour 13
+  0,                                      !- Minute 13
+  0.44,                                   !- Value Until Time 13
+  18,                                     !- Hour 14
+  0,                                      !- Minute 14
+  0.26,                                   !- Value Until Time 14
+  19,                                     !- Hour 15
+  0,                                      !- Minute 15
+  0.21,                                   !- Value Until Time 15
+  20,                                     !- Hour 16
+  0,                                      !- Minute 16
+  0.15,                                   !- Value Until Time 16
+  21,                                     !- Hour 17
+  0,                                      !- Minute 17
+  0.17,                                   !- Value Until Time 17
+  22,                                     !- Hour 18
+  0,                                      !- Minute 18
+  0.08,                                   !- Value Until Time 18
+  24,                                     !- Hour 19
+  0,                                      !- Minute 19
+  0.05;                                   !- Value Until Time 19
+
+OS:Schedule:Day,
+  {07036ab6-a0ee-4077-a800-025faf243238}, !- Handle
+  Medium Office Hot Water Equipment Summer Design Day Schedule 1, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  5,                                      !- Hour 1
+  0,                                      !- Minute 1
+  0.05,                                   !- Value Until Time 1
+  6,                                      !- Hour 2
+  0,                                      !- Minute 2
+  0.08,                                   !- Value Until Time 2
+  7,                                      !- Hour 3
+  0,                                      !- Minute 3
+  0.07,                                   !- Value Until Time 3
+  8,                                      !- Hour 4
+  0,                                      !- Minute 4
+  0.19,                                   !- Value Until Time 4
+  9,                                      !- Hour 5
+  0,                                      !- Minute 5
+  0.35,                                   !- Value Until Time 5
+  10,                                     !- Hour 6
+  0,                                      !- Minute 6
+  0.38,                                   !- Value Until Time 6
+  11,                                     !- Hour 7
+  0,                                      !- Minute 7
+  0.39,                                   !- Value Until Time 7
+  12,                                     !- Hour 8
+  0,                                      !- Minute 8
+  0.47,                                   !- Value Until Time 8
+  13,                                     !- Hour 9
+  0,                                      !- Minute 9
+  0.57,                                   !- Value Until Time 9
+  14,                                     !- Hour 10
+  0,                                      !- Minute 10
+  0.54,                                   !- Value Until Time 10
+  15,                                     !- Hour 11
+  0,                                      !- Minute 11
+  0.34,                                   !- Value Until Time 11
+  16,                                     !- Hour 12
+  0,                                      !- Minute 12
+  0.33,                                   !- Value Until Time 12
+  17,                                     !- Hour 13
+  0,                                      !- Minute 13
+  0.44,                                   !- Value Until Time 13
+  18,                                     !- Hour 14
+  0,                                      !- Minute 14
+  0.26,                                   !- Value Until Time 14
+  19,                                     !- Hour 15
+  0,                                      !- Minute 15
+  0.21,                                   !- Value Until Time 15
+  20,                                     !- Hour 16
+  0,                                      !- Minute 16
+  0.15,                                   !- Value Until Time 16
+  21,                                     !- Hour 17
+  0,                                      !- Minute 17
+  0.17,                                   !- Value Until Time 17
+  22,                                     !- Hour 18
+  0,                                      !- Minute 18
+  0.08,                                   !- Value Until Time 18
+  24,                                     !- Hour 19
+  0,                                      !- Minute 19
+  0.05;                                   !- Value Until Time 19
+
+OS:Schedule:Rule,
+  {6d036340-94be-4b3a-ab65-7d5896d73f0a}, !- Handle
+  Medium Office Hot Water Schedule Weekdays Rule, !- Name
+  {a82adb05-983a-45da-8edf-1631bd981ef3}, !- Schedule Ruleset Name
+  1,                                      !- Rule Order
+  {e356e2b1-4dd0-48af-bedf-342484ac60df}, !- Day Schedule Name
+  ,                                       !- Apply Sunday
+  Yes,                                    !- Apply Monday
+  Yes,                                    !- Apply Tuesday
+  Yes,                                    !- Apply Wednesday
+  Yes,                                    !- Apply Thursday
+  Yes;                                    !- Apply Friday
+
+OS:Schedule:Day,
+  {e356e2b1-4dd0-48af-bedf-342484ac60df}, !- Handle
+  Medium Office Hot Water Equipment Weekdays Schedule, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  5,                                      !- Hour 1
+  0,                                      !- Minute 1
+  0.05,                                   !- Value Until Time 1
+  6,                                      !- Hour 2
+  0,                                      !- Minute 2
+  0.08,                                   !- Value Until Time 2
+  7,                                      !- Hour 3
+  0,                                      !- Minute 3
+  0.07,                                   !- Value Until Time 3
+  8,                                      !- Hour 4
+  0,                                      !- Minute 4
+  0.19,                                   !- Value Until Time 4
+  9,                                      !- Hour 5
+  0,                                      !- Minute 5
+  0.35,                                   !- Value Until Time 5
+  10,                                     !- Hour 6
+  0,                                      !- Minute 6
+  0.38,                                   !- Value Until Time 6
+  11,                                     !- Hour 7
+  0,                                      !- Minute 7
+  0.39,                                   !- Value Until Time 7
+  12,                                     !- Hour 8
+  0,                                      !- Minute 8
+  0.47,                                   !- Value Until Time 8
+  13,                                     !- Hour 9
+  0,                                      !- Minute 9
+  0.57,                                   !- Value Until Time 9
+  14,                                     !- Hour 10
+  0,                                      !- Minute 10
+  0.54,                                   !- Value Until Time 10
+  15,                                     !- Hour 11
+  0,                                      !- Minute 11
+  0.34,                                   !- Value Until Time 11
+  16,                                     !- Hour 12
+  0,                                      !- Minute 12
+  0.33,                                   !- Value Until Time 12
+  17,                                     !- Hour 13
+  0,                                      !- Minute 13
+  0.44,                                   !- Value Until Time 13
+  18,                                     !- Hour 14
+  0,                                      !- Minute 14
+  0.26,                                   !- Value Until Time 14
+  19,                                     !- Hour 15
+  0,                                      !- Minute 15
+  0.21,                                   !- Value Until Time 15
+  20,                                     !- Hour 16
+  0,                                      !- Minute 16
+  0.15,                                   !- Value Until Time 16
+  21,                                     !- Hour 17
+  0,                                      !- Minute 17
+  0.17,                                   !- Value Until Time 17
+  22,                                     !- Hour 18
+  0,                                      !- Minute 18
+  0.08,                                   !- Value Until Time 18
+  24,                                     !- Hour 19
+  0,                                      !- Minute 19
+  0.05;                                   !- Value Until Time 19
+
+OS:Schedule:Day,
+  {04ea0cd2-a815-4cb8-b634-5d948a5fe323}, !- Handle
+  Medium Office Hot Water Equipment Winter Design Day Schedule, !- Name
+  ,                                       !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  5,                                      !- Hour 1
+  0,                                      !- Minute 1
+  0.05,                                   !- Value Until Time 1
+  6,                                      !- Hour 2
+  0,                                      !- Minute 2
+  0.08,                                   !- Value Until Time 2
+  7,                                      !- Hour 3
+  0,                                      !- Minute 3
+  0.07,                                   !- Value Until Time 3
+  8,                                      !- Hour 4
+  0,                                      !- Minute 4
+  0.11,                                   !- Value Until Time 4
+  9,                                      !- Hour 5
+  0,                                      !- Minute 5
+  0.15,                                   !- Value Until Time 5
+  10,                                     !- Hour 6
+  0,                                      !- Minute 6
+  0.21,                                   !- Value Until Time 6
+  11,                                     !- Hour 7
+  0,                                      !- Minute 7
+  0.19,                                   !- Value Until Time 7
+  12,                                     !- Hour 8
+  0,                                      !- Minute 8
+  0.23,                                   !- Value Until Time 8
+  13,                                     !- Hour 9
+  0,                                      !- Minute 9
+  0.2,                                    !- Value Until Time 9
+  14,                                     !- Hour 10
+  0,                                      !- Minute 10
+  0.19,                                   !- Value Until Time 10
+  15,                                     !- Hour 11
+  0,                                      !- Minute 11
+  0.15,                                   !- Value Until Time 11
+  16,                                     !- Hour 12
+  0,                                      !- Minute 12
+  0.13,                                   !- Value Until Time 12
+  17,                                     !- Hour 13
+  0,                                      !- Minute 13
+  0.14,                                   !- Value Until Time 13
+  21,                                     !- Hour 14
+  0,                                      !- Minute 14
+  0.07,                                   !- Value Until Time 14
+  22,                                     !- Hour 15
+  0,                                      !- Minute 15
+  0.09,                                   !- Value Until Time 15
+  24,                                     !- Hour 16
+  0,                                      !- Minute 16
+  0.05;                                   !- Value Until Time 16
+
+OS:Schedule:Day,
+  {0aa26ced-1869-48ee-a06e-d5a802449edc}, !- Handle
+  Medium Office Hot Water Equipment Winter Design Day Schedule 1, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  5,                                      !- Hour 1
+  0,                                      !- Minute 1
+  0.05,                                   !- Value Until Time 1
+  6,                                      !- Hour 2
+  0,                                      !- Minute 2
+  0.08,                                   !- Value Until Time 2
+  7,                                      !- Hour 3
+  0,                                      !- Minute 3
+  0.07,                                   !- Value Until Time 3
+  8,                                      !- Hour 4
+  0,                                      !- Minute 4
+  0.11,                                   !- Value Until Time 4
+  9,                                      !- Hour 5
+  0,                                      !- Minute 5
+  0.15,                                   !- Value Until Time 5
+  10,                                     !- Hour 6
+  0,                                      !- Minute 6
+  0.21,                                   !- Value Until Time 6
+  11,                                     !- Hour 7
+  0,                                      !- Minute 7
+  0.19,                                   !- Value Until Time 7
+  12,                                     !- Hour 8
+  0,                                      !- Minute 8
+  0.23,                                   !- Value Until Time 8
+  13,                                     !- Hour 9
+  0,                                      !- Minute 9
+  0.2,                                    !- Value Until Time 9
+  14,                                     !- Hour 10
+  0,                                      !- Minute 10
+  0.19,                                   !- Value Until Time 10
+  15,                                     !- Hour 11
+  0,                                      !- Minute 11
+  0.15,                                   !- Value Until Time 11
+  16,                                     !- Hour 12
+  0,                                      !- Minute 12
+  0.13,                                   !- Value Until Time 12
+  17,                                     !- Hour 13
+  0,                                      !- Minute 13
+  0.14,                                   !- Value Until Time 13
+  21,                                     !- Hour 14
+  0,                                      !- Minute 14
+  0.07,                                   !- Value Until Time 14
+  22,                                     !- Hour 15
+  0,                                      !- Minute 15
+  0.09,                                   !- Value Until Time 15
+  24,                                     !- Hour 16
+  0,                                      !- Minute 16
+  0.05;                                   !- Value Until Time 16
+
+OS:Schedule:Rule,
+  {3dc796a4-8413-40c2-bae1-f1df836dfbea}, !- Handle
+  Medium Office Hot Water Schedule Saturday Rule, !- Name
+  {a82adb05-983a-45da-8edf-1631bd981ef3}, !- Schedule Ruleset Name
+  0,                                      !- Rule Order
+  {9a2a60f6-e9ad-4c76-ab28-2a44e5776b99}, !- Day Schedule Name
+  ,                                       !- Apply Sunday
+  ,                                       !- Apply Monday
+  ,                                       !- Apply Tuesday
+  ,                                       !- Apply Wednesday
+  ,                                       !- Apply Thursday
+  ,                                       !- Apply Friday
+  Yes;                                    !- Apply Saturday
+
+OS:Schedule:Day,
+  {9a2a60f6-e9ad-4c76-ab28-2a44e5776b99}, !- Handle
+  Medium Office Hot Water Equipment Saturday Schedule, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  5,                                      !- Hour 1
+  0,                                      !- Minute 1
+  0.05,                                   !- Value Until Time 1
+  6,                                      !- Hour 2
+  0,                                      !- Minute 2
+  0.08,                                   !- Value Until Time 2
+  7,                                      !- Hour 3
+  0,                                      !- Minute 3
+  0.07,                                   !- Value Until Time 3
+  8,                                      !- Hour 4
+  0,                                      !- Minute 4
+  0.11,                                   !- Value Until Time 4
+  9,                                      !- Hour 5
+  0,                                      !- Minute 5
+  0.15,                                   !- Value Until Time 5
+  10,                                     !- Hour 6
+  0,                                      !- Minute 6
+  0.21,                                   !- Value Until Time 6
+  11,                                     !- Hour 7
+  0,                                      !- Minute 7
+  0.19,                                   !- Value Until Time 7
+  12,                                     !- Hour 8
+  0,                                      !- Minute 8
+  0.23,                                   !- Value Until Time 8
+  13,                                     !- Hour 9
+  0,                                      !- Minute 9
+  0.2,                                    !- Value Until Time 9
+  14,                                     !- Hour 10
+  0,                                      !- Minute 10
+  0.19,                                   !- Value Until Time 10
+  15,                                     !- Hour 11
+  0,                                      !- Minute 11
+  0.15,                                   !- Value Until Time 11
+  16,                                     !- Hour 12
+  0,                                      !- Minute 12
+  0.13,                                   !- Value Until Time 12
+  17,                                     !- Hour 13
+  0,                                      !- Minute 13
+  0.14,                                   !- Value Until Time 13
+  21,                                     !- Hour 14
+  0,                                      !- Minute 14
+  0.07,                                   !- Value Until Time 14
+  22,                                     !- Hour 15
+  0,                                      !- Minute 15
+  0.09,                                   !- Value Until Time 15
+  24,                                     !- Hour 16
+  0,                                      !- Minute 16
+  0.05;                                   !- Value Until Time 16
+
+OS:Schedule:Ruleset,
+  {3894555f-e927-4bc3-a4b5-6e5425e9f9da}, !- Handle
+  Medium Office Infiltration Schedule,    !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  {07e0bf03-f282-40c7-bd74-e5ea9617b2e2}, !- Default Day Schedule Name
+  {cd49224b-b682-43b5-bc7d-4ed689b00f90}, !- Summer Design Day Schedule Name
+  {2248c622-faf0-483e-a969-0a26ca79999d}; !- Winter Design Day Schedule Name
+
+OS:Schedule:Day,
+  {07e0bf03-f282-40c7-bd74-e5ea9617b2e2}, !- Handle
+  Medium Office Infiltration Default Schedule, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  24,                                     !- Hour 1
+  0,                                      !- Minute 1
+  1;                                      !- Value Until Time 1
+
+OS:Schedule:Day,
+  {de7e7116-d065-48af-a569-d3f2277661f0}, !- Handle
+  Medium Office Infiltration Summer Design Day Schedule, !- Name
+  ,                                       !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  6,                                      !- Hour 1
+  0,                                      !- Minute 1
+  1,                                      !- Value Until Time 1
+  22,                                     !- Hour 2
+  0,                                      !- Minute 2
+  0.25,                                   !- Value Until Time 2
+  24,                                     !- Hour 3
+  0,                                      !- Minute 3
+  1;                                      !- Value Until Time 3
+
+OS:Schedule:Day,
+  {cd49224b-b682-43b5-bc7d-4ed689b00f90}, !- Handle
+  Medium Office Infiltration Summer Design Day Schedule 1, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  6,                                      !- Hour 1
+  0,                                      !- Minute 1
+  1,                                      !- Value Until Time 1
+  22,                                     !- Hour 2
+  0,                                      !- Minute 2
+  0.25,                                   !- Value Until Time 2
+  24,                                     !- Hour 3
+  0,                                      !- Minute 3
+  1;                                      !- Value Until Time 3
+
+OS:Schedule:Rule,
+  {f454bac2-13c4-459a-b3dd-93f691322093}, !- Handle
+  Medium Office Infiltration Schedule Weekdays Rule, !- Name
+  {3894555f-e927-4bc3-a4b5-6e5425e9f9da}, !- Schedule Ruleset Name
+  1,                                      !- Rule Order
+  {1492a6ed-3f4e-48bf-b441-1f330db949e9}, !- Day Schedule Name
+  ,                                       !- Apply Sunday
+  Yes,                                    !- Apply Monday
+  Yes,                                    !- Apply Tuesday
+  Yes,                                    !- Apply Wednesday
+  Yes,                                    !- Apply Thursday
+  Yes;                                    !- Apply Friday
+
+OS:Schedule:Day,
+  {1492a6ed-3f4e-48bf-b441-1f330db949e9}, !- Handle
+  Medium Office Infiltration Weekdays Schedule, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  6,                                      !- Hour 1
+  0,                                      !- Minute 1
+  1,                                      !- Value Until Time 1
+  22,                                     !- Hour 2
+  0,                                      !- Minute 2
+  0.25,                                   !- Value Until Time 2
+  24,                                     !- Hour 3
+  0,                                      !- Minute 3
+  1;                                      !- Value Until Time 3
+
+OS:Schedule:Day,
+  {769bd461-2858-4de0-96d2-a4d84d698438}, !- Handle
+  Medium Office Infiltration Winter Design Day Schedule, !- Name
+  ,                                       !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  6,                                      !- Hour 1
+  0,                                      !- Minute 1
+  1,                                      !- Value Until Time 1
+  18,                                     !- Hour 2
+  0,                                      !- Minute 2
+  0.25,                                   !- Value Until Time 2
+  24,                                     !- Hour 3
+  0,                                      !- Minute 3
+  1;                                      !- Value Until Time 3
+
+OS:Schedule:Day,
+  {2248c622-faf0-483e-a969-0a26ca79999d}, !- Handle
+  Medium Office Infiltration Winter Design Day Schedule 1, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  6,                                      !- Hour 1
+  0,                                      !- Minute 1
+  1,                                      !- Value Until Time 1
+  18,                                     !- Hour 2
+  0,                                      !- Minute 2
+  0.25,                                   !- Value Until Time 2
+  24,                                     !- Hour 3
+  0,                                      !- Minute 3
+  1;                                      !- Value Until Time 3
+
+OS:Schedule:Rule,
+  {073347af-16a8-48c2-b8e4-563cfad54810}, !- Handle
+  Medium Office Infiltration Schedule Saturday Rule, !- Name
+  {3894555f-e927-4bc3-a4b5-6e5425e9f9da}, !- Schedule Ruleset Name
+  0,                                      !- Rule Order
+  {6836cae7-a144-405f-8f1d-11b5e5d17226}, !- Day Schedule Name
+  ,                                       !- Apply Sunday
+  ,                                       !- Apply Monday
+  ,                                       !- Apply Tuesday
+  ,                                       !- Apply Wednesday
+  ,                                       !- Apply Thursday
+  ,                                       !- Apply Friday
+  Yes;                                    !- Apply Saturday
+
+OS:Schedule:Day,
+  {6836cae7-a144-405f-8f1d-11b5e5d17226}, !- Handle
+  Medium Office Infiltration Saturday Schedule, !- Name
+  {e1417495-0923-49bc-8a1e-0fe3989380a2}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  6,                                      !- Hour 1
+  0,                                      !- Minute 1
+  1,                                      !- Value Until Time 1
+  18,                                     !- Hour 2
+  0,                                      !- Minute 2
+  0.25,                                   !- Value Until Time 2
+  24,                                     !- Hour 3
+  0,                                      !- Minute 3
+  1;                                      !- Value Until Time 3
+
+OS:Schedule:Ruleset,
+  {cda6882b-c7c7-458a-b5c6-144ec27b53dd}, !- Handle
+  Medium Office Cooling Setpoint Schedule, !- Name
+  {9fb242af-866e-43b2-9e36-20f2ec391279}, !- Schedule Type Limits Name
+  {c8b291ba-f9e4-47ca-9b4d-7c29c0761b12}, !- Default Day Schedule Name
+  {4708b24b-6936-42e4-82a1-22e9f24878ca}; !- Summer Design Day Schedule Name
+
+OS:Schedule:Day,
+  {c8b291ba-f9e4-47ca-9b4d-7c29c0761b12}, !- Handle
+  Medium Office Cooling Setpoint All Other Days Schedule, !- Name
+  {9fb242af-866e-43b2-9e36-20f2ec391279}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  24,                                     !- Hour 1
+  0,                                      !- Minute 1
+  26.7;                                   !- Value Until Time 1
+
+OS:Schedule:Day,
+  {b655d968-2ef7-4324-a791-b5ccaafc4cc9}, !- Handle
+  Medium Office Cooling Setpoint Summer Design Day Schedule, !- Name
+  {9fb242af-866e-43b2-9e36-20f2ec391279}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  6,                                      !- Hour 1
+  0,                                      !- Minute 1
+  26.7,                                   !- Value Until Time 1
+  22,                                     !- Hour 2
+  0,                                      !- Minute 2
+  24,                                     !- Value Until Time 2
+  24,                                     !- Hour 3
+  0,                                      !- Minute 3
+  26.7;                                   !- Value Until Time 3
+
+OS:Schedule:Day,
+  {4708b24b-6936-42e4-82a1-22e9f24878ca}, !- Handle
+  Medium Office Cooling Setpoint Summer Design Day Schedule 1, !- Name
+  {9fb242af-866e-43b2-9e36-20f2ec391279}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  6,                                      !- Hour 1
+  0,                                      !- Minute 1
+  26.7,                                   !- Value Until Time 1
+  22,                                     !- Hour 2
+  0,                                      !- Minute 2
+  24,                                     !- Value Until Time 2
+  24,                                     !- Hour 3
+  0,                                      !- Minute 3
+  26.7;                                   !- Value Until Time 3
+
+OS:Schedule:Rule,
+  {5bba6e6b-9ae9-4806-805d-2813211d2d6c}, !- Handle
+  Medium Office Cooling Setpoint Schedule Weekdays Rule, !- Name
+  {cda6882b-c7c7-458a-b5c6-144ec27b53dd}, !- Schedule Ruleset Name
+  1,                                      !- Rule Order
+  {0b24c860-2173-433e-b4f4-79fc36d2fbd7}, !- Day Schedule Name
+  ,                                       !- Apply Sunday
+  Yes,                                    !- Apply Monday
+  Yes,                                    !- Apply Tuesday
+  Yes,                                    !- Apply Wednesday
+  Yes,                                    !- Apply Thursday
+  Yes;                                    !- Apply Friday
+
+OS:Schedule:Day,
+  {0b24c860-2173-433e-b4f4-79fc36d2fbd7}, !- Handle
+  Medium Office Cooling Setpoint Weekdays Schedule, !- Name
+  {9fb242af-866e-43b2-9e36-20f2ec391279}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  6,                                      !- Hour 1
+  0,                                      !- Minute 1
+  26.7,                                   !- Value Until Time 1
+  22,                                     !- Hour 2
+  0,                                      !- Minute 2
+  24,                                     !- Value Until Time 2
+  24,                                     !- Hour 3
+  0,                                      !- Minute 3
+  26.7;                                   !- Value Until Time 3
+
+OS:Schedule:Rule,
+  {bd885f7e-5905-4702-9cb7-2dff91570e48}, !- Handle
+  Medium Office Cooling Setpoint Schedule Saturday Rule, !- Name
+  {cda6882b-c7c7-458a-b5c6-144ec27b53dd}, !- Schedule Ruleset Name
+  0,                                      !- Rule Order
+  {7bd4975e-4f42-4fd5-a55e-9f4ffb10dd1d}, !- Day Schedule Name
+  ,                                       !- Apply Sunday
+  ,                                       !- Apply Monday
+  ,                                       !- Apply Tuesday
+  ,                                       !- Apply Wednesday
+  ,                                       !- Apply Thursday
+  ,                                       !- Apply Friday
+  Yes;                                    !- Apply Saturday
+
+OS:Schedule:Day,
+  {7bd4975e-4f42-4fd5-a55e-9f4ffb10dd1d}, !- Handle
+  Medium Office Cooling Setpoint Saturday Schedule, !- Name
+  {9fb242af-866e-43b2-9e36-20f2ec391279}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  6,                                      !- Hour 1
+  0,                                      !- Minute 1
+  26.7,                                   !- Value Until Time 1
+  18,                                     !- Hour 2
+  0,                                      !- Minute 2
+  24,                                     !- Value Until Time 2
+  24,                                     !- Hour 3
+  0,                                      !- Minute 3
+  26.7;                                   !- Value Until Time 3
+
+OS:Schedule:Ruleset,
+  {7e126aeb-7f98-4b95-84ef-4ee09e2b70eb}, !- Handle
+  Medium Office Heating Setpoint Schedule, !- Name
+  {9fb242af-866e-43b2-9e36-20f2ec391279}, !- Schedule Type Limits Name
+  {0852037c-84dd-4821-b49a-95c270f41e08}, !- Default Day Schedule Name
+  ,                                       !- Summer Design Day Schedule Name
+  {0949d5e8-0f9e-4ae2-b2f2-7c8183dc14fe}; !- Winter Design Day Schedule Name
+
+OS:Schedule:Day,
+  {0852037c-84dd-4821-b49a-95c270f41e08}, !- Handle
+  Medium Office Heating Setpoint All Other Days Schedule, !- Name
+  {9fb242af-866e-43b2-9e36-20f2ec391279}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  24,                                     !- Hour 1
+  0,                                      !- Minute 1
+  15.6;                                   !- Value Until Time 1
+
+OS:Schedule:Day,
+  {498a71a9-1139-4f24-8ac7-3f4fae7d4df8}, !- Handle
+  Medium Office Heating Setpoint Winter Design Day Schedule, !- Name
+  {9fb242af-866e-43b2-9e36-20f2ec391279}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  24,                                     !- Hour 1
+  0,                                      !- Minute 1
+  21;                                     !- Value Until Time 1
+
+OS:Schedule:Day,
+  {0949d5e8-0f9e-4ae2-b2f2-7c8183dc14fe}, !- Handle
+  Medium Office Heating Setpoint Winter Design Day Schedule 1, !- Name
+  {9fb242af-866e-43b2-9e36-20f2ec391279}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  24,                                     !- Hour 1
+  0,                                      !- Minute 1
+  21;                                     !- Value Until Time 1
+
+OS:Schedule:Rule,
+  {e2441fed-b90a-433d-a3f6-c492bd255096}, !- Handle
+  Medium Office Heating Setpoint Schedule Weekdays Rule, !- Name
+  {7e126aeb-7f98-4b95-84ef-4ee09e2b70eb}, !- Schedule Ruleset Name
+  1,                                      !- Rule Order
+  {13393cdf-bf5a-4915-a98e-94e82efdd9e1}, !- Day Schedule Name
+  ,                                       !- Apply Sunday
+  Yes,                                    !- Apply Monday
+  Yes,                                    !- Apply Tuesday
+  Yes,                                    !- Apply Wednesday
+  Yes,                                    !- Apply Thursday
+  Yes;                                    !- Apply Friday
+
+OS:Schedule:Day,
+  {13393cdf-bf5a-4915-a98e-94e82efdd9e1}, !- Handle
+  Medium Office Heating Setpoint Weekdays Schedule, !- Name
+  {9fb242af-866e-43b2-9e36-20f2ec391279}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  6,                                      !- Hour 1
+  0,                                      !- Minute 1
+  15.6,                                   !- Value Until Time 1
+  22,                                     !- Hour 2
+  0,                                      !- Minute 2
+  21,                                     !- Value Until Time 2
+  24,                                     !- Hour 3
+  0,                                      !- Minute 3
+  15.6;                                   !- Value Until Time 3
+
+OS:Schedule:Rule,
+  {2f7b2dd2-2dcb-449e-8cff-8d14eda07d25}, !- Handle
+  Medium Office Heating Setpoint Schedule Saturday Rule, !- Name
+  {7e126aeb-7f98-4b95-84ef-4ee09e2b70eb}, !- Schedule Ruleset Name
+  0,                                      !- Rule Order
+  {720e954f-d916-4ad1-9295-2a428d72dcd6}, !- Day Schedule Name
+  ,                                       !- Apply Sunday
+  ,                                       !- Apply Monday
+  ,                                       !- Apply Tuesday
+  ,                                       !- Apply Wednesday
+  ,                                       !- Apply Thursday
+  ,                                       !- Apply Friday
+  Yes;                                    !- Apply Saturday
+
+OS:Schedule:Day,
+  {720e954f-d916-4ad1-9295-2a428d72dcd6}, !- Handle
+  Medium Office Heating Setpoint Saturday Schedule, !- Name
+  {9fb242af-866e-43b2-9e36-20f2ec391279}, !- Schedule Type Limits Name
+  ,                                       !- Interpolate to Timestep
+  6,                                      !- Hour 1
+  0,                                      !- Minute 1
+  15.6,                                   !- Value Until Time 1
+  18,                                     !- Hour 2
+  0,                                      !- Minute 2
+  21,                                     !- Value Until Time 2
+  24,                                     !- Hour 3
+  0,                                      !- Minute 3
+  15.6;                                   !- Value Until Time 3
+
+OS:DefaultConstructionSet,
+  {7b7aef3f-c9aa-4c55-9af4-4b5c8b0f7dc8}, !- Handle
+  Default Constructions,                  !- Name
+  {808fd8f7-af9a-452e-9ea9-d2be87f650ec}, !- Default Exterior Surface Constructions Name
+  {1bd8b5da-c530-41e7-bc5b-c59d79f22a75}, !- Default Interior Surface Constructions Name
+  {ce428980-9c44-4b34-a281-6da7b6b01295}, !- Default Ground Contact Surface Constructions Name
+  {54006d19-ccef-48f1-b745-56c6f500bd2c}, !- Default Exterior SubSurface Constructions Name
+  {06b8308f-8f7a-49f2-8739-589f1b951054}, !- Default Interior SubSurface Constructions Name
+  {09532d4c-944b-423e-acf3-558eb899fb6b}, !- Interior Partition Construction Name
+  ,                                       !- Space Shading Construction Name
+  ,                                       !- Building Shading Construction Name
+  ,                                       !- Site Shading Construction Name
+  ;                                       !- Adiabatic Surface Construction Name
+
+OS:DefaultSurfaceConstructions,
+  {808fd8f7-af9a-452e-9ea9-d2be87f650ec}, !- Handle
+  Exterior Surface Constructions,         !- Name
+  ,                                       !- Floor Construction Name
+  {99a23b6a-496d-4bbf-8db9-4c428f727222}, !- Wall Construction Name
+  {b98babe1-ab4e-4b0c-9235-d3cc70476bd5}; !- Roof Ceiling Construction Name
+
+OS:DefaultSurfaceConstructions,
+  {1bd8b5da-c530-41e7-bc5b-c59d79f22a75}, !- Handle
+  Interior Surface Constructions,         !- Name
+  {005991cb-5000-4416-8d1e-ce56d09fa87d}, !- Floor Construction Name
+  {0453c067-aad3-45e3-8ec6-bc856cc1db11}, !- Wall Construction Name
+  {70c796c5-9453-4a09-b244-2de01bb15b14}; !- Roof Ceiling Construction Name
+
+OS:DefaultSurfaceConstructions,
+  {ce428980-9c44-4b34-a281-6da7b6b01295}, !- Handle
+  Ground Contact Surface Constructions,   !- Name
+  {b6edc526-a486-498b-bfa4-c21c631002fc}, !- Floor Construction Name
+  ,                                       !- Wall Construction Name
+  ;                                       !- Roof Ceiling Construction Name
+
+OS:DefaultSubSurfaceConstructions,
+  {54006d19-ccef-48f1-b745-56c6f500bd2c}, !- Handle
+  Exterior SubSurface Constructions,      !- Name
+  {a28d8850-53f4-42a8-8f89-8b486d4881aa}, !- Fixed Window Construction Name
+  {a28d8850-53f4-42a8-8f89-8b486d4881aa}, !- Operable Window Construction Name
+  {9fc87e4e-fe1c-48e4-a3a8-67c6ed480ff7}, !- Door Construction Name
+  {a28d8850-53f4-42a8-8f89-8b486d4881aa}, !- Glass Door Construction Name
+  {9fc87e4e-fe1c-48e4-a3a8-67c6ed480ff7}, !- Overhead Door Construction Name
+  {a28d8850-53f4-42a8-8f89-8b486d4881aa}, !- Skylight Construction Name
+  {a28d8850-53f4-42a8-8f89-8b486d4881aa}, !- Tubular Daylight Dome Construction Name
+  {a28d8850-53f4-42a8-8f89-8b486d4881aa}; !- Tubular Daylight Diffuser Construction Name
+
+OS:DefaultSubSurfaceConstructions,
+  {06b8308f-8f7a-49f2-8739-589f1b951054}, !- Handle
+  Interior SubSurface Constructions,      !- Name
+  {9516d4dc-75d5-4398-ad40-301901c265bb}, !- Fixed Window Construction Name
+  {9516d4dc-75d5-4398-ad40-301901c265bb}, !- Operable Window Construction Name
+  {d2d21bc9-553d-40ed-b9db-442e0b1cd03b}, !- Door Construction Name
+  {9516d4dc-75d5-4398-ad40-301901c265bb}, !- Glass Door Construction Name
+  {d2d21bc9-553d-40ed-b9db-442e0b1cd03b}, !- Overhead Door Construction Name
+  {9516d4dc-75d5-4398-ad40-301901c265bb}, !- Skylight Construction Name
+  {9516d4dc-75d5-4398-ad40-301901c265bb}, !- Tubular Daylight Dome Construction Name
+  {9516d4dc-75d5-4398-ad40-301901c265bb}; !- Tubular Daylight Diffuser Construction Name
+
+OS:Material,
+  {09f75c43-6768-48d2-8f37-bc8fec3b1d2f}, !- Handle
+  M01 100mm brick,                        !- Name
+  MediumRough,                            !- Roughness
+  0.1016,                                 !- Thickness {m}
+  0.89,                                   !- Conductivity {W/m-K}
+  1920,                                   !- Density {kg/m3}
+  790;                                    !- Specific Heat {J/kg-K}
+
+OS:Material,
+  {7ab09d6e-c05e-45d5-8ae0-c9ecdcac43ff}, !- Handle
+  M15 200mm heavyweight concrete,         !- Name
+  MediumRough,                            !- Roughness
+  0.2032,                                 !- Thickness {m}
+  1.95,                                   !- Conductivity {W/m-K}
+  2240,                                   !- Density {kg/m3}
+  900;                                    !- Specific Heat {J/kg-K}
+
+OS:Material,
+  {871c1829-02fb-463f-9d57-720d67b6797f}, !- Handle
+  I02 50mm insulation board,              !- Name
+  MediumRough,                            !- Roughness
+  0.0508,                                 !- Thickness {m}
+  0.03,                                   !- Conductivity {W/m-K}
+  43,                                     !- Density {kg/m3}
+  1210;                                   !- Specific Heat {J/kg-K}
+
+OS:Material:AirGap,
+  {d7ae65e8-245c-4373-9a58-3be5a56abbf1}, !- Handle
+  F04 Wall air space resistance,          !- Name
+  0.15;                                   !- Thermal Resistance {m2-K/W}
+
+OS:Material,
+  {c7fbe9a1-0afb-480a-9133-ff5a044a6cb8}, !- Handle
+  G01a 19mm gypsum board,                 !- Name
+  MediumSmooth,                           !- Roughness
+  0.019,                                  !- Thickness {m}
+  0.16,                                   !- Conductivity {W/m-K}
+  800,                                    !- Density {kg/m3}
+  1090;                                   !- Specific Heat {J/kg-K}
+
+OS:Construction,
+  {99a23b6a-496d-4bbf-8db9-4c428f727222}, !- Handle
+  Exterior Wall,                          !- Name
+  ,                                       !- Surface Rendering Name
+  {09f75c43-6768-48d2-8f37-bc8fec3b1d2f}, !- Layer 1
+  {7ab09d6e-c05e-45d5-8ae0-c9ecdcac43ff}, !- Layer 2
+  {871c1829-02fb-463f-9d57-720d67b6797f}, !- Layer 3
+  {d7ae65e8-245c-4373-9a58-3be5a56abbf1}, !- Layer 4
+  {c7fbe9a1-0afb-480a-9133-ff5a044a6cb8}; !- Layer 5
+
+OS:StandardsInformation:Construction,
+  {6c527291-5275-434b-8e1d-38f5d2f0096c}, !- Handle
+  {99a23b6a-496d-4bbf-8db9-4c428f727222}, !- Construction Name
+  ,                                       !- Intended Surface Type
+  ,                                       !- Standards Construction Type
+  2,                                      !- Perturbable Layer
+  Insulation,                             !- Perturbable Layer Type
+  ;                                       !- Other Perturbable Layer Type
+
+OS:Material,
+  {000ad83e-50c1-4288-a6b0-a1cd55e304d7}, !- Handle
+  M11 100mm lightweight concrete,         !- Name
+  MediumRough,                            !- Roughness
+  0.1016,                                 !- Thickness {m}
+  0.53,                                   !- Conductivity {W/m-K}
+  1280,                                   !- Density {kg/m3}
+  840;                                    !- Specific Heat {J/kg-K}
+
+OS:Material:AirGap,
+  {fdf5676d-da9f-4d0c-ae01-8dbc1271590d}, !- Handle
+  F05 Ceiling air space resistance,       !- Name
+  0.18;                                   !- Thermal Resistance {m2-K/W}
+
+OS:Material,
+  {93efd558-9d84-492d-9a03-ae7d3ef24455}, !- Handle
+  F16 Acoustic tile,                      !- Name
+  MediumSmooth,                           !- Roughness
+  0.0191,                                 !- Thickness {m}
+  0.06,                                   !- Conductivity {W/m-K}
+  368,                                    !- Density {kg/m3}
+  590;                                    !- Specific Heat {J/kg-K}
+
+OS:Construction,
+  {b98babe1-ab4e-4b0c-9235-d3cc70476bd5}, !- Handle
+  Exterior Roof,                          !- Name
+  ,                                       !- Surface Rendering Name
+  {000ad83e-50c1-4288-a6b0-a1cd55e304d7}, !- Layer 1
+  {fdf5676d-da9f-4d0c-ae01-8dbc1271590d}, !- Layer 2
+  {93efd558-9d84-492d-9a03-ae7d3ef24455}; !- Layer 3
+
+OS:Construction,
+  {005991cb-5000-4416-8d1e-ce56d09fa87d}, !- Handle
+  Interior Floor,                         !- Name
+  ,                                       !- Surface Rendering Name
+  {93efd558-9d84-492d-9a03-ae7d3ef24455}, !- Layer 1
+  {fdf5676d-da9f-4d0c-ae01-8dbc1271590d}, !- Layer 2
+  {000ad83e-50c1-4288-a6b0-a1cd55e304d7}; !- Layer 3
+
+OS:Construction:AirBoundary,
+  {0453c067-aad3-45e3-8ec6-bc856cc1db11}, !- Handle
+  Air Wall,                               !- Name
+  ,                                       !- Air Exchange Method
+  0;                                      !- Simple Mixing Air Changes per Hour {1/hr}
+
+OS:Construction,
+  {70c796c5-9453-4a09-b244-2de01bb15b14}, !- Handle
+  Interior Ceiling,                       !- Name
+  ,                                       !- Surface Rendering Name
+  {000ad83e-50c1-4288-a6b0-a1cd55e304d7}, !- Layer 1
+  {fdf5676d-da9f-4d0c-ae01-8dbc1271590d}, !- Layer 2
+  {93efd558-9d84-492d-9a03-ae7d3ef24455}; !- Layer 3
+
+OS:Material,
+  {b083cf5a-4a67-4daf-9da1-d4d05f48842b}, !- Handle
+  MAT-CC05 8 HW CONCRETE,                 !- Name
+  Rough,                                  !- Roughness
+  0.2032,                                 !- Thickness {m}
+  1.311,                                  !- Conductivity {W/m-K}
+  2240,                                   !- Density {kg/m3}
+  836.8,                                  !- Specific Heat {J/kg-K}
+  0.9,                                    !- Thermal Absorptance
+  0.7,                                    !- Solar Absorptance
+  0.7;                                    !- Visible Absorptance
+
+OS:Material:NoMass,
+  {ad46aead-1a5b-4bfb-a339-f6282206a3d1}, !- Handle
+  CP02 CARPET PAD,                        !- Name
+  VeryRough,                              !- Roughness
+  0.2165,                                 !- Thermal Resistance {m2-K/W}
+  0.9,                                    !- Thermal Absorptance
+  0.7,                                    !- Solar Absorptance
+  0.8;                                    !- Visible Absorptance
+
+OS:Construction,
+  {b6edc526-a486-498b-bfa4-c21c631002fc}, !- Handle
+  Slab,                                   !- Name
+  ,                                       !- Surface Rendering Name
+  {b083cf5a-4a67-4daf-9da1-d4d05f48842b}, !- Layer 1
+  {ad46aead-1a5b-4bfb-a339-f6282206a3d1}; !- Layer 2
+
+OS:WindowMaterial:SimpleGlazingSystem,
+  {b5b609ca-4154-4dca-bbf8-c128fc70d504}, !- Handle
+  Simple Glazing,                         !- Name
+  3.23646,                                !- U-Factor {W/m2-K}
+  0.39,                                   !- Solar Heat Gain Coefficient
+  0.6;                                    !- Visible Transmittance
+
+OS:WindowMaterial:Glazing,
+  {fb4bc998-b00f-4ec8-8ef1-af6578376ed0}, !- Handle
+  Clear 3mm,                              !- Name
+  SpectralAverage,                        !- Optical Data Type
+  ,                                       !- Window Glass Spectral Data Set Name
+  0.003,                                  !- Thickness {m}
+  0.837,                                  !- Solar Transmittance at Normal Incidence
+  0.075,                                  !- Front Side Solar Reflectance at Normal Incidence
+  0.075,                                  !- Back Side Solar Reflectance at Normal Incidence
+  0.898,                                  !- Visible Transmittance at Normal Incidence
+  0.081,                                  !- Front Side Visible Reflectance at Normal Incidence
+  0.081,                                  !- Back Side Visible Reflectance at Normal Incidence
+  0,                                      !- Infrared Transmittance at Normal Incidence
+  0.084,                                  !- Front Side Infrared Hemispherical Emissivity
+  0.084,                                  !- Back Side Infrared Hemispherical Emissivity
+  0.9;                                    !- Conductivity {W/m-K}
+
+OS:WindowMaterial:Gas,
+  {b96f5699-337a-409d-9344-7888b13913e7}, !- Handle
+  Air 13mm,                               !- Name
+  Air,                                    !- Gas Type
+  0.0127;                                 !- Thickness {m}
+
+OS:Construction,
+  {a28d8850-53f4-42a8-8f89-8b486d4881aa}, !- Handle
+  Exterior Window,                        !- Name
+  ,                                       !- Surface Rendering Name
+  {b5b609ca-4154-4dca-bbf8-c128fc70d504}; !- Layer 1
+
+OS:Material,
+  {2f8772b4-fe33-4158-989a-5b388a05a763}, !- Handle
+  F08 Metal surface,                      !- Name
+  Smooth,                                 !- Roughness
+  0.0008,                                 !- Thickness {m}
+  45.28,                                  !- Conductivity {W/m-K}
+  7824,                                   !- Density {kg/m3}
+  500;                                    !- Specific Heat {J/kg-K}
+
+OS:Material,
+  {2b4e8d31-5f0d-432c-9f2b-a98dc68d753b}, !- Handle
+  I02 25mm insulation board,              !- Name
+  MediumRough,                            !- Roughness
+  0.0254,                                 !- Thickness {m}
+  0.03,                                   !- Conductivity {W/m-K}
+  43,                                     !- Density {kg/m3}
+  1210;                                   !- Specific Heat {J/kg-K}
+
+OS:Construction,
+  {9fc87e4e-fe1c-48e4-a3a8-67c6ed480ff7}, !- Handle
+  Exterior Door,                          !- Name
+  ,                                       !- Surface Rendering Name
+  {2f8772b4-fe33-4158-989a-5b388a05a763}, !- Layer 1
+  {2b4e8d31-5f0d-432c-9f2b-a98dc68d753b}; !- Layer 2
+
+OS:StandardsInformation:Construction,
+  {1c3ef1f9-8e14-4d19-99d4-baeda66e7b0b}, !- Handle
+  {9fc87e4e-fe1c-48e4-a3a8-67c6ed480ff7}, !- Construction Name
+  ,                                       !- Intended Surface Type
+  ,                                       !- Standards Construction Type
+  1,                                      !- Perturbable Layer
+  Insulation,                             !- Perturbable Layer Type
+  ;                                       !- Other Perturbable Layer Type
+
+OS:Construction,
+  {9516d4dc-75d5-4398-ad40-301901c265bb}, !- Handle
+  Interior Window,                        !- Name
+  ,                                       !- Surface Rendering Name
+  {b5b609ca-4154-4dca-bbf8-c128fc70d504}; !- Layer 1
+
+OS:Material,
+  {f95f0223-97e3-4936-b60e-10d1b84236cc}, !- Handle
+  G05 25mm wood,                          !- Name
+  MediumSmooth,                           !- Roughness
+  0.0254,                                 !- Thickness {m}
+  0.15,                                   !- Conductivity {W/m-K}
+  608,                                    !- Density {kg/m3}
+  1630;                                   !- Specific Heat {J/kg-K}
+
+OS:Construction,
+  {d2d21bc9-553d-40ed-b9db-442e0b1cd03b}, !- Handle
+  Interior Door,                          !- Name
+  ,                                       !- Surface Rendering Name
+  {f95f0223-97e3-4936-b60e-10d1b84236cc}; !- Layer 1
+
+OS:Construction,
+  {09532d4c-944b-423e-acf3-558eb899fb6b}, !- Handle
+  Interior Partition,                     !- Name
+  ,                                       !- Surface Rendering Name
+  {f95f0223-97e3-4936-b60e-10d1b84236cc}; !- Layer 1
+
+OS:SpaceType,
+  {182f9537-8465-403d-8db3-3e81250289ca}, !- Handle
+  Space Type 1;                           !- Name
+
+OS:Lights:Definition,
+  {505ead44-7a43-4ddd-8c56-c7a7470d2fb2}, !- Handle
+  Lights Definition 1,                    !- Name
+  Watts/Area,                             !- Design Level Calculation Method
+  ,                                       !- Lighting Level {W}
+  10,                                     !- Watts per Space Floor Area {W/m2}
+  ;                                       !- Watts per Person {W/person}
+
+OS:Lights,
+  {a18b6e6b-7b80-4d17-9908-b9c86cf90372}, !- Handle
+  Lights 1,                               !- Name
+  {505ead44-7a43-4ddd-8c56-c7a7470d2fb2}, !- Lights Definition Name
+  {182f9537-8465-403d-8db3-3e81250289ca}, !- Space or SpaceType Name
+  ,                                       !- Schedule Name
+  1,                                      !- Fraction Replaceable
+  ,                                       !- Multiplier
+  General;                                !- End-Use Subcategory
+
+OS:ElectricEquipment:Definition,
+  {a7da90c0-16b4-42bd-902b-95451e172b9b}, !- Handle
+  Electric Equipment Definition 1,        !- Name
+  Watts/Area,                             !- Design Level Calculation Method
+  ,                                       !- Design Level {W}
+  5,                                      !- Watts per Space Floor Area {W/m2}
+  ;                                       !- Watts per Person {W/person}
+
+OS:ElectricEquipment,
+  {99ad3d41-d8c9-4a29-b89b-26f462d4a3ab}, !- Handle
+  Electric Equipment 1,                   !- Name
+  {a7da90c0-16b4-42bd-902b-95451e172b9b}, !- Electric Equipment Definition Name
+  {182f9537-8465-403d-8db3-3e81250289ca}, !- Space or SpaceType Name
+  ,                                       !- Schedule Name
+  ,                                       !- Multiplier
+  General;                                !- End-Use Subcategory
+
+OS:People:Definition,
+  {ee9c3a0b-e643-4c27-85d8-1b5bf2d4c5be}, !- Handle
+  People Definition 1,                    !- Name
+  People/Area,                            !- Number of People Calculation Method
+  ,                                       !- Number of People {people}
+  0.05,                                   !- People per Space Floor Area {person/m2}
+  ,                                       !- Space Floor Area per Person {m2/person}
+  0.3;                                    !- Fraction Radiant
+
+OS:People,
+  {1c7cf3e9-bdda-473e-bb17-eb45785a9141}, !- Handle
+  People 1,                               !- Name
+  {ee9c3a0b-e643-4c27-85d8-1b5bf2d4c5be}, !- People Definition Name
+  {182f9537-8465-403d-8db3-3e81250289ca}, !- Space or SpaceType Name
+  ,                                       !- Number of People Schedule Name
+  ,                                       !- Activity Level Schedule Name
+  ,                                       !- Surface Name/Angle Factor List Name
+  ,                                       !- Work Efficiency Schedule Name
+  ,                                       !- Clothing Insulation Schedule Name
+  ,                                       !- Air Velocity Schedule Name
+  1;                                      !- Multiplier
+
+OS:Facility,
+  {41fd00d7-2828-4c68-af96-ef8d1743e3ef}; !- Handle
+
+OS:Building,
+  {94ef9c3c-9508-4ea7-ba27-5460f8f6f724}, !- Handle
+  Building 1,                             !- Name
+  ,                                       !- Building Sector Type
+  ,                                       !- North Axis {deg}
+  ,                                       !- Nominal Floor to Floor Height {m}
+  {182f9537-8465-403d-8db3-3e81250289ca}, !- Space Type Name
+  {2abf4457-9986-4dcb-858e-9ce8c65c48b3}, !- Default Construction Set Name
+  {c0328153-1299-4213-b6f7-8540652d4880}; !- Default Schedule Set Name
+
+OS:ThermalZone,
+  {a89ba35e-b1a0-496b-9210-5fa1050abf06}, !- Handle
+  Thermal Zone 1,                         !- Name
+  ,                                       !- Multiplier
+  ,                                       !- Ceiling Height {m}
+  ,                                       !- Volume {m3}
+  ,                                       !- Floor Area {m2}
+  ,                                       !- Zone Inside Convection Algorithm
+  ,                                       !- Zone Outside Convection Algorithm
+  ,                                       !- Zone Conditioning Equipment List Name
+  {2bbc1941-6efa-47a8-bd93-836729fb1e55}, !- Zone Air Inlet Port List
+  {6d20646c-85e8-4004-bbe0-ec4e57a1ee3d}, !- Zone Air Exhaust Port List
+  {8a275627-f971-480b-ba85-fc51aee9e9db}, !- Zone Air Node Name
+  {c43169be-563e-4698-acc9-5ae5f6db17db}, !- Zone Return Air Port List
+  {5a95532d-2df3-42e3-8946-75ac5d206df9}, !- Primary Daylighting Control Name
+  0.25,                                   !- Fraction of Zone Controlled by Primary Daylighting Control
+  ,                                       !- Secondary Daylighting Control Name
+  ,                                       !- Fraction of Zone Controlled by Secondary Daylighting Control
+  {c214c17e-34b3-4cc1-868c-775fbf965702}, !- Illuminance Map Name
+  ,                                       !- Group Rendering Name
+  {c2157f5c-3a86-4952-b906-bbd475968061}, !- Thermostat Name
+  No;                                     !- Use Ideal Air Loads
+
+OS:Node,
+  {521860cc-1a6d-4d0d-9afe-63585d0b28c1}, !- Handle
+  Node 1,                                 !- Name
+  {8a275627-f971-480b-ba85-fc51aee9e9db}, !- Inlet Port
+  ;                                       !- Outlet Port
+
+OS:Connection,
+  {8a275627-f971-480b-ba85-fc51aee9e9db}, !- Handle
+  {a89ba35e-b1a0-496b-9210-5fa1050abf06}, !- Source Object
+  11,                                     !- Outlet Port
+  {521860cc-1a6d-4d0d-9afe-63585d0b28c1}, !- Target Object
+  2;                                      !- Inlet Port
+
+OS:PortList,
+  {2bbc1941-6efa-47a8-bd93-836729fb1e55}, !- Handle
+  {a89ba35e-b1a0-496b-9210-5fa1050abf06}, !- HVAC Component
+  {dd931870-f1f9-4740-9271-21fcb21bb217}; !- Port 1
+
+OS:PortList,
+  {6d20646c-85e8-4004-bbe0-ec4e57a1ee3d}, !- Handle
+  {a89ba35e-b1a0-496b-9210-5fa1050abf06}; !- HVAC Component
+
+OS:PortList,
+  {c43169be-563e-4698-acc9-5ae5f6db17db}, !- Handle
+  {a89ba35e-b1a0-496b-9210-5fa1050abf06}, !- HVAC Component
+  {007954eb-1a1e-4e60-804a-00edb0afedda}; !- Port 1
+
+OS:Sizing:Zone,
+  {57a24024-992f-46da-841d-f0c2db6b4584}, !- Handle
+  {a89ba35e-b1a0-496b-9210-5fa1050abf06}, !- Zone or ZoneList Name
+  SupplyAirTemperature,                   !- Zone Cooling Design Supply Air Temperature Input Method
+  14,                                     !- Zone Cooling Design Supply Air Temperature {C}
+  11.11,                                  !- Zone Cooling Design Supply Air Temperature Difference {deltaC}
+  SupplyAirTemperature,                   !- Zone Heating Design Supply Air Temperature Input Method
+  40,                                     !- Zone Heating Design Supply Air Temperature {C}
+  11.11,                                  !- Zone Heating Design Supply Air Temperature Difference {deltaC}
+  0.0085,                                 !- Zone Cooling Design Supply Air Humidity Ratio {kg-H2O/kg-air}
+  0.008,                                  !- Zone Heating Design Supply Air Humidity Ratio {kg-H2O/kg-air}
+  ,                                       !- Zone Heating Sizing Factor
+  ,                                       !- Zone Cooling Sizing Factor
+  DesignDay,                              !- Cooling Design Air Flow Method
+  ,                                       !- Cooling Design Air Flow Rate {m3/s}
+  ,                                       !- Cooling Minimum Air Flow per Zone Floor Area {m3/s-m2}
+  ,                                       !- Cooling Minimum Air Flow {m3/s}
+  ,                                       !- Cooling Minimum Air Flow Fraction
+  DesignDay,                              !- Heating Design Air Flow Method
+  ,                                       !- Heating Design Air Flow Rate {m3/s}
+  ,                                       !- Heating Maximum Air Flow per Zone Floor Area {m3/s-m2}
+  ,                                       !- Heating Maximum Air Flow {m3/s}
+  ,                                       !- Heating Maximum Air Flow Fraction
+  No,                                     !- Account for Dedicated Outdoor Air System
+  NeutralSupplyAir,                       !- Dedicated Outdoor Air System Control Strategy
+  autosize,                               !- Dedicated Outdoor Air Low Setpoint Temperature for Design {C}
+  autosize;                               !- Dedicated Outdoor Air High Setpoint Temperature for Design {C}
+
+OS:ZoneHVAC:EquipmentList,
+  {50477ce3-38a9-4b73-a92f-ec22f910cbaa}, !- Handle
+  Zone HVAC Equipment List 1,             !- Name
+  {a89ba35e-b1a0-496b-9210-5fa1050abf06}, !- Thermal Zone
+  ,                                       !- Load Distribution Scheme
+  {a88a9fb9-37e8-4ddc-9503-d9833d587a90}, !- Zone Equipment 1
+  1,                                      !- Zone Equipment Cooling Sequence 1
+  1,                                      !- Zone Equipment Heating or No-Load Sequence 1
+  ,                                       !- Zone Equipment Sequential Cooling Fraction Schedule Name 1
+  ;                                       !- Zone Equipment Sequential Heating Fraction Schedule Name 1
+
+OS:ThermostatSetpoint:DualSetpoint,
+  {c2157f5c-3a86-4952-b906-bbd475968061}, !- Handle
+  Thermostat Setpoint Dual Setpoint 1,    !- Name
+  {7e126aeb-7f98-4b95-84ef-4ee09e2b70eb}, !- Heating Setpoint Temperature Schedule Name
+  {cda6882b-c7c7-458a-b5c6-144ec27b53dd}; !- Cooling Setpoint Temperature Schedule Name
+
+OS:BuildingStory,
+  {4fc6ec11-6215-4146-b4a7-c9fbbb3ccaf0}, !- Handle
+  Building Story 1,                       !- Name
+  0,                                      !- Nominal Z Coordinate {m}
+  3,                                      !- Nominal Floor to Floor Height {m}
+  ,                                       !- Default Construction Set Name
+  ,                                       !- Default Schedule Set Name
+  ;                                       !- Group Rendering Name
+
+OS:Space,
+  {4ac98b87-e68b-4c6c-a98b-1d2cce7480de}, !- Handle
+  Space 1,                                !- Name
+  ,                                       !- Space Type Name
+  ,                                       !- Default Construction Set Name
+  ,                                       !- Default Schedule Set Name
+  ,                                       !- Direction of Relative North {deg}
+  ,                                       !- X Origin {m}
+  ,                                       !- Y Origin {m}
+  ,                                       !- Z Origin {m}
+  {4fc6ec11-6215-4146-b4a7-c9fbbb3ccaf0}, !- Building Story Name
+  {a89ba35e-b1a0-496b-9210-5fa1050abf06}; !- Thermal Zone Name
+
+OS:Surface,
+  {a3ecaa37-28cc-4a47-b55c-6571031c64b9}, !- Handle
+  Surface 1,                              !- Name
+  Floor,                                  !- Surface Type
+  ,                                       !- Construction Name
+  {4ac98b87-e68b-4c6c-a98b-1d2cce7480de}, !- Space Name
+  Ground,                                 !- Outside Boundary Condition
+  ,                                       !- Outside Boundary Condition Object
+  NoSun,                                  !- Sun Exposure
+  NoWind,                                 !- Wind Exposure
+  ,                                       !- View Factor to Ground
+  ,                                       !- Number of Vertices
+  0, 0, 0,                                !- X,Y,Z Vertex 1 {m}
+  0, 10, 0,                               !- X,Y,Z Vertex 2 {m}
+  10, 10, 0,                              !- X,Y,Z Vertex 3 {m}
+  10, 0, 0;                               !- X,Y,Z Vertex 4 {m}
+
+OS:Surface,
+  {b07dcb0a-aaed-47d6-ae4c-8d106c8fdeaf}, !- Handle
+  Surface 2,                              !- Name
+  Wall,                                   !- Surface Type
+  ,                                       !- Construction Name
+  {4ac98b87-e68b-4c6c-a98b-1d2cce7480de}, !- Space Name
+  Outdoors,                               !- Outside Boundary Condition
+  ,                                       !- Outside Boundary Condition Object
+  SunExposed,                             !- Sun Exposure
+  WindExposed,                            !- Wind Exposure
+  ,                                       !- View Factor to Ground
+  ,                                       !- Number of Vertices
+  0, 10, 3,                               !- X,Y,Z Vertex 1 {m}
+  0, 10, 0,                               !- X,Y,Z Vertex 2 {m}
+  0, 0, 0,                                !- X,Y,Z Vertex 3 {m}
+  0, 0, 3;                                !- X,Y,Z Vertex 4 {m}
+
+OS:Surface,
+  {0a3035a8-4f91-4615-9ede-b6269a2901c4}, !- Handle
+  Surface 3,                              !- Name
+  Wall,                                   !- Surface Type
+  ,                                       !- Construction Name
+  {4ac98b87-e68b-4c6c-a98b-1d2cce7480de}, !- Space Name
+  Surface,                                !- Outside Boundary Condition
+  {6596f879-72a8-4790-b521-c7eb872db76b}, !- Outside Boundary Condition Object
+  NoSun,                                  !- Sun Exposure
+  NoWind,                                 !- Wind Exposure
+  ,                                       !- View Factor to Ground
+  ,                                       !- Number of Vertices
+  10, 10, 3,                              !- X,Y,Z Vertex 1 {m}
+  10, 10, 0,                              !- X,Y,Z Vertex 2 {m}
+  0, 10, 0,                               !- X,Y,Z Vertex 3 {m}
+  0, 10, 3;                               !- X,Y,Z Vertex 4 {m}
+
+OS:Surface,
+  {94b173a0-46bf-4765-b38b-e782b3711d40}, !- Handle
+  Surface 4,                              !- Name
+  Wall,                                   !- Surface Type
+  ,                                       !- Construction Name
+  {4ac98b87-e68b-4c6c-a98b-1d2cce7480de}, !- Space Name
+  Surface,                                !- Outside Boundary Condition
+  {7e1c9d65-76ff-4308-b13a-b3d112ddb54a}, !- Outside Boundary Condition Object
+  NoSun,                                  !- Sun Exposure
+  NoWind,                                 !- Wind Exposure
+  ,                                       !- View Factor to Ground
+  ,                                       !- Number of Vertices
+  10, 0, 3,                               !- X,Y,Z Vertex 1 {m}
+  10, 0, 0,                               !- X,Y,Z Vertex 2 {m}
+  10, 10, 0,                              !- X,Y,Z Vertex 3 {m}
+  10, 10, 3;                              !- X,Y,Z Vertex 4 {m}
+
+OS:Surface,
+  {e13d947b-1295-4f2f-bfc7-35ba63eb1e33}, !- Handle
+  Surface 5,                              !- Name
+  Wall,                                   !- Surface Type
+  ,                                       !- Construction Name
+  {4ac98b87-e68b-4c6c-a98b-1d2cce7480de}, !- Space Name
+  Outdoors,                               !- Outside Boundary Condition
+  ,                                       !- Outside Boundary Condition Object
+  SunExposed,                             !- Sun Exposure
+  WindExposed,                            !- Wind Exposure
+  ,                                       !- View Factor to Ground
+  ,                                       !- Number of Vertices
+  0, 0, 3,                                !- X,Y,Z Vertex 1 {m}
+  0, 0, 0,                                !- X,Y,Z Vertex 2 {m}
+  10, 0, 0,                               !- X,Y,Z Vertex 3 {m}
+  10, 0, 3;                               !- X,Y,Z Vertex 4 {m}
+
+OS:Surface,
+  {ba47343f-45bd-4cc7-9d5b-59053c39d673}, !- Handle
+  Surface 6,                              !- Name
+  RoofCeiling,                            !- Surface Type
+  ,                                       !- Construction Name
+  {4ac98b87-e68b-4c6c-a98b-1d2cce7480de}, !- Space Name
+  Outdoors,                               !- Outside Boundary Condition
+  ,                                       !- Outside Boundary Condition Object
+  SunExposed,                             !- Sun Exposure
+  WindExposed,                            !- Wind Exposure
+  ,                                       !- View Factor to Ground
+  ,                                       !- Number of Vertices
+  10, 0, 3,                               !- X,Y,Z Vertex 1 {m}
+  10, 10, 3,                              !- X,Y,Z Vertex 2 {m}
+  0, 10, 3,                               !- X,Y,Z Vertex 3 {m}
+  0, 0, 3;                                !- X,Y,Z Vertex 4 {m}
+
+OS:Space,
+  {1c8cee02-5519-4c67-8f8e-11be54cdd68f}, !- Handle
+  Space 2,                                !- Name
+  ,                                       !- Space Type Name
+  ,                                       !- Default Construction Set Name
+  ,                                       !- Default Schedule Set Name
+  ,                                       !- Direction of Relative North {deg}
+  10,                                     !- X Origin {m}
+  ,                                       !- Y Origin {m}
+  ,                                       !- Z Origin {m}
+  {4fc6ec11-6215-4146-b4a7-c9fbbb3ccaf0}, !- Building Story Name
+  {a89ba35e-b1a0-496b-9210-5fa1050abf06}; !- Thermal Zone Name
+
+OS:Surface,
+  {52e21937-9d50-40ae-8ed4-4502fa80d611}, !- Handle
+  Surface 7,                              !- Name
+  Floor,                                  !- Surface Type
+  ,                                       !- Construction Name
+  {1c8cee02-5519-4c67-8f8e-11be54cdd68f}, !- Space Name
+  Ground,                                 !- Outside Boundary Condition
+  ,                                       !- Outside Boundary Condition Object
+  NoSun,                                  !- Sun Exposure
+  NoWind,                                 !- Wind Exposure
+  ,                                       !- View Factor to Ground
+  ,                                       !- Number of Vertices
+  0, 0, 0,                                !- X,Y,Z Vertex 1 {m}
+  0, 10, 0,                               !- X,Y,Z Vertex 2 {m}
+  10, 10, 0,                              !- X,Y,Z Vertex 3 {m}
+  10, 0, 0;                               !- X,Y,Z Vertex 4 {m}
+
+OS:Surface,
+  {64cbf499-6550-4086-94fd-269619dde4d4}, !- Handle
+  Surface 8,                              !- Name
+  Wall,                                   !- Surface Type
+  ,                                       !- Construction Name
+  {1c8cee02-5519-4c67-8f8e-11be54cdd68f}, !- Space Name
+  Surface,                                !- Outside Boundary Condition
+  {634a5854-b1c9-43ee-b047-cabba454ed8e}, !- Outside Boundary Condition Object
+  NoSun,                                  !- Sun Exposure
+  NoWind,                                 !- Wind Exposure
+  ,                                       !- View Factor to Ground
+  ,                                       !- Number of Vertices
+  10, 10, 3,                              !- X,Y,Z Vertex 1 {m}
+  10, 10, 0,                              !- X,Y,Z Vertex 2 {m}
+  0, 10, 0,                               !- X,Y,Z Vertex 3 {m}
+  0, 10, 3;                               !- X,Y,Z Vertex 4 {m}
+
+OS:Surface,
+  {48314984-b35f-4070-94a4-4273fed3be55}, !- Handle
+  Surface 9,                              !- Name
+  Wall,                                   !- Surface Type
+  ,                                       !- Construction Name
+  {1c8cee02-5519-4c67-8f8e-11be54cdd68f}, !- Space Name
+  Outdoors,                               !- Outside Boundary Condition
+  ,                                       !- Outside Boundary Condition Object
+  SunExposed,                             !- Sun Exposure
+  WindExposed,                            !- Wind Exposure
+  ,                                       !- View Factor to Ground
+  ,                                       !- Number of Vertices
+  10, 0, 3,                               !- X,Y,Z Vertex 1 {m}
+  10, 0, 0,                               !- X,Y,Z Vertex 2 {m}
+  10, 10, 0,                              !- X,Y,Z Vertex 3 {m}
+  10, 10, 3;                              !- X,Y,Z Vertex 4 {m}
+
+OS:Surface,
+  {7e1c9d65-76ff-4308-b13a-b3d112ddb54a}, !- Handle
+  Surface 10,                             !- Name
+  Wall,                                   !- Surface Type
+  ,                                       !- Construction Name
+  {1c8cee02-5519-4c67-8f8e-11be54cdd68f}, !- Space Name
+  Surface,                                !- Outside Boundary Condition
+  {94b173a0-46bf-4765-b38b-e782b3711d40}, !- Outside Boundary Condition Object
+  NoSun,                                  !- Sun Exposure
+  NoWind,                                 !- Wind Exposure
+  ,                                       !- View Factor to Ground
+  ,                                       !- Number of Vertices
+  0, 10, 3,                               !- X,Y,Z Vertex 1 {m}
+  0, 10, 0,                               !- X,Y,Z Vertex 2 {m}
+  0, 0, 0,                                !- X,Y,Z Vertex 3 {m}
+  0, 0, 3;                                !- X,Y,Z Vertex 4 {m}
+
+OS:Surface,
+  {5a3ac697-9d1b-48b9-94f0-b1763c5e0e13}, !- Handle
+  Surface 11,                             !- Name
+  Wall,                                   !- Surface Type
+  ,                                       !- Construction Name
+  {1c8cee02-5519-4c67-8f8e-11be54cdd68f}, !- Space Name
+  Outdoors,                               !- Outside Boundary Condition
+  ,                                       !- Outside Boundary Condition Object
+  SunExposed,                             !- Sun Exposure
+  WindExposed,                            !- Wind Exposure
+  ,                                       !- View Factor to Ground
+  ,                                       !- Number of Vertices
+  0, 0, 3,                                !- X,Y,Z Vertex 1 {m}
+  0, 0, 0,                                !- X,Y,Z Vertex 2 {m}
+  10, 0, 0,                               !- X,Y,Z Vertex 3 {m}
+  10, 0, 3;                               !- X,Y,Z Vertex 4 {m}
+
+OS:Surface,
+  {a1b988ae-bf2a-49ff-b9b9-f17ce19bd44b}, !- Handle
+  Surface 12,                             !- Name
+  RoofCeiling,                            !- Surface Type
+  ,                                       !- Construction Name
+  {1c8cee02-5519-4c67-8f8e-11be54cdd68f}, !- Space Name
+  Outdoors,                               !- Outside Boundary Condition
+  ,                                       !- Outside Boundary Condition Object
+  SunExposed,                             !- Sun Exposure
+  WindExposed,                            !- Wind Exposure
+  ,                                       !- View Factor to Ground
+  ,                                       !- Number of Vertices
+  10, 0, 3,                               !- X,Y,Z Vertex 1 {m}
+  10, 10, 3,                              !- X,Y,Z Vertex 2 {m}
+  0, 10, 3,                               !- X,Y,Z Vertex 3 {m}
+  0, 0, 3;                                !- X,Y,Z Vertex 4 {m}
+
+OS:Space,
+  {e278bc58-bfdf-4029-9c26-fa1f48f18a6f}, !- Handle
+  Space 3,                                !- Name
+  ,                                       !- Space Type Name
+  ,                                       !- Default Construction Set Name
+  ,                                       !- Default Schedule Set Name
+  ,                                       !- Direction of Relative North {deg}
+  ,                                       !- X Origin {m}
+  10,                                     !- Y Origin {m}
+  ,                                       !- Z Origin {m}
+  {4fc6ec11-6215-4146-b4a7-c9fbbb3ccaf0}, !- Building Story Name
+  {a89ba35e-b1a0-496b-9210-5fa1050abf06}; !- Thermal Zone Name
+
+OS:Surface,
+  {bfc3cbc2-62c4-424e-88ee-14aa3393c81d}, !- Handle
+  Surface 13,                             !- Name
+  Floor,                                  !- Surface Type
+  ,                                       !- Construction Name
+  {e278bc58-bfdf-4029-9c26-fa1f48f18a6f}, !- Space Name
+  Ground,                                 !- Outside Boundary Condition
+  ,                                       !- Outside Boundary Condition Object
+  NoSun,                                  !- Sun Exposure
+  NoWind,                                 !- Wind Exposure
+  ,                                       !- View Factor to Ground
+  ,                                       !- Number of Vertices
+  0, 0, 0,                                !- X,Y,Z Vertex 1 {m}
+  0, 10, 0,                               !- X,Y,Z Vertex 2 {m}
+  10, 10, 0,                              !- X,Y,Z Vertex 3 {m}
+  10, 0, 0;                               !- X,Y,Z Vertex 4 {m}
+
+OS:Surface,
+  {8ebd1b7f-0aeb-4db8-8302-2c6ce52d6bc8}, !- Handle
+  Surface 14,                             !- Name
+  Wall,                                   !- Surface Type
+  ,                                       !- Construction Name
+  {e278bc58-bfdf-4029-9c26-fa1f48f18a6f}, !- Space Name
+  Outdoors,                               !- Outside Boundary Condition
+  ,                                       !- Outside Boundary Condition Object
+  SunExposed,                             !- Sun Exposure
+  WindExposed,                            !- Wind Exposure
+  ,                                       !- View Factor to Ground
+  ,                                       !- Number of Vertices
+  10, 10, 3,                              !- X,Y,Z Vertex 1 {m}
+  10, 10, 0,                              !- X,Y,Z Vertex 2 {m}
+  0, 10, 0,                               !- X,Y,Z Vertex 3 {m}
+  0, 10, 3;                               !- X,Y,Z Vertex 4 {m}
+
+OS:Surface,
+  {d10cb3dc-2615-421a-8ad3-b85e07de6c1c}, !- Handle
+  Surface 15,                             !- Name
+  Wall,                                   !- Surface Type
+  ,                                       !- Construction Name
+  {e278bc58-bfdf-4029-9c26-fa1f48f18a6f}, !- Space Name
+  Surface,                                !- Outside Boundary Condition
+  {f389f851-f161-4ced-b277-47658375621d}, !- Outside Boundary Condition Object
+  NoSun,                                  !- Sun Exposure
+  NoWind,                                 !- Wind Exposure
+  ,                                       !- View Factor to Ground
+  ,                                       !- Number of Vertices
+  10, 0, 3,                               !- X,Y,Z Vertex 1 {m}
+  10, 0, 0,                               !- X,Y,Z Vertex 2 {m}
+  10, 10, 0,                              !- X,Y,Z Vertex 3 {m}
+  10, 10, 3;                              !- X,Y,Z Vertex 4 {m}
+
+OS:Surface,
+  {925f8983-81ef-4f1a-b6e1-b737e5f01f84}, !- Handle
+  Surface 16,                             !- Name
+  Wall,                                   !- Surface Type
+  ,                                       !- Construction Name
+  {e278bc58-bfdf-4029-9c26-fa1f48f18a6f}, !- Space Name
+  Outdoors,                               !- Outside Boundary Condition
+  ,                                       !- Outside Boundary Condition Object
+  SunExposed,                             !- Sun Exposure
+  WindExposed,                            !- Wind Exposure
+  ,                                       !- View Factor to Ground
+  ,                                       !- Number of Vertices
+  0, 10, 3,                               !- X,Y,Z Vertex 1 {m}
+  0, 10, 0,                               !- X,Y,Z Vertex 2 {m}
+  0, 0, 0,                                !- X,Y,Z Vertex 3 {m}
+  0, 0, 3;                                !- X,Y,Z Vertex 4 {m}
+
+OS:Surface,
+  {6596f879-72a8-4790-b521-c7eb872db76b}, !- Handle
+  Surface 17,                             !- Name
+  Wall,                                   !- Surface Type
+  ,                                       !- Construction Name
+  {e278bc58-bfdf-4029-9c26-fa1f48f18a6f}, !- Space Name
+  Surface,                                !- Outside Boundary Condition
+  {0a3035a8-4f91-4615-9ede-b6269a2901c4}, !- Outside Boundary Condition Object
+  NoSun,                                  !- Sun Exposure
+  NoWind,                                 !- Wind Exposure
+  ,                                       !- View Factor to Ground
+  ,                                       !- Number of Vertices
+  0, 0, 3,                                !- X,Y,Z Vertex 1 {m}
+  0, 0, 0,                                !- X,Y,Z Vertex 2 {m}
+  10, 0, 0,                               !- X,Y,Z Vertex 3 {m}
+  10, 0, 3;                               !- X,Y,Z Vertex 4 {m}
+
+OS:Surface,
+  {52d46cb2-991b-4d89-aba0-4b2e16f07b34}, !- Handle
+  Surface 18,                             !- Name
+  RoofCeiling,                            !- Surface Type
+  ,                                       !- Construction Name
+  {e278bc58-bfdf-4029-9c26-fa1f48f18a6f}, !- Space Name
+  Outdoors,                               !- Outside Boundary Condition
+  ,                                       !- Outside Boundary Condition Object
+  SunExposed,                             !- Sun Exposure
+  WindExposed,                            !- Wind Exposure
+  ,                                       !- View Factor to Ground
+  ,                                       !- Number of Vertices
+  10, 0, 3,                               !- X,Y,Z Vertex 1 {m}
+  10, 10, 3,                              !- X,Y,Z Vertex 2 {m}
+  0, 10, 3,                               !- X,Y,Z Vertex 3 {m}
+  0, 0, 3;                                !- X,Y,Z Vertex 4 {m}
+
+OS:Space,
+  {58b3c06c-278e-4b4e-9900-8dda4c98d45c}, !- Handle
+  Space 4,                                !- Name
+  ,                                       !- Space Type Name
+  ,                                       !- Default Construction Set Name
+  ,                                       !- Default Schedule Set Name
+  ,                                       !- Direction of Relative North {deg}
+  10,                                     !- X Origin {m}
+  10,                                     !- Y Origin {m}
+  ,                                       !- Z Origin {m}
+  {4fc6ec11-6215-4146-b4a7-c9fbbb3ccaf0}, !- Building Story Name
+  {a89ba35e-b1a0-496b-9210-5fa1050abf06}; !- Thermal Zone Name
+
+OS:Surface,
+  {337d01f9-cf50-4361-a861-980c7c4faa43}, !- Handle
+  Surface 19,                             !- Name
+  Floor,                                  !- Surface Type
+  ,                                       !- Construction Name
+  {58b3c06c-278e-4b4e-9900-8dda4c98d45c}, !- Space Name
+  Ground,                                 !- Outside Boundary Condition
+  ,                                       !- Outside Boundary Condition Object
+  NoSun,                                  !- Sun Exposure
+  NoWind,                                 !- Wind Exposure
+  ,                                       !- View Factor to Ground
+  ,                                       !- Number of Vertices
+  0, 0, 0,                                !- X,Y,Z Vertex 1 {m}
+  0, 10, 0,                               !- X,Y,Z Vertex 2 {m}
+  10, 10, 0,                              !- X,Y,Z Vertex 3 {m}
+  10, 0, 0;                               !- X,Y,Z Vertex 4 {m}
+
+OS:Surface,
+  {2d6a4ad3-e093-4c25-ba9a-bd47b7e59700}, !- Handle
+  Surface 20,                             !- Name
+  Wall,                                   !- Surface Type
+  ,                                       !- Construction Name
+  {58b3c06c-278e-4b4e-9900-8dda4c98d45c}, !- Space Name
+  Outdoors,                               !- Outside Boundary Condition
+  ,                                       !- Outside Boundary Condition Object
+  SunExposed,                             !- Sun Exposure
+  WindExposed,                            !- Wind Exposure
+  ,                                       !- View Factor to Ground
+  ,                                       !- Number of Vertices
+  10, 10, 3,                              !- X,Y,Z Vertex 1 {m}
+  10, 10, 0,                              !- X,Y,Z Vertex 2 {m}
+  0, 10, 0,                               !- X,Y,Z Vertex 3 {m}
+  0, 10, 3;                               !- X,Y,Z Vertex 4 {m}
+
+OS:Surface,
+  {091666eb-7dc5-4a38-8913-a458210f9bb4}, !- Handle
+  Surface 21,                             !- Name
+  Wall,                                   !- Surface Type
+  ,                                       !- Construction Name
+  {58b3c06c-278e-4b4e-9900-8dda4c98d45c}, !- Space Name
+  Outdoors,                               !- Outside Boundary Condition
+  ,                                       !- Outside Boundary Condition Object
+  SunExposed,                             !- Sun Exposure
+  WindExposed,                            !- Wind Exposure
+  ,                                       !- View Factor to Ground
+  ,                                       !- Number of Vertices
+  10, 0, 3,                               !- X,Y,Z Vertex 1 {m}
+  10, 0, 0,                               !- X,Y,Z Vertex 2 {m}
+  10, 10, 0,                              !- X,Y,Z Vertex 3 {m}
+  10, 10, 3;                              !- X,Y,Z Vertex 4 {m}
+
+OS:Surface,
+  {f389f851-f161-4ced-b277-47658375621d}, !- Handle
+  Surface 22,                             !- Name
+  Wall,                                   !- Surface Type
+  ,                                       !- Construction Name
+  {58b3c06c-278e-4b4e-9900-8dda4c98d45c}, !- Space Name
+  Surface,                                !- Outside Boundary Condition
+  {d10cb3dc-2615-421a-8ad3-b85e07de6c1c}, !- Outside Boundary Condition Object
+  NoSun,                                  !- Sun Exposure
+  NoWind,                                 !- Wind Exposure
+  ,                                       !- View Factor to Ground
+  ,                                       !- Number of Vertices
+  0, 10, 3,                               !- X,Y,Z Vertex 1 {m}
+  0, 10, 0,                               !- X,Y,Z Vertex 2 {m}
+  0, 0, 0,                                !- X,Y,Z Vertex 3 {m}
+  0, 0, 3;                                !- X,Y,Z Vertex 4 {m}
+
+OS:Surface,
+  {634a5854-b1c9-43ee-b047-cabba454ed8e}, !- Handle
+  Surface 23,                             !- Name
+  Wall,                                   !- Surface Type
+  ,                                       !- Construction Name
+  {58b3c06c-278e-4b4e-9900-8dda4c98d45c}, !- Space Name
+  Surface,                                !- Outside Boundary Condition
+  {64cbf499-6550-4086-94fd-269619dde4d4}, !- Outside Boundary Condition Object
+  NoSun,                                  !- Sun Exposure
+  NoWind,                                 !- Wind Exposure
+  ,                                       !- View Factor to Ground
+  ,                                       !- Number of Vertices
+  0, 0, 3,                                !- X,Y,Z Vertex 1 {m}
+  0, 0, 0,                                !- X,Y,Z Vertex 2 {m}
+  10, 0, 0,                               !- X,Y,Z Vertex 3 {m}
+  10, 0, 3;                               !- X,Y,Z Vertex 4 {m}
+
+OS:Surface,
+  {6a004a98-42c4-4c5e-9c45-15ade306ee7f}, !- Handle
+  Surface 24,                             !- Name
+  RoofCeiling,                            !- Surface Type
+  ,                                       !- Construction Name
+  {58b3c06c-278e-4b4e-9900-8dda4c98d45c}, !- Space Name
+  Outdoors,                               !- Outside Boundary Condition
+  ,                                       !- Outside Boundary Condition Object
+  SunExposed,                             !- Sun Exposure
+  WindExposed,                            !- Wind Exposure
+  ,                                       !- View Factor to Ground
+  ,                                       !- Number of Vertices
+  10, 0, 3,                               !- X,Y,Z Vertex 1 {m}
+  10, 10, 3,                              !- X,Y,Z Vertex 2 {m}
+  0, 10, 3,                               !- X,Y,Z Vertex 3 {m}
+  0, 0, 3;                                !- X,Y,Z Vertex 4 {m}
+
+OS:SubSurface,
+  {64681506-6303-47e8-a9d9-9b14cea8608b}, !- Handle
+  Sub Surface 1,                          !- Name
+  Door,                                   !- Sub Surface Type
+  ,                                       !- Construction Name
+  {e13d947b-1295-4f2f-bfc7-35ba63eb1e33}, !- Surface Name
+  ,                                       !- Outside Boundary Condition Object
+  ,                                       !- View Factor to Ground
+  ,                                       !- Frame and Divider Name
+  ,                                       !- Multiplier
+  ,                                       !- Number of Vertices
+  2, 0, 2,                                !- X,Y,Z Vertex 1 {m}
+  2, 0, 0,                                !- X,Y,Z Vertex 2 {m}
+  4, 0, 0,                                !- X,Y,Z Vertex 3 {m}
+  4, 0, 2;                                !- X,Y,Z Vertex 4 {m}
+
+OS:SubSurface,
+  {852f9a22-556c-43bc-80cd-8ffe9fa5eaa3}, !- Handle
+  Sub Surface 2,                          !- Name
+  FixedWindow,                            !- Sub Surface Type
+  ,                                       !- Construction Name
+  {48314984-b35f-4070-94a4-4273fed3be55}, !- Surface Name
+  ,                                       !- Outside Boundary Condition Object
+  ,                                       !- View Factor to Ground
+  ,                                       !- Frame and Divider Name
+  ,                                       !- Multiplier
+  ,                                       !- Number of Vertices
+  10, 2, 2,                               !- X,Y,Z Vertex 1 {m}
+  10, 2, 1,                               !- X,Y,Z Vertex 2 {m}
+  10, 8, 1,                               !- X,Y,Z Vertex 3 {m}
+  10, 8, 2;                               !- X,Y,Z Vertex 4 {m}
+
+OS:ShadingSurfaceGroup,
+  {a0d5d869-a4e4-498b-b37f-ce1da400c666}, !- Handle
+  Sub Surface 2 Shading Surfaces,         !- Name
+  Space,                                  !- Shading Surface Type
+  {1c8cee02-5519-4c67-8f8e-11be54cdd68f}, !- Space Name
+  ,                                       !- Direction of Relative North {deg}
+  ,                                       !- X Origin {m}
+  ,                                       !- Y Origin {m}
+  ,                                       !- Z Origin {m}
+  {852f9a22-556c-43bc-80cd-8ffe9fa5eaa3}; !- Shaded Object Name
+
+OS:ShadingSurface,
+  {d46642eb-0faa-4018-89e1-fa98e4720fa8}, !- Handle
+  Shading Surface 1,                      !- Name
+  ,                                       !- Construction Name
+  {a0d5d869-a4e4-498b-b37f-ce1da400c666}, !- Shading Surface Group Name
+  ,                                       !- Transmittance Schedule Name
+  ,                                       !- Number of Vertices
+  10, 8.1, 2.1,                           !- X,Y,Z Vertex 1 {m}
+  10, 1.9, 2.1,                           !- X,Y,Z Vertex 2 {m}
+  10.55, 1.9, 2.1,                        !- X,Y,Z Vertex 3 {m}
+  10.55, 8.1, 2.1;                        !- X,Y,Z Vertex 4 {m}
+
+OS:Daylighting:Control,
+  {5a95532d-2df3-42e3-8946-75ac5d206df9}, !- Handle
+  Daylighting Control 1,                  !- Name
+  {1c8cee02-5519-4c67-8f8e-11be54cdd68f}, !- Space Name
+  5,                                      !- Position X-Coordinate {m}
+  5,                                      !- Position Y-Coordinate {m}
+  1.1;                                    !- Position Z-Coordinate {m}
+
+OS:IlluminanceMap,
+  {c214c17e-34b3-4cc1-868c-775fbf965702}, !- Handle
+  Illuminance Map 1,                      !- Name
+  {1c8cee02-5519-4c67-8f8e-11be54cdd68f}, !- Space Name
+  1,                                      !- Origin X-Coordinate {m}
+  1,                                      !- Origin Y-Coordinate {m}
+  1.1,                                    !- Origin Z-Coordinate {m}
+  ,                                       !- Psi Rotation Around X-Axis {deg}
+  ,                                       !- Theta Rotation Around Y-Axis {deg}
+  ,                                       !- Phi Rotation Around Z-Axis {deg}
+  8,                                      !- X Length {m}
+  ,                                       !- Number of X Grid Points
+  8;                                      !- Y Length {m}
+
+OS:Glare:Sensor,
+  {59b04ac2-9673-486d-af59-08f9dd9484fb}, !- Handle
+  Glare Sensor 1,                         !- Name
+  {1c8cee02-5519-4c67-8f8e-11be54cdd68f}, !- Space Name
+  5,                                      !- Position X-Coordinate {m}
+  5,                                      !- Position Y-Coordinate {m}
+  1.1;                                    !- Position Z-Coordinate {m}
+
+OS:InteriorPartitionSurfaceGroup,
+  {2b32aa30-22a2-4e06-9511-212f3b00eeb4}, !- Handle
+  Interior Partition Surface Group 1,     !- Name
+  {1c8cee02-5519-4c67-8f8e-11be54cdd68f}; !- Space Name
+
+OS:InteriorPartitionSurface,
+  {c3ef1d5f-211b-4535-966f-85befac775ab}, !- Handle
+  Interior Partition Surface 1,           !- Name
+  ,                                       !- Construction Name
+  {2b32aa30-22a2-4e06-9511-212f3b00eeb4}, !- Interior Partition Surface Group Name
+  ,                                       !- Convert to Internal Mass
+  ,                                       !- Surface Area {m2}
+  ,                                       !- Number of Vertices
+  5, 8, 1,                                !- X,Y,Z Vertex 1 {m}
+  5, 6, 1,                                !- X,Y,Z Vertex 2 {m}
+  8, 6, 1,                                !- X,Y,Z Vertex 3 {m}
+  8, 8, 1;                                !- X,Y,Z Vertex 4 {m}
+
+OS:ElectricEquipment:Definition,
+  {387698a7-33e1-49a8-9d75-36b8029825bd}, !- Handle
+  Printer Definition,                     !- Name
+  EquipmentLevel,                         !- Design Level Calculation Method
+  200,                                    !- Design Level {W}
+  ,                                       !- Watts per Space Floor Area {W/m2}
+  ;                                       !- Watts per Person {W/person}
+
+OS:ElectricEquipment,
+  {703e4ddc-16b9-46ed-a2f9-aac562572d1b}, !- Handle
+  Printer,                                !- Name
+  {387698a7-33e1-49a8-9d75-36b8029825bd}, !- Electric Equipment Definition Name
+  {58b3c06c-278e-4b4e-9900-8dda4c98d45c}, !- Space or SpaceType Name
+  ,                                       !- Schedule Name
+  ,                                       !- Multiplier
+  General;                                !- End-Use Subcategory
+
+OS:ShadingSurfaceGroup,
+  {c4a5d83a-6f15-4678-8e52-f0ead08eb53f}, !- Handle
+  Shading Surface Group 1,                !- Name
+  Building;                               !- Shading Surface Type
+
+OS:ShadingSurface,
+  {0cb98327-80c2-4bcc-b1b9-baddb85d9e75}, !- Handle
+  Shading Surface 2,                      !- Name
+  ,                                       !- Construction Name
+  {c4a5d83a-6f15-4678-8e52-f0ead08eb53f}, !- Shading Surface Group Name
+  ,                                       !- Transmittance Schedule Name
+  ,                                       !- Number of Vertices
+  2, 0, 2,                                !- X,Y,Z Vertex 1 {m}
+  2, -1, 2,                               !- X,Y,Z Vertex 2 {m}
+  4, -1, 2,                               !- X,Y,Z Vertex 3 {m}
+  4, 0, 2;                                !- X,Y,Z Vertex 4 {m}
+
+OS:ShadingSurfaceGroup,
+  {3c2fdab3-dd9d-4f1c-ac75-b30953189d30}, !- Handle
+  Shading Surface Group 2,                !- Name
+  Site;                                   !- Shading Surface Type
+
+OS:ShadingSurface,
+  {56a24389-777c-468f-8480-d1398a39b3e0}, !- Handle
+  Shading Surface 3,                      !- Name
+  ,                                       !- Construction Name
+  {3c2fdab3-dd9d-4f1c-ac75-b30953189d30}, !- Shading Surface Group Name
+  ,                                       !- Transmittance Schedule Name
+  ,                                       !- Number of Vertices
+  -30, 0, 20,                             !- X,Y,Z Vertex 1 {m}
+  -30, 0, 0,                              !- X,Y,Z Vertex 2 {m}
+  -30, 20, 0,                             !- X,Y,Z Vertex 3 {m}
+  -30, 20, 20;                            !- X,Y,Z Vertex 4 {m}
+
+OS:Schedule:Compact,
+  {0dbfd573-6b49-4406-8884-2a477c982cdc}, !- Handle
+  ALWAYS_ON,                              !- Name
+  {72f30d18-bde7-4149-b1be-13d3bcd221f9}, !- Schedule Type Limits Name
+  Through: 12/31,                         !- Field 1
+  For: AllDays,                           !- Field 2
+  Until: 24:00,                           !- Field 3
+  1;                                      !- Field 4
+
+OS:Fan:ConstantVolume,
+  {9f60d4f0-a4d4-4e2d-8dd7-def709c76d0c}, !- Handle
+  Standard Fan,                           !- Name
+  {0dbfd573-6b49-4406-8884-2a477c982cdc}, !- Availability Schedule Name
+  ,                                       !- Fan Total Efficiency
+  ,                                       !- Pressure Rise {Pa}
+  AutoSize,                               !- Maximum Flow Rate {m3/s}
+  ,                                       !- Motor Efficiency
+  ,                                       !- Motor In Airstream Fraction
+  {225793d3-3492-4c59-9652-75103272ae37}, !- Air Inlet Node Name
+  {d6176604-ee60-43eb-aded-d8407f62b750}, !- Air Outlet Node Name
+  ;                                       !- End-Use Subcategory
+
+OS:ScheduleTypeLimits,
+  {72f30d18-bde7-4149-b1be-13d3bcd221f9}, !- Handle
+  OnOff,                                  !- Name
+  0,                                      !- Lower Limit Value
+  1,                                      !- Upper Limit Value
+  Discrete,                               !- Numeric Type
+  Availability;                           !- Unit Type
+
+OS:Coil:Heating:Gas,
+  {f5f15b01-beba-40b1-b44c-6955d7dae9a6}, !- Handle
+  Coil Heating Gas 1,                     !- Name
+  {0dbfd573-6b49-4406-8884-2a477c982cdc}, !- Availability Schedule Name
+  0.8,                                    !- Gas Burner Efficiency
+  AutoSize,                               !- Nominal Capacity {W}
+  {e0182018-be3c-405c-a0a5-39b3712051eb}, !- Air Inlet Node Name
+  {b933f56d-4091-4667-8b84-1b384685c5d4}, !- Air Outlet Node Name
+  ,                                       !- Temperature Setpoint Node Name
+  0,                                      !- Parasitic Electric Load {W}
+  ,                                       !- Part Load Fraction Correlation Curve Name
+  0;                                      !- Parasitic Gas Load {W}
+
+OS:Curve:Biquadratic,
+  {0e9aedf5-0bed-461d-8941-48e58eb56657}, !- Handle
+  Curve Biquadratic 1,                    !- Name
+  0.42415,                                !- Coefficient1 Constant
+  0.04426,                                !- Coefficient2 x
+  -0.00042,                               !- Coefficient3 x**2
+  0.00333,                                !- Coefficient4 y
+  -8e-05,                                 !- Coefficient5 y**2
+  -0.00021,                               !- Coefficient6 x*y
+  17,                                     !- Minimum Value of x
+  22,                                     !- Maximum Value of x
+  13,                                     !- Minimum Value of y
+  46,                                     !- Maximum Value of y
+  -1000,                                  !- Minimum Curve Output
+  1000;                                   !- Maximum Curve Output
+
+OS:Curve:Quadratic,
+  {2a495242-14f4-4fb3-b340-e8caa76daa5b}, !- Handle
+  Curve Quadratic 1,                      !- Name
+  0.77136,                                !- Coefficient1 Constant
+  0.34053,                                !- Coefficient2 x
+  -0.11088,                               !- Coefficient3 x**2
+  0.75918,                                !- Minimum Value of x
+  1.13877,                                !- Maximum Value of x
+  -1000,                                  !- Minimum Curve Output
+  1000;                                   !- Maximum Curve Output
+
+OS:Curve:Biquadratic,
+  {3ec7297f-f52f-4193-91aa-be0f869df5df}, !- Handle
+  Curve Biquadratic 2,                    !- Name
+  1.23649,                                !- Coefficient1 Constant
+  -0.02431,                               !- Coefficient2 x
+  0.00057,                                !- Coefficient3 x**2
+  -0.01434,                               !- Coefficient4 y
+  0.00063,                                !- Coefficient5 y**2
+  -0.00038,                               !- Coefficient6 x*y
+  17,                                     !- Minimum Value of x
+  22,                                     !- Maximum Value of x
+  0,                                      !- Minimum Value of y
+  46,                                     !- Maximum Value of y
+  -1000,                                  !- Minimum Curve Output
+  1000;                                   !- Maximum Curve Output
+
+OS:Curve:Quadratic,
+  {2c4dd6e5-da87-4625-98d0-33b45acf8696}, !- Handle
+  Curve Quadratic 2,                      !- Name
+  1.2055,                                 !- Coefficient1 Constant
+  -0.32953,                               !- Coefficient2 x
+  0.12308,                                !- Coefficient3 x**2
+  0.75918,                                !- Minimum Value of x
+  1.13877,                                !- Maximum Value of x
+  -1000,                                  !- Minimum Curve Output
+  1000;                                   !- Maximum Curve Output
+
+OS:Curve:Quadratic,
+  {40222797-9eb8-4c6a-910b-c45b283daf8e}, !- Handle
+  Curve Quadratic 3,                      !- Name
+  0.771,                                  !- Coefficient1 Constant
+  0.229,                                  !- Coefficient2 x
+  0,                                      !- Coefficient3 x**2
+  0,                                      !- Minimum Value of x
+  1,                                      !- Maximum Value of x
+  0.71,                                   !- Minimum Curve Output
+  1;                                      !- Maximum Curve Output
+
+OS:Coil:Cooling:DX:SingleSpeed,
+  {9a8dee0f-c8e9-487b-baa4-f6e54a704526}, !- Handle
+  Coil Cooling DX Single Speed 1,         !- Name
+  {0dbfd573-6b49-4406-8884-2a477c982cdc}, !- Availability Schedule Name
+  autosize,                               !- Rated Total Cooling Capacity {W}
+  autosize,                               !- Rated Sensible Heat Ratio
+  3,                                      !- Rated COP {W/W}
+  autosize,                               !- Rated Air Flow Rate {m3/s}
+  773.3,                                  !- Rated Evaporator Fan Power Per Volume Flow Rate {W/(m3/s)}
+  {6595b5c8-66a5-47f5-8f70-bfa2ba1fb1b1}, !- Air Inlet Node Name
+  {992984e7-eba1-4dfc-9314-319957856b1e}, !- Air Outlet Node Name
+  {0e9aedf5-0bed-461d-8941-48e58eb56657}, !- Total Cooling Capacity Function of Temperature Curve Name
+  {2a495242-14f4-4fb3-b340-e8caa76daa5b}, !- Total Cooling Capacity Function of Flow Fraction Curve Name
+  {3ec7297f-f52f-4193-91aa-be0f869df5df}, !- Energy Input Ratio Function of Temperature Curve Name
+  {2c4dd6e5-da87-4625-98d0-33b45acf8696}, !- Energy Input Ratio Function of Flow Fraction Curve Name
+  {40222797-9eb8-4c6a-910b-c45b283daf8e}, !- Part Load Fraction Correlation Curve Name
+  -25,                                    !- Minimum Outdoor Dry-Bulb Temperature for Compressor Operation {C}
+  ,                                       !- Nominal Time for Condensate Removal to Begin {s}
+  ,                                       !- Ratio of Initial Moisture Evaporation Rate and Steady State Latent Capacity {dimensionless}
+  ,                                       !- Maximum Cycling Rate {cycles/hr}
+  ,                                       !- Latent Capacity Time Constant {s}
+  ,                                       !- Condenser Air Inlet Node Name
+  AirCooled,                              !- Condenser Type
+  0,                                      !- Evaporative Condenser Effectiveness {dimensionless}
+  Autosize,                               !- Evaporative Condenser Air Flow Rate {m3/s}
+  Autosize,                               !- Evaporative Condenser Pump Rated Power Consumption {W}
+  0,                                      !- Crankcase Heater Capacity {W}
+  0,                                      !- Maximum Outdoor Dry-Bulb Temperature for Crankcase Heater Operation {C}
+  ,                                       !- Supply Water Storage Tank Name
+  ,                                       !- Condensate Collection Water Storage Tank Name
+  0,                                      !- Basin Heater Capacity {W/K}
+  10,                                     !- Basin Heater Setpoint Temperature {C}
+  ;                                       !- Basin Heater Operating Schedule Name
+
+OS:EvaporativeCooler:Direct:ResearchSpecial,
+  {e7f67a2c-5b6d-48fb-bd4d-3932e54aa7e1}, !- Handle
+  Evaporative Cooler Direct Research Special 1, !- Name
+  {0dbfd573-6b49-4406-8884-2a477c982cdc}, !- Availability Schedule Name
+  1,                                      !- Cooler Design Effectiveness
+  0,                                      !- Recirculating Water Pump Power Consumption {W}
+  ,                                       !- Primary Air Design Flow Rate {m3/s}
+  {88afa5dc-ee79-4915-8497-0a611e83353b}, !- Air Inlet Node Name
+  {cda88b8d-3c30-4558-bd09-199d83eb2681}, !- Air Outlet Node Name
+  {8650ff70-4110-4486-b9cf-55045c69b8b2}, !- Sensor Node Name
+  0,                                      !- Drift Loss Fraction
+  0,                                      !- Blowdown Concentration Ratio
+  ,                                       !- Effectiveness Flow Ratio Modifier Curve Name
+  0.1,                                    !- Water Pump Power Sizing Factor {W/(m3/s)}
+  ,                                       !- Water Pump Power Modifier Curve Name
+  16,                                     !- Evaporative Operation Minimum Drybulb Temperature
+  24,                                     !- Evaporative Operation Maximum Limit Wetbulb Temperature
+  28;                                     !- Evaporative Operation Maximum Limit Drybulb Temperature
+
+OS:AirTerminal:SingleDuct:ConstantVolume:NoReheat,
+  {a88a9fb9-37e8-4ddc-9503-d9833d587a90}, !- Handle
+  Air Terminal Single Duct Constant Volume No Reheat 1, !- Name
+  {0dbfd573-6b49-4406-8884-2a477c982cdc}, !- Availability Schedule Name
+  {78d7e697-5124-48fb-80c7-0a938f5c03e3}, !- Air Inlet Node Name
+  {84fe7839-9ca5-4129-a4d8-a66eda945bfc}, !- Air Outlet Node Name
+  AutoSize;                               !- Maximum Air Flow Rate {m3/s}
+
+OS:Controller:OutdoorAir,
+  {2f453425-0e8c-44b0-921e-7c60218bae8a}, !- Handle
+  Controller Outdoor Air 1,               !- Name
+  ,                                       !- Relief Air Outlet Node Name
+  ,                                       !- Return Air Node Name
+  ,                                       !- Mixed Air Node Name
+  ,                                       !- Actuator Node Name
+  0,                                      !- Minimum Outdoor Air Flow Rate {m3/s}
+  Autosize,                               !- Maximum Outdoor Air Flow Rate {m3/s}
+  NoEconomizer,                           !- Economizer Control Type
+  ModulateFlow,                           !- Economizer Control Action Type
+  28,                                     !- Economizer Maximum Limit Dry-Bulb Temperature {C}
+  64000,                                  !- Economizer Maximum Limit Enthalpy {J/kg}
+  ,                                       !- Economizer Maximum Limit Dewpoint Temperature {C}
+  ,                                       !- Electronic Enthalpy Limit Curve Name
+  -100,                                   !- Economizer Minimum Limit Dry-Bulb Temperature {C}
+  NoLockout,                              !- Lockout Type
+  FixedMinimum,                           !- Minimum Limit Type
+  ,                                       !- Minimum Outdoor Air Schedule Name
+  ,                                       !- Minimum Fraction of Outdoor Air Schedule Name
+  ,                                       !- Maximum Fraction of Outdoor Air Schedule Name
+  {2052fcb6-8172-419a-9600-f5f42965207a}, !- Controller Mechanical Ventilation
+  ,                                       !- Time of Day Economizer Control Schedule Name
+  No,                                     !- High Humidity Control
+  ,                                       !- Humidistat Control Zone Name
+  ,                                       !- High Humidity Outdoor Air Flow Ratio
+  ,                                       !- Control High Indoor Humidity Based on Outdoor Humidity Ratio
+  BypassWhenWithinEconomizerLimits;       !- Heat Recovery Bypass Control Type
+
+OS:Controller:MechanicalVentilation,
+  {2052fcb6-8172-419a-9600-f5f42965207a}, !- Handle
+  Controller Mechanical Ventilation 1,    !- Name
+  {47c6ecd7-4b7f-4f52-bf53-ccbd89ea4c41}, !- Availability Schedule
+  ,                                       !- Demand Controlled Ventilation
+  ;                                       !- System Outdoor Air Method
+
+OS:Schedule:Constant,
+  {47c6ecd7-4b7f-4f52-bf53-ccbd89ea4c41}, !- Handle
+  Always On Discrete,                     !- Name
+  {9aa0f9f1-8b39-43ef-8b00-983fae540527}, !- Schedule Type Limits Name
+  1;                                      !- Value
+
+OS:ScheduleTypeLimits,
+  {9aa0f9f1-8b39-43ef-8b00-983fae540527}, !- Handle
+  OnOff 1,                                !- Name
+  0,                                      !- Lower Limit Value
+  1,                                      !- Upper Limit Value
+  Discrete,                               !- Numeric Type
+  Availability;                           !- Unit Type
+
+OS:AirLoopHVAC:OutdoorAirSystem,
+  {74eff7fe-484c-4cdd-b51c-eaf89776a182}, !- Handle
+  Air Loop HVAC Outdoor Air System 1,     !- Name
+  {2f453425-0e8c-44b0-921e-7c60218bae8a}, !- Controller Name
+  ,                                       !- Outdoor Air Equipment List Name
+  ,                                       !- Availability Manager List Name
+  {944e180a-ddb1-4c98-80e9-64bb4bf42413}, !- Mixed Air Node Name
+  {1f32e296-3697-43ea-a35c-975adf4ce3f2}, !- Outdoor Air Stream Node Name
+  {3c17d94e-fcd2-4b36-b3b6-a1b19018eee7}, !- Relief Air Stream Node Name
+  {b0216618-b0f0-409f-9ba6-e61daa4d8076}; !- Return Air Stream Node Name
+
+OS:Node,
+  {b7879d15-2825-4e66-8d79-77df279d72d8}, !- Handle
+  Node 2,                                 !- Name
+  ,                                       !- Inlet Port
+  {88afa5dc-ee79-4915-8497-0a611e83353b}; !- Outlet Port
+
+OS:Node,
+  {1a8092cf-fefd-4759-9e4a-96cc8d7ba073}, !- Handle
+  Node 3,                                 !- Name
+  {3c17d94e-fcd2-4b36-b3b6-a1b19018eee7}, !- Inlet Port
+  ;                                       !- Outlet Port
+
+OS:Connection,
+  {3c17d94e-fcd2-4b36-b3b6-a1b19018eee7}, !- Handle
+  {74eff7fe-484c-4cdd-b51c-eaf89776a182}, !- Source Object
+  7,                                      !- Outlet Port
+  {1a8092cf-fefd-4759-9e4a-96cc8d7ba073}, !- Target Object
+  2;                                      !- Inlet Port
+
+OS:AirLoopHVAC,
+  {695feaf9-39c3-4949-84e8-c0aba87b6a7d}, !- Handle
+  Air Loop HVAC 1,                        !- Name
+  ,                                       !- Controller List Name
+  {47c6ecd7-4b7f-4f52-bf53-ccbd89ea4c41}, !- Availability Schedule
+  {542038c4-a887-4bf5-b4d2-d4f98538ce45}, !- Availability Manager List Name
+  autosize,                               !- Design Supply Air Flow Rate {m3/s}
+  1,                                      !- Design Return Air Flow Fraction of Supply Air Flow
+  ,                                       !- Branch List Name
+  ,                                       !- Connector List Name
+  {3a372aa7-d44a-4e7e-840e-10657fad6e49}, !- Supply Side Inlet Node Name
+  {4f9448a4-aae6-4cbd-93cf-32d556f3ccbe}, !- Demand Side Outlet Node Name
+  {7afc5df7-e1f4-4f67-a3ec-fdaef995f1d2}, !- Demand Side Inlet Node A
+  {b89f0ec9-92ce-475d-8005-1760297c6aa6}, !- Supply Side Outlet Node A
+  ,                                       !- Demand Side Inlet Node B
+  ,                                       !- Supply Side Outlet Node B
+  ,                                       !- Return Air Bypass Flow Temperature Setpoint Schedule Name
+  {41146a0d-80bf-4aaa-81f0-5ae40bd46884}, !- Demand Mixer Name
+  {db790a58-149b-4323-8954-1bddfd57c610}, !- Demand Splitter A Name
+  ,                                       !- Demand Splitter B Name
+  ;                                       !- Supply Splitter Name
+
+OS:Node,
+  {bbfaf2fa-d5f5-4bbd-b1df-3d086c5a3af2}, !- Handle
+  Node 4,                                 !- Name
+  {3a372aa7-d44a-4e7e-840e-10657fad6e49}, !- Inlet Port
+  {b0216618-b0f0-409f-9ba6-e61daa4d8076}; !- Outlet Port
+
+OS:Node,
+  {d01de46a-2113-43e3-a3d0-7fb17725205f}, !- Handle
+  Node 5,                                 !- Name
+  {d6176604-ee60-43eb-aded-d8407f62b750}, !- Inlet Port
+  {b89f0ec9-92ce-475d-8005-1760297c6aa6}; !- Outlet Port
+
+OS:Connection,
+  {3a372aa7-d44a-4e7e-840e-10657fad6e49}, !- Handle
+  {695feaf9-39c3-4949-84e8-c0aba87b6a7d}, !- Source Object
+  9,                                      !- Outlet Port
+  {bbfaf2fa-d5f5-4bbd-b1df-3d086c5a3af2}, !- Target Object
+  2;                                      !- Inlet Port
+
+OS:Connection,
+  {b89f0ec9-92ce-475d-8005-1760297c6aa6}, !- Handle
+  {d01de46a-2113-43e3-a3d0-7fb17725205f}, !- Source Object
+  3,                                      !- Outlet Port
+  {695feaf9-39c3-4949-84e8-c0aba87b6a7d}, !- Target Object
+  12;                                     !- Inlet Port
+
+OS:Node,
+  {13c11cda-dd64-4143-808e-04f12f405f9d}, !- Handle
+  Node 6,                                 !- Name
+  {7afc5df7-e1f4-4f67-a3ec-fdaef995f1d2}, !- Inlet Port
+  {c1929c81-cb62-41b3-b8fc-467c3509bd86}; !- Outlet Port
+
+OS:Node,
+  {cf5f4b59-4378-44c0-adb4-0deef0b4dd6f}, !- Handle
+  Node 7,                                 !- Name
+  {7fadd8a7-0fb1-4391-adee-46cd55946099}, !- Inlet Port
+  {4f9448a4-aae6-4cbd-93cf-32d556f3ccbe}; !- Outlet Port
+
+OS:Node,
+  {9d289cd0-7b80-4584-8e64-dc03c4ef0a59}, !- Handle
+  Node 8,                                 !- Name
+  {84fe7839-9ca5-4129-a4d8-a66eda945bfc}, !- Inlet Port
+  {dd931870-f1f9-4740-9271-21fcb21bb217}; !- Outlet Port
+
+OS:Connection,
+  {7afc5df7-e1f4-4f67-a3ec-fdaef995f1d2}, !- Handle
+  {695feaf9-39c3-4949-84e8-c0aba87b6a7d}, !- Source Object
+  11,                                     !- Outlet Port
+  {13c11cda-dd64-4143-808e-04f12f405f9d}, !- Target Object
+  2;                                      !- Inlet Port
+
+OS:Connection,
+  {4f9448a4-aae6-4cbd-93cf-32d556f3ccbe}, !- Handle
+  {cf5f4b59-4378-44c0-adb4-0deef0b4dd6f}, !- Source Object
+  3,                                      !- Outlet Port
+  {695feaf9-39c3-4949-84e8-c0aba87b6a7d}, !- Target Object
+  10;                                     !- Inlet Port
+
+OS:AirLoopHVAC:ZoneSplitter,
+  {db790a58-149b-4323-8954-1bddfd57c610}, !- Handle
+  Air Loop HVAC Zone Splitter 1,          !- Name
+  {c1929c81-cb62-41b3-b8fc-467c3509bd86}, !- Inlet Node Name
+  {9de883d8-9350-40ff-be07-dbcdb1eb5aa7}; !- Outlet Node Name 1
+
+OS:AirLoopHVAC:ZoneMixer,
+  {41146a0d-80bf-4aaa-81f0-5ae40bd46884}, !- Handle
+  Air Loop HVAC Zone Mixer 1,             !- Name
+  {7fadd8a7-0fb1-4391-adee-46cd55946099}, !- Outlet Node Name
+  {4d1ffacb-85fe-4e83-8563-d9b285b118e2}; !- Inlet Node Name 1
+
+OS:Connection,
+  {c1929c81-cb62-41b3-b8fc-467c3509bd86}, !- Handle
+  {13c11cda-dd64-4143-808e-04f12f405f9d}, !- Source Object
+  3,                                      !- Outlet Port
+  {db790a58-149b-4323-8954-1bddfd57c610}, !- Target Object
+  2;                                      !- Inlet Port
+
+OS:Connection,
+  {7fadd8a7-0fb1-4391-adee-46cd55946099}, !- Handle
+  {41146a0d-80bf-4aaa-81f0-5ae40bd46884}, !- Source Object
+  2,                                      !- Outlet Port
+  {cf5f4b59-4378-44c0-adb4-0deef0b4dd6f}, !- Target Object
+  2;                                      !- Inlet Port
+
+OS:Sizing:System,
+  {73ddd16c-aa36-416d-bdf9-2eab4ba907af}, !- Handle
+  {695feaf9-39c3-4949-84e8-c0aba87b6a7d}, !- AirLoop Name
+  ,                                       !- Type of Load to Size On
+  ,                                       !- Design Outdoor Air Flow Rate {m3/s}
+  0.3,                                    !- Central Heating Maximum System Air Flow Ratio
+  7,                                      !- Preheat Design Temperature {C}
+  0.008,                                  !- Preheat Design Humidity Ratio {kg-H2O/kg-Air}
+  12.8,                                   !- Precool Design Temperature {C}
+  0.008,                                  !- Precool Design Humidity Ratio {kg-H2O/kg-Air}
+  12.8,                                   !- Central Cooling Design Supply Air Temperature {C}
+  16.7,                                   !- Central Heating Design Supply Air Temperature {C}
+  ,                                       !- Sizing Option
+  Yes,                                    !- 100% Outdoor Air in Cooling
+  Yes,                                    !- 100% Outdoor Air in Heating
+  0.0085,                                 !- Central Cooling Design Supply Air Humidity Ratio {kg-H2O/kg-Air}
+  ,                                       !- Central Heating Design Supply Air Humidity Ratio {kg-H2O/kg-Air}
+  ,                                       !- Cooling Design Air Flow Method
+  ,                                       !- Cooling Design Air Flow Rate {m3/s}
+  ,                                       !- Heating Design Air Flow Method
+  ,                                       !- Heating Design Air Flow Rate {m3/s}
+  ,                                       !- System Outdoor Air Method
+  1,                                      !- Zone Maximum Outdoor Air Fraction {dimensionless}
+  0.0099676501,                           !- Cooling Supply Air Flow Rate Per Floor Area {m3/s-m2}
+  1,                                      !- Cooling Fraction of Autosized Cooling Supply Air Flow Rate
+  3.9475456e-05,                          !- Cooling Supply Air Flow Rate Per Unit Cooling Capacity {m3/s-W}
+  0.0099676501,                           !- Heating Supply Air Flow Rate Per Floor Area {m3/s-m2}
+  1,                                      !- Heating Fraction of Autosized Heating Supply Air Flow Rate
+  1,                                      !- Heating Fraction of Autosized Cooling Supply Air Flow Rate
+  3.1588213e-05,                          !- Heating Supply Air Flow Rate Per Unit Heating Capacity {m3/s-W}
+  CoolingDesignCapacity,                  !- Cooling Design Capacity Method
+  autosize,                               !- Cooling Design Capacity {W}
+  234.7,                                  !- Cooling Design Capacity Per Floor Area {W/m2}
+  1,                                      !- Fraction of Autosized Cooling Design Capacity
+  HeatingDesignCapacity,                  !- Heating Design Capacity Method
+  autosize,                               !- Heating Design Capacity {W}
+  157,                                    !- Heating Design Capacity Per Floor Area {W/m2}
+  1,                                      !- Fraction of Autosized Heating Design Capacity
+  OnOff,                                  !- Central Cooling Capacity Control Method
+  autosize;                               !- Occupant Diversity
+
+OS:AvailabilityManagerAssignmentList,
+  {542038c4-a887-4bf5-b4d2-d4f98538ce45}, !- Handle
+  Air Loop HVAC 1 AvailabilityManagerAssignmentList; !- Name
+
+OS:Node,
+  {aea62603-ad1c-4e40-9386-576db581d223}, !- Handle
+  Node 9,                                 !- Name
+  {007954eb-1a1e-4e60-804a-00edb0afedda}, !- Inlet Port
+  {4d1ffacb-85fe-4e83-8563-d9b285b118e2}; !- Outlet Port
+
+OS:Connection,
+  {dd931870-f1f9-4740-9271-21fcb21bb217}, !- Handle
+  {9d289cd0-7b80-4584-8e64-dc03c4ef0a59}, !- Source Object
+  3,                                      !- Outlet Port
+  {2bbc1941-6efa-47a8-bd93-836729fb1e55}, !- Target Object
+  2;                                      !- Inlet Port
+
+OS:Connection,
+  {007954eb-1a1e-4e60-804a-00edb0afedda}, !- Handle
+  {c43169be-563e-4698-acc9-5ae5f6db17db}, !- Source Object
+  2,                                      !- Outlet Port
+  {aea62603-ad1c-4e40-9386-576db581d223}, !- Target Object
+  2;                                      !- Inlet Port
+
+OS:Connection,
+  {4d1ffacb-85fe-4e83-8563-d9b285b118e2}, !- Handle
+  {aea62603-ad1c-4e40-9386-576db581d223}, !- Source Object
+  3,                                      !- Outlet Port
+  {41146a0d-80bf-4aaa-81f0-5ae40bd46884}, !- Target Object
+  3;                                      !- Inlet Port
+
+OS:Node,
+  {0912213f-1b7a-4655-8045-4cb40d7ffd31}, !- Handle
+  Node 10,                                !- Name
+  {9de883d8-9350-40ff-be07-dbcdb1eb5aa7}, !- Inlet Port
+  {78d7e697-5124-48fb-80c7-0a938f5c03e3}; !- Outlet Port
+
+OS:Connection,
+  {9de883d8-9350-40ff-be07-dbcdb1eb5aa7}, !- Handle
+  {db790a58-149b-4323-8954-1bddfd57c610}, !- Source Object
+  3,                                      !- Outlet Port
+  {0912213f-1b7a-4655-8045-4cb40d7ffd31}, !- Target Object
+  2;                                      !- Inlet Port
+
+OS:Connection,
+  {78d7e697-5124-48fb-80c7-0a938f5c03e3}, !- Handle
+  {0912213f-1b7a-4655-8045-4cb40d7ffd31}, !- Source Object
+  3,                                      !- Outlet Port
+  {a88a9fb9-37e8-4ddc-9503-d9833d587a90}, !- Target Object
+  3;                                      !- Inlet Port
+
+OS:Connection,
+  {84fe7839-9ca5-4129-a4d8-a66eda945bfc}, !- Handle
+  {a88a9fb9-37e8-4ddc-9503-d9833d587a90}, !- Source Object
+  4,                                      !- Outlet Port
+  {9d289cd0-7b80-4584-8e64-dc03c4ef0a59}, !- Target Object
+  2;                                      !- Inlet Port
+
+OS:Connection,
+  {d6176604-ee60-43eb-aded-d8407f62b750}, !- Handle
+  {9f60d4f0-a4d4-4e2d-8dd7-def709c76d0c}, !- Source Object
+  9,                                      !- Outlet Port
+  {d01de46a-2113-43e3-a3d0-7fb17725205f}, !- Target Object
+  2;                                      !- Inlet Port
+
+OS:Node,
+  {47acbaad-fd90-48d9-ae0a-193bf2cdf68b}, !- Handle
+  Node 11,                                !- Name
+  {b933f56d-4091-4667-8b84-1b384685c5d4}, !- Inlet Port
+  {225793d3-3492-4c59-9652-75103272ae37}; !- Outlet Port
+
+OS:Connection,
+  {b933f56d-4091-4667-8b84-1b384685c5d4}, !- Handle
+  {f5f15b01-beba-40b1-b44c-6955d7dae9a6}, !- Source Object
+  6,                                      !- Outlet Port
+  {47acbaad-fd90-48d9-ae0a-193bf2cdf68b}, !- Target Object
+  2;                                      !- Inlet Port
+
+OS:Connection,
+  {225793d3-3492-4c59-9652-75103272ae37}, !- Handle
+  {47acbaad-fd90-48d9-ae0a-193bf2cdf68b}, !- Source Object
+  3,                                      !- Outlet Port
+  {9f60d4f0-a4d4-4e2d-8dd7-def709c76d0c}, !- Target Object
+  8;                                      !- Inlet Port
+
+OS:Node,
+  {920ce11c-7173-4bc6-811f-64add0e04d31}, !- Handle
+  Node 12,                                !- Name
+  {992984e7-eba1-4dfc-9314-319957856b1e}, !- Inlet Port
+  {e0182018-be3c-405c-a0a5-39b3712051eb}; !- Outlet Port
+
+OS:Connection,
+  {992984e7-eba1-4dfc-9314-319957856b1e}, !- Handle
+  {9a8dee0f-c8e9-487b-baa4-f6e54a704526}, !- Source Object
+  9,                                      !- Outlet Port
+  {920ce11c-7173-4bc6-811f-64add0e04d31}, !- Target Object
+  2;                                      !- Inlet Port
+
+OS:Connection,
+  {e0182018-be3c-405c-a0a5-39b3712051eb}, !- Handle
+  {920ce11c-7173-4bc6-811f-64add0e04d31}, !- Source Object
+  3,                                      !- Outlet Port
+  {f5f15b01-beba-40b1-b44c-6955d7dae9a6}, !- Target Object
+  5;                                      !- Inlet Port
+
+OS:Node,
+  {f7a65d08-de78-41b4-adf8-cead931fd95e}, !- Handle
+  Node 13,                                !- Name
+  {944e180a-ddb1-4c98-80e9-64bb4bf42413}, !- Inlet Port
+  {6595b5c8-66a5-47f5-8f70-bfa2ba1fb1b1}; !- Outlet Port
+
+OS:Connection,
+  {b0216618-b0f0-409f-9ba6-e61daa4d8076}, !- Handle
+  {bbfaf2fa-d5f5-4bbd-b1df-3d086c5a3af2}, !- Source Object
+  3,                                      !- Outlet Port
+  {74eff7fe-484c-4cdd-b51c-eaf89776a182}, !- Target Object
+  8;                                      !- Inlet Port
+
+OS:Connection,
+  {944e180a-ddb1-4c98-80e9-64bb4bf42413}, !- Handle
+  {74eff7fe-484c-4cdd-b51c-eaf89776a182}, !- Source Object
+  5,                                      !- Outlet Port
+  {f7a65d08-de78-41b4-adf8-cead931fd95e}, !- Target Object
+  2;                                      !- Inlet Port
+
+OS:Connection,
+  {6595b5c8-66a5-47f5-8f70-bfa2ba1fb1b1}, !- Handle
+  {f7a65d08-de78-41b4-adf8-cead931fd95e}, !- Source Object
+  3,                                      !- Outlet Port
+  {9a8dee0f-c8e9-487b-baa4-f6e54a704526}, !- Target Object
+  8;                                      !- Inlet Port
+
+OS:Node,
+  {8650ff70-4110-4486-b9cf-55045c69b8b2}, !- Handle
+  Node 14,                                !- Name
+  {cda88b8d-3c30-4558-bd09-199d83eb2681}, !- Inlet Port
+  {1f32e296-3697-43ea-a35c-975adf4ce3f2}; !- Outlet Port
+
+OS:Connection,
+  {1f32e296-3697-43ea-a35c-975adf4ce3f2}, !- Handle
+  {8650ff70-4110-4486-b9cf-55045c69b8b2}, !- Source Object
+  3,                                      !- Outlet Port
+  {74eff7fe-484c-4cdd-b51c-eaf89776a182}, !- Target Object
+  6;                                      !- Inlet Port
+
+OS:Connection,
+  {88afa5dc-ee79-4915-8497-0a611e83353b}, !- Handle
+  {b7879d15-2825-4e66-8d79-77df279d72d8}, !- Source Object
+  3,                                      !- Outlet Port
+  {e7f67a2c-5b6d-48fb-bd4d-3932e54aa7e1}, !- Target Object
+  6;                                      !- Inlet Port
+
+OS:Connection,
+  {cda88b8d-3c30-4558-bd09-199d83eb2681}, !- Handle
+  {e7f67a2c-5b6d-48fb-bd4d-3932e54aa7e1}, !- Source Object
+  7,                                      !- Outlet Port
+  {8650ff70-4110-4486-b9cf-55045c69b8b2}, !- Target Object
+  2;                                      !- Inlet Port
+
+OS:SetpointManager:MixedAir,
+  {78713106-566f-4cfa-babe-f0b490104dd2}, !- Handle
+  Setpoint Manager Mixed Air 1,           !- Name
+  Temperature,                            !- Control Variable
+  {d01de46a-2113-43e3-a3d0-7fb17725205f}, !- Reference Setpoint Node Name
+  {47acbaad-fd90-48d9-ae0a-193bf2cdf68b}, !- Fan Inlet Node Name
+  {d01de46a-2113-43e3-a3d0-7fb17725205f}, !- Fan Outlet Node Name
+  {f7a65d08-de78-41b4-adf8-cead931fd95e}; !- Setpoint Node or NodeList Name
+
+OS:SetpointManager:MixedAir,
+  {717b213c-4a9b-414c-a2cf-f67eb868199b}, !- Handle
+  Setpoint Manager Mixed Air 2,           !- Name
+  Temperature,                            !- Control Variable
+  {d01de46a-2113-43e3-a3d0-7fb17725205f}, !- Reference Setpoint Node Name
+  {47acbaad-fd90-48d9-ae0a-193bf2cdf68b}, !- Fan Inlet Node Name
+  {d01de46a-2113-43e3-a3d0-7fb17725205f}, !- Fan Outlet Node Name
+  {920ce11c-7173-4bc6-811f-64add0e04d31}; !- Setpoint Node or NodeList Name
+
+OS:SetpointManager:MixedAir,
+  {ccb2da17-a1a0-424e-a232-7cfcf2667d99}, !- Handle
+  Setpoint Manager Mixed Air 3,           !- Name
+  Temperature,                            !- Control Variable
+  {d01de46a-2113-43e3-a3d0-7fb17725205f}, !- Reference Setpoint Node Name
+  {47acbaad-fd90-48d9-ae0a-193bf2cdf68b}, !- Fan Inlet Node Name
+  {d01de46a-2113-43e3-a3d0-7fb17725205f}, !- Fan Outlet Node Name
+  {47acbaad-fd90-48d9-ae0a-193bf2cdf68b}; !- Setpoint Node or NodeList Name
+
+OS:SetpointManager:MixedAir,
+  {82c82c4f-48ba-4dc0-9a20-8dde7fc7eb7c}, !- Handle
+  Setpoint Manager Mixed Air 4,           !- Name
+  Temperature,                            !- Control Variable
+  {d01de46a-2113-43e3-a3d0-7fb17725205f}, !- Reference Setpoint Node Name
+  {47acbaad-fd90-48d9-ae0a-193bf2cdf68b}, !- Fan Inlet Node Name
+  {d01de46a-2113-43e3-a3d0-7fb17725205f}, !- Fan Outlet Node Name
+  {8650ff70-4110-4486-b9cf-55045c69b8b2}; !- Setpoint Node or NodeList Name
+
+OS:SetpointManager:SingleZone:Reheat,
+  {bd3ce314-afb8-423d-beb3-4c6f2fa028bd}, !- Handle
+  Setpoint Manager Single Zone Reheat 1,  !- Name
+  -99,                                    !- Minimum Supply Air Temperature {C}
+  99,                                     !- Maximum Supply Air Temperature {C}
+  {a89ba35e-b1a0-496b-9210-5fa1050abf06}, !- Control Zone Name
+  {d01de46a-2113-43e3-a3d0-7fb17725205f}; !- Setpoint Node or NodeList Name
+
+OS:Output:Variable,
+  {e655e325-b11b-482e-b831-4cfb2d74e1a3}, !- Handle
+  Output Variable 1,                      !- Name
+  ,                                       !- Key Value
+  Zone Outdoor Air Drybulb Temperature;   !- Variable Name
+
+OS:Output:Variable,
+  {8b1a8687-1716-4953-8882-738999af05de}, !- Handle
+  Output Variable 2,                      !- Name
+  ,                                       !- Key Value
+  Zone Outdoor Air Wetbulb Temperature;   !- Variable Name
+
+OS:Output:Variable,
+  {ffc74e02-c88e-45a3-a1d1-6ecf1525bb56}, !- Handle
+  Output Variable 3,                      !- Name
+  ,                                       !- Key Value
+  Surface Inside Face Temperature;        !- Variable Name
+
+OS:Output:Variable,
+  {6e33edc1-d093-412b-92e0-2d01673580ac}, !- Handle
+  Output Variable 4,                      !- Name
+  ,                                       !- Key Value
+  Surface Outside Face Temperature;       !- Variable Name
+
+OS:Output:Meter,
+  {c9c8f702-5279-4c88-b4b0-7d3e04a1dcaf}, !- Handle
+  Electricity:Facility,                   !- Name
+  Hourly;                                 !- Reporting Frequency
+
+OS:Output:Meter,
+  {8bf49588-4210-48c8-aa60-1a60a0d28cd4}, !- Handle
+  NaturalGas:Facility,                    !- Name
+  Hourly;                                 !- Reporting Frequency
+
+OS:Output:Meter,
+  {0e9cb85d-8940-4610-98d2-54c80bd31c29}, !- Handle
+  Propane:Facility,                       !- Name
+  Hourly;                                 !- Reporting Frequency
+
+OS:Construction,
+  {6dca80f0-f84e-4faa-b299-51d9d4c2124a}, !- Handle
+  Exterior Roof adj roof insulation,      !- Name
+  ,                                       !- Surface Rendering Name
+  {000ad83e-50c1-4288-a6b0-a1cd55e304d7}, !- Layer 1
+  {fdf5676d-da9f-4d0c-ae01-8dbc1271590d}, !- Layer 2
+  {194d3589-e41b-4be8-b737-0449067001f9}; !- Layer 3
+
+OS:Material,
+  {194d3589-e41b-4be8-b737-0449067001f9}, !- Handle
+  F16 Acoustic tile_R-value 35.0% increase, !- Name
+  MediumSmooth,                           !- Roughness
+  0.025785,                               !- Thickness {m}
+  0.06,                                   !- Conductivity {W/m-K}
+  368,                                    !- Density {kg/m3}
+  590;                                    !- Specific Heat {J/kg-K}
+
+OS:DefaultConstructionSet,
+  {2abf4457-9986-4dcb-858e-9ce8c65c48b3}, !- Handle
+  Default Constructions adj roof insulation, !- Name
+  {92017620-2de9-4a7e-aec1-034ffdacc9ff}, !- Default Exterior Surface Constructions Name
+  {1bd8b5da-c530-41e7-bc5b-c59d79f22a75}, !- Default Interior Surface Constructions Name
+  {ce428980-9c44-4b34-a281-6da7b6b01295}, !- Default Ground Contact Surface Constructions Name
+  {54006d19-ccef-48f1-b745-56c6f500bd2c}, !- Default Exterior SubSurface Constructions Name
+  {06b8308f-8f7a-49f2-8739-589f1b951054}, !- Default Interior SubSurface Constructions Name
+  {09532d4c-944b-423e-acf3-558eb899fb6b}, !- Interior Partition Construction Name
+  ,                                       !- Space Shading Construction Name
+  ,                                       !- Building Shading Construction Name
+  ,                                       !- Site Shading Construction Name
+  ;                                       !- Adiabatic Surface Construction Name
+
+OS:DefaultSurfaceConstructions,
+  {92017620-2de9-4a7e-aec1-034ffdacc9ff}, !- Handle
+  Exterior Surface Constructions adj roof insulation, !- Name
+  ,                                       !- Floor Construction Name
+  {99a23b6a-496d-4bbf-8db9-4c428f727222}, !- Wall Construction Name
+  {6dca80f0-f84e-4faa-b299-51d9d4c2124a}; !- Roof Ceiling Construction Name
+
diff --git a/resources/Examples/compact_osw/measures/IncreaseRoofRValuePython/tests/test_measure.py b/resources/Examples/compact_osw/measures/IncreaseRoofRValuePython/tests/test_measure.py
new file mode 100644
index 00000000000..73430deeca9
--- /dev/null
+++ b/resources/Examples/compact_osw/measures/IncreaseRoofRValuePython/tests/test_measure.py
@@ -0,0 +1,103 @@
+import pytest
+import openstudio
+import pathlib
+from measure import IncreaseInsulationRValueForRoofsByPercentagePython
+
+
+class TestIncreaseInsulationRValueForRoofsByPercentagePython:
+
+    def test_number_of_arguments_and_argument_names(self):
+        """
+        Test that the arguments are what we expect
+        """
+        # create an instance of the measure
+        measure = IncreaseInsulationRValueForRoofsByPercentagePython()
+
+        # make an empty model
+        model = openstudio.model.Model()
+
+        # get arguments and test that they are what we are expecting
+        arguments = measure.arguments(model)
+        assert arguments.size() == 1
+        assert arguments[0].name() == 'r_value'
+
+    def test_optional_model_for_arguments(self):
+        """
+        Tests that passing model to arguments() is optional
+        """
+        # create an instance of the measure
+        measure = IncreaseInsulationRValueForRoofsByPercentagePython()
+
+        # Ruby allows **not** passing model to the method, so test that
+        arguments = measure.arguments()
+        assert arguments.size() == 1
+        assert arguments[0].name() == 'r_value'
+
+    def test_good_argument_values(self):
+        """
+        Test running the measure with appropriate arguments, and that the
+        measure runs fine and with expected results
+        """
+
+        # create an instance of the measure
+        measure = IncreaseInsulationRValueForRoofsByPercentagePython()
+
+        # create runner with empty OSW
+        # osw = openstudio.WorkflowJSON()    # TODO: FIXME
+        osw = openstudio.openstudioutilitiesfiletypes.WorkflowJSON()
+        runner = openstudio.measure.OSRunner(osw)
+
+        # load the test model
+        # translator = openstudio.osversion.VersionTranslator()
+        # path = openstudio.toPath(
+        #     str(pathlib.Path(__file__).parent.absolute()
+        #         / "examle_model.osm"))
+        # model = translator.loadModel(path)
+        # assert(model.is_initialized())
+        # model = model.get()
+
+        model = openstudio.model.exampleModel()
+
+        arguments = measure.arguments(model)
+        argument_map = openstudio.measure.convertOSArgumentVectorToMap(arguments)
+
+        # create hash of argument values.
+        # If the argument has a default that you want to use,
+        # you don't need it in the dict
+        args_dict = {}
+        args_dict['r_value'] = 35.0
+        # using defaults values from measure.py for other arguments
+
+        # populate argument with specified hash value if specified
+        for arg in arguments:
+            temp_arg_var = arg.clone()
+            if arg.name() in args_dict:
+                assert(temp_arg_var.setValue(args_dict[arg.name()]))
+                argument_map[arg.name()] = temp_arg_var
+
+        print("run measure:")
+
+        # run the measure
+        measure.run(model, runner, argument_map)
+        result = runner.result()
+
+        # show_output(result)
+        print(f"results: {result}")
+
+        # assert that it ran correctly
+        assert result.value().valueName() == 'Success'
+        assert len(result.info()) == 1
+        assert len(result.warnings()) == 0
+
+        assert result.finalCondition().get().logMessage() == (
+            "The existing insulation for roofs was increased by 35.0%. This was applied to 4,306 (ft^2) "
+            "across 1 roof constructions: Exterior Roof adj roof insulation (R-4.6).")
+        assert result.info()[0].logMessage() == (
+            "For construction'Exterior Roof adj roof insulation', "
+            "material 'F16 Acoustic tile_R-value 35.0% increase' was altered.")
+
+        # save the model to test output directory
+        output_file_path = openstudio.toPath(
+            str(pathlib.Path(__file__).parent.absolute()
+                / "output" / "test_output.osm"))
+        model.save(output_file_path, True)
diff --git a/src/cli/measure_manager_test.rb b/ruby/engine/measure_manager_test.rb
similarity index 100%
rename from src/cli/measure_manager_test.rb
rename to ruby/engine/measure_manager_test.rb
diff --git a/ruby/test/CMakeLists.txt b/ruby/test/CMakeLists.txt
new file mode 100644
index 00000000000..2082774c11b
--- /dev/null
+++ b/ruby/test/CMakeLists.txt
@@ -0,0 +1,62 @@
+if(BUILD_TESTING)
+
+  # find all tests
+  file(GLOB RUBY_TEST_SRC "./*.rb")
+
+  # TODO: It doesn't not work with this executable that's in build/Ruby-install/bin
+  # message("RUBY_EXECUTABLE=${RUBY_EXECUTABLE}")
+
+  set(_RUBY_POSSIBLE_EXECUTABLE_NAMES
+    ruby
+    ruby2.7
+    ruby2.7.2
+    ruby27
+    ruby272)
+
+  # TODO: this isn't great but I haven't found a better way to locate the system ruby (and avoid the one in build/Ruby-install/)
+  find_program(SYSTEM_RUBY_EXECUTABLE NAMES ${_RUBY_POSSIBLE_EXECUTABLE_NAMES}
+    HINTS
+      "/usr/local/rvm/rubies/ruby-2.7.2/bin/"
+      "/usr/local/rvm/rubies/ruby-2.7.2/bin/ruby"
+      "/usr/share/rvm/rubies/ruby-2.7.2/bin/"
+      "$ENV{HOME}/.rvm/rubies/ruby-2.7.2/bin/"
+
+      "C:/Ruby27-x64/bin/"
+
+       "/usr/local/ruby272/bin/"
+       "/usr/local/ruby27/bin/"
+       "/usr/bin/"
+       "/usr/local/bin/"
+
+
+    NO_DEFAULT_PATH)
+
+  if(NOT SYSTEM_RUBY_EXECUTABLE)
+    message(WARNING "Your system ruby wasn't found, you won't be able to run the `ctest -R RubyTest` command and the tests won't be created at all.")
+  else()
+    message(STATUS "Found SYSTEM_RUBY_EXECUTABLE=${SYSTEM_RUBY_EXECUTABLE}")
+
+    # add a test for each unit test
+    set(RUBY_TEST_REQUIRES "#include test files")
+    foreach(f ${RUBY_TEST_SRC})
+
+      file(READ "${f}" CONTENTS)
+      string(REGEX MATCHALL "def +test_([A-Za-z_0-9 ]+)" FOUND_TESTS ${CONTENTS})
+
+      foreach(HIT ${FOUND_TESTS})
+        string(REGEX REPLACE "def +test_([A-Za-z_0-9]+)" "\\1" TEST_NAME ${HIT})
+        string(REGEX MATCH "/?([A-Za-z_0-9 ]+)\\.rb" FILE_NAME ${f})
+        string(REGEX REPLACE "/?([A-Za-z_0-9 ]+)\\.rb" "\\1" FILE_NAME ${FILE_NAME})
+
+        # Call with Ruby itself
+        add_test(NAME "RubyTest-${FILE_NAME}-${TEST_NAME}"
+          COMMAND "${CMAKE_COMMAND}" -E chdir "${CMAKE_CURRENT_BINARY_DIR}"
+          "${SYSTEM_RUBY_EXECUTABLE}" "-I" "$<TARGET_FILE_DIR:openstudio_rb>" "${f}" "--name=test_${TEST_NAME}"
+        )
+
+        set_tests_properties("RubyTest-${FILE_NAME}-${TEST_NAME}" PROPERTIES TIMEOUT 660 )
+      endforeach()
+    endforeach()
+
+  endif()
+endif()
diff --git a/ruby/openstudio_test.in b/ruby/test/openstudio_test.in
similarity index 100%
rename from ruby/openstudio_test.in
rename to ruby/test/openstudio_test.in
diff --git a/src/cli/test/execute_python_script_with_arguments_test.py b/src/cli/test/execute_python_script_with_arguments_test.py
new file mode 100644
index 00000000000..6e756c4175e
--- /dev/null
+++ b/src/cli/test/execute_python_script_with_arguments_test.py
@@ -0,0 +1,13 @@
+import sys
+from pathlib import Path
+
+argv0 = sys.argv[0]
+thisFile = Path(__file__)
+# In Python, argv[0] is the name of the program
+if argv0 != thisFile.name:
+    raise ValueError(f"Wrong sys.argv[0] / program name:  sys.argv[0]={argv0}, __file__={thisFile}")
+
+if len(sys.argv) != 3:
+    raise ValueError("You must supply two arguments to this script")
+
+print(f"Hello from {sys.argv[1]}, at {sys.argv[2]}")
diff --git a/src/cli/test/execute_ruby_script_with_arguments_test.rb b/src/cli/test/execute_ruby_script_with_arguments_test.rb
new file mode 100644
index 00000000000..0d415e04b6d
--- /dev/null
+++ b/src/cli/test/execute_ruby_script_with_arguments_test.rb
@@ -0,0 +1,2 @@
+raise "You must supply 2 arguments" if ARGV.size != 2
+puts "Hello from #{ARGV[0]}, at #{ARGV[1]}"
diff --git a/src/utilities/bcl/templates/EnergyPlusMeasure/tests/energyplus_measure_test.rb b/src/utilities/bcl/templates/EnergyPlusMeasure/tests/energyplus_measure_test.rb
index e56bddf7a9b..b486a8bc82b 100644
--- a/src/utilities/bcl/templates/EnergyPlusMeasure/tests/energyplus_measure_test.rb
+++ b/src/utilities/bcl/templates/EnergyPlusMeasure/tests/energyplus_measure_test.rb
@@ -23,6 +23,8 @@ def test_number_of_arguments_and_argument_names
     arguments = measure.arguments(workspace)
     assert_equal(1, arguments.size)
     assert_equal('zone_name', arguments[0].name)
+    assert(arguments[0].required)
+    assert_equal('String', arguments[0].type.valueName)
   end
 
   def test_bad_argument_values
diff --git a/src/utilities/bcl/templates/EnergyPlusMeasure/tests/test_energyplus_measure.py b/src/utilities/bcl/templates/EnergyPlusMeasure/tests/test_energyplus_measure.py
new file mode 100644
index 00000000000..e47e8c43fdc
--- /dev/null
+++ b/src/utilities/bcl/templates/EnergyPlusMeasure/tests/test_energyplus_measure.py
@@ -0,0 +1,117 @@
+"""Insert your copyright here."""
+
+from pathlib import Path
+
+import openstudio
+import pytest
+
+from measure import EnergyPlusMeasureName
+
+
+class TestEnergyPlusMeasureName:
+    """Py.test module for EnergyPlusMeasureName."""
+
+    def test_number_of_arguments_and_argument_names(self):
+        """Test that the arguments are what we expect."""
+        # create an instance of the measure
+        measure = EnergyPlusMeasureName()
+
+        # make an empty workspace
+        workspace = openstudio.Workspace(openstudio.StrictnessLevel("Draft"), openstudio.IddFileType("EnergyPlus"))
+
+        # get arguments and test that they are what we are expecting
+        arguments = measure.arguments(workspace)
+        assert arguments.size() == 1
+        assert arguments[0].name() == "zone_name"
+        assert arguments[0].required()
+        assert arguments[0].type().valueName() == "String"
+
+    def test_bad_argument_values(self):
+        """Test running the measure with inappropriate arguments, and that the measure reports failure."""
+        # create an instance of the measure
+        measure = EnergyPlusMeasureName()
+
+        # create runner with empty OSW
+        # osw = openstudio.WorkflowJSON()    # TODO: FIXME
+        osw = openstudio.openstudioutilitiesfiletypes.WorkflowJSON()
+        runner = openstudio.measure.OSRunner(osw)
+
+        # make an empty workspace
+        workspace = openstudio.Workspace(openstudio.StrictnessLevel("Draft"), openstudio.IddFileType("EnergyPlus"))
+
+        # get arguments
+        arguments = measure.arguments(workspace)
+        argument_map = openstudio.measure.convertOSArgumentVectorToMap(arguments)
+
+        # set argument values to bad value
+        zone_name = arguments[0].clone()
+        assert zone_name.setValue("")
+        argument_map["zone_name"] = zone_name
+
+        # run the measure
+        measure.run(workspace, runner, argument_map)
+        result = runner.result()
+        assert result.value().valueName() == "Fail"
+
+        # check that there are still no thermal zones
+        assert len(workspace.getObjectsByType("Zone")) == 0
+
+    def test_good_argument_values(self):
+        """Test running the measure with appropriate arguments.
+
+        Asserts that the measure runs fine and with expected results.
+        """
+        # create an instance of the measure
+        measure = EnergyPlusMeasureName()
+
+        # create runner with empty OSW
+        # osw = openstudio.WorkflowJSON()    # TODO: FIXME
+        osw = openstudio.openstudioutilitiesfiletypes.WorkflowJSON()
+        runner = openstudio.measure.OSRunner(osw)
+
+        # make an empty workspace
+        workspace = openstudio.Workspace(openstudio.StrictnessLevel("Draft"), openstudio.IddFileType("EnergyPlus"))
+
+        # check that there are no thermal zones
+        assert len(workspace.getObjectsByType("Zone")) == 0
+
+        # get arguments
+        arguments = measure.arguments(workspace)
+        argument_map = openstudio.measure.convertOSArgumentVectorToMap(arguments)
+
+        # set argument values to good values
+        # create hash of argument values.
+        # If the argument has a default that you want to use,
+        # you don't need it in the dict
+        args_dict = {}
+        args_dict["zone_name"] = "New Zone"
+        # using defaults values from measure.py for other arguments
+
+        # populate argument with specified hash value if specified
+        for arg in arguments:
+            temp_arg_var = arg.clone()
+            if arg.name() in args_dict:
+                assert temp_arg_var.setValue(args_dict[arg.name()])
+                argument_map[arg.name()] = temp_arg_var
+
+        # run the measure
+        measure.run(workspace, runner, argument_map)
+        result = runner.result()
+
+        # show the output
+        print(f"results: {result}")
+
+        # assert that it ran correctly
+        assert result.value().valueName() == "Success"
+
+        # check that there is now 1 thermal zones
+        assert len(workspace.getObjectsByType("Zone")) == 1
+
+        # check that zone is properly named
+        zone = workspace.getObjectsByType("Zone")[0]
+        assert zone.getString(0).is_initialized()
+        assert zone.getString(0).get() == "New Zone"
+
+        # save the workspace to test output directory
+        output_file_path = openstudio.toPath(str(Path(__file__).parent.absolute() / "output" / "test_output.idf"))
+        workspace.save(output_file_path, True)
diff --git a/src/utilities/bcl/templates/ModelMeasure/tests/test_model_measure.py b/src/utilities/bcl/templates/ModelMeasure/tests/test_model_measure.py
new file mode 100644
index 00000000000..972ff5d6c05
--- /dev/null
+++ b/src/utilities/bcl/templates/ModelMeasure/tests/test_model_measure.py
@@ -0,0 +1,128 @@
+"""insert your copyright here."""
+
+import pytest
+import openstudio
+from pathlib import Path
+from measure import ModelMeasureName
+
+
+class TestModelMeasureName:
+    """Py.test module for ModelMeasureName."""
+
+    def test_number_of_arguments_and_argument_names(self):
+        """Test that the arguments are what we expect."""
+        # create an instance of the measure
+        measure = ModelMeasureName()
+
+        # make an empty model
+        model = openstudio.model.Model()
+
+        # get arguments and test that they are what we are expecting
+        arguments = measure.arguments(model)
+        assert arguments.size() == 1
+        assert arguments[0].name() == "space_name"
+
+    def test_bad_argument_values(self):
+        """Test running the measure with inappropriate arguments, and that the measure reports failure."""
+        # create an instance of the measure
+        measure = ModelMeasureName()
+
+        # create runner with empty OSW
+        # osw = openstudio.WorkflowJSON()    # TODO: FIXME
+        osw = openstudio.openstudioutilitiesfiletypes.WorkflowJSON()
+        runner = openstudio.measure.OSRunner(osw)
+
+        # Make an empty model
+        model = openstudio.model.Model()
+
+        # get arguments
+        arguments = measure.arguments(model)
+        argument_map = openstudio.measure.convertOSArgumentVectorToMap(arguments)
+
+        # create hash of argument values.
+        # If the argument has a default that you want to use,
+        # you don't need it in the dict
+        args_dict = {}
+        args_dict["space_name"] = ""
+
+        # populate argument with specified hash value if specified
+        for arg in arguments:
+            temp_arg_var = arg.clone()
+            if arg.name() in args_dict:
+                assert temp_arg_var.setValue(args_dict[arg.name()])
+                argument_map[arg.name()] = temp_arg_var
+
+        # run the measure
+        measure.run(model, runner, argument_map)
+        result = runner.result()
+
+        # show the output
+        # show_output(result)
+        print(f"results: {result}")
+
+        # assert that it failed
+        assert result.value().valueName() == "Fail"
+
+    def test_good_argument_values(self):
+        """Test running the measure with appropriate arguments.
+
+        Asserts that the measure runs fine and with expected results.
+        """
+        # create an instance of the measure
+        measure = ModelMeasureName()
+
+        # create runner with empty OSW
+        # osw = openstudio.WorkflowJSON()    # TODO: FIXME
+        osw = openstudio.openstudioutilitiesfiletypes.WorkflowJSON()
+        runner = openstudio.measure.OSRunner(osw)
+
+        # load the test model
+        translator = openstudio.osversion.VersionTranslator()
+        path = openstudio.toPath(str(Path(__file__).parent.absolute() / "example_model.osm"))
+        model = translator.loadModel(path)
+        assert model.is_initialized()
+        model = model.get()
+
+        # store the number of spaces in the seed model
+        num_spaces_seed = len(model.getSpaces())
+
+        # get arguments
+        arguments = measure.arguments(model)
+        argument_map = openstudio.measure.convertOSArgumentVectorToMap(arguments)
+
+        # create hash of argument values.
+        # If the argument has a default that you want to use,
+        # you don't need it in the dict
+        args_dict = {}
+        args_dict["space_name"] = "New Space"
+        # using defaults values from measure.py for other arguments
+
+        # populate argument with specified hash value if specified
+        for arg in arguments:
+            temp_arg_var = arg.clone()
+            if arg.name() in args_dict:
+                assert temp_arg_var.setValue(args_dict[arg.name()])
+                argument_map[arg.name()] = temp_arg_var
+
+        # run the measure
+        measure.run(model, runner, argument_map)
+        result = runner.result()
+
+        # show the output
+        # show_output(result)
+        print(f"results: {result}")
+
+        # assert that it ran correctly
+        assert result.value().valueName() == "Success"
+        assert len(result.info()) == 1
+        assert len(result.warnings()) == 0
+
+        assert result.info()[0].logMessage() == "Space New Space was added."
+
+        # check that there is now 1 more space
+        num_spaces_final = len(model.getSpaces())
+        assert num_spaces_final == num_spaces_seed + 1
+
+        # save the model to test output directory
+        output_file_path = openstudio.toPath(str(Path(__file__).parent.absolute() / "output" / "test_output.osm"))
+        model.save(output_file_path, True)
diff --git a/src/utilities/bcl/templates/ReportingMeasure/tests/test_reporting_measure.py b/src/utilities/bcl/templates/ReportingMeasure/tests/test_reporting_measure.py
new file mode 100644
index 00000000000..8c88a9f9f53
--- /dev/null
+++ b/src/utilities/bcl/templates/ReportingMeasure/tests/test_reporting_measure.py
@@ -0,0 +1,236 @@
+"""Insert your copyright here."""
+
+from pathlib import Path
+import os
+import openstudio
+import pytest
+import subprocess
+
+from measure import ReportingMeasureName
+
+CURRENT_DIR_PATH = Path(__file__).parent.absolute()
+MODEL_IN_PATH_DEFAULT = CURRENT_DIR_PATH / "example_model.osm"
+EPW_IN_PATH_DEFAULT = CURRENT_DIR_PATH / "USA_CO_Golden-NREL.724666_TMY3.epw"
+
+
+class TestReportingMeasureName:
+    """Py.test module for ReportingMeasureName."""
+
+    @staticmethod
+    def run_dir(test_name: str) -> Path:
+        return CURRENT_DIR_PATH / f"output/{test_name}"
+
+    @staticmethod
+    def model_out_path(test_name) -> Path:
+        return TestReportingMeasureName.run_dir(test_name) / "example_model.osm"
+
+    @staticmethod
+    def sql_path(test_name) -> Path:
+        return TestReportingMeasureName.run_dir(test_name) / "run/eplusout.sql"
+
+    @staticmethod
+    def report_path(test_name) -> Path:
+        return TestReportingMeasureName.run_dir(test_name) / "report.html"
+
+    @staticmethod
+    def setup_test(test_name: str, idf_output_requests: openstudio.IdfObjectVector, model_in_path: Path = MODEL_IN_PATH_DEFAULT, epw_path: Path =
+                   EPW_IN_PATH_DEFAULT):
+
+        run_dir = TestReportingMeasureName.run_dir(test_name)
+        run_dir.mkdir(parents=True, exist_ok=True)
+
+        report_path = TestReportingMeasureName.report_path(test_name)
+        if report_path.exists():
+            report_path.unlink()
+
+        assert model_in_path.exists()
+
+        model_out_path = TestReportingMeasureName.model_out_path(test_name)
+        if model_out_path.exists():
+            model_out_path.unlink()
+
+        # convert output requests to OSM for testing, OS App and PAT will add these to the E+ Idf
+        workspace = openstudio.Workspace(openstudio.StrictnessLevel("Draft"), openstudio.IddFileType("EnergyPlus"))
+        workspace.addObjects(idf_output_requests)
+        rt = openstudio.energyplus.ReverseTranslator()
+        request_model = rt.translateWorkspace(workspace)
+
+        translator = openstudio.osversion.VersionTranslator()
+        model = translator.loadModel(openstudio.toPath(str(model_in_path)))
+        assert model.is_initialized()
+        model = model.get()
+        model.addObjects(request_model.objects())
+        model.save(str(model_out_path), True)
+
+        if os.environ.get('OPENSTUDIO_TEST_NO_CACHE_SQLFILE'):
+            sql_file = TestReportingMeasureName.sql_path(test_name)
+            if sql_file.exists():
+                sql_file.unlink()
+
+        osw_path = run_dir / 'in.osw'
+
+        # workflow = openstudio.WorkflowJSON()    # TODO: FIXME
+        workflow = openstudio.openstudioutilitiesfiletypes.WorkflowJSON()
+
+        workflow.setSeedFile(str(model_out_path))
+        workflow.setWeatherFile(str(epw_path))
+        workflow.saveAs(str(osw_path))
+
+        # TODO: use the system openstudio for now, replace with openstudio.getOpenStudioCLI eventually
+        cli_path = 'openstudio'
+        args = [cli_path, 'run', '-w', str(osw_path)]
+        print(" ".join(args))
+        subprocess.check_call(args)
+
+    def test_number_of_arguments_and_argument_names(self):
+        """Test that the arguments are what we expect."""
+        # create an instance of the measure
+        measure = ReportingMeasureName()
+
+        # make an empty model
+        model = openstudio.model.Model()
+
+        # get arguments and test that they are what we are expecting
+        arguments = measure.arguments(model)
+        assert arguments.size() == 1
+        assert arguments[0].name() == "report_drybulb_temp"
+
+    def test_with_drybulb_temp(self):
+        """Test running the measure with appropriate arguments, with db temp."""
+        test_name = 'test_with_drybulb_temp'
+
+        # create an instance of the measure
+        measure = ReportingMeasureName()
+
+        # create runner with empty OSW
+        # osw = openstudio.WorkflowJSON()    # TODO: FIXME
+        osw = openstudio.openstudioutilitiesfiletypes.WorkflowJSON()
+        runner = openstudio.measure.OSRunner(osw)
+
+        # make an empty model
+        model = openstudio.model.Model()
+
+        # get arguments
+        arguments = measure.arguments(model)
+        argument_map = openstudio.measure.convertOSArgumentVectorToMap(arguments)
+
+        # set argument values to bad value
+        report_drybulb_temp = arguments[0].clone()
+        assert report_drybulb_temp.setValue(True)
+        argument_map["report_drybulb_temp"] = report_drybulb_temp
+
+        # temp set path so idf_output_requests work
+        runner.setLastOpenStudioModelPath(openstudio.toPath(str(MODEL_IN_PATH_DEFAULT)))
+
+        # get the energyplus output requests, this will be done automatically by OS App and PAT
+        idf_output_requests = measure.energyPlusOutputRequests(runner, argument_map)
+        assert idf_output_requests.size() == 1
+
+        # mimic the process of running this measure in OS App or PAT. Optionally set custom model_in_path and custom epw_path.
+        epw_path = EPW_IN_PATH_DEFAULT
+        TestReportingMeasureName.setup_test(test_name=test_name, idf_output_requests=idf_output_requests)
+
+        model_out_path = TestReportingMeasureName.model_out_path(test_name)
+        assert model_out_path.exists()
+        assert epw_path.exists()
+        assert TestReportingMeasureName.sql_path(test_name).exists()
+
+        # set up runner, this will happen automatically when measure is run in PAT or OpenStudio
+        runner.setLastOpenStudioModelPath(openstudio.toPath(str(model_out_path)))
+        runner.setLastEpwFilePath(openstudio.toPath(str(epw_path)))
+        runner.setLastEnergyPlusSqlFilePath(openstudio.toPath(str(TestReportingMeasureName.sql_path(test_name))))
+
+        # delete the output if it exists
+        report_path = TestReportingMeasureName.report_path(test_name)
+        if report_path.exists():
+            report_path.unlink()
+
+        assert not report_path.exists()
+
+        # temporarily change directory to the run directory and run the measure
+        start_dir = Path.cwd()
+        #try:
+        os.chdir(TestReportingMeasureName.run_dir(test_name))
+
+        # run the measure
+        measure.run(runner, argument_map)
+        result = runner.result()
+        print(result)
+        assert result.value().valueName() == "Success"
+        assert len(result.warnings()) == 0
+        os.chdir(start_dir)
+        #except:
+        #    os.chdir(start_dir)
+
+        # make sure the report file exists
+        assert report_path.exists()
+
+    def test_without_drybulb_temp(self):
+        """Test running the measure with appropriate arguments, without db temp."""
+        test_name = 'test_without_drybulb_temp'
+
+        # create an instance of the measure
+        measure = ReportingMeasureName()
+
+        # create runner with empty OSW
+        # osw = openstudio.WorkflowJSON()    # TODO: FIXME
+        osw = openstudio.openstudioutilitiesfiletypes.WorkflowJSON()
+        runner = openstudio.measure.OSRunner(osw)
+
+        # make an empty model
+        model = openstudio.model.Model()
+
+        # get arguments
+        arguments = measure.arguments(model)
+        argument_map = openstudio.measure.convertOSArgumentVectorToMap(arguments)
+
+        # set argument values to bad value
+        report_drybulb_temp = arguments[0].clone()
+        assert report_drybulb_temp.setValue(False)
+        argument_map["report_drybulb_temp"] = report_drybulb_temp
+
+        # temp set path so idf_output_requests work
+        runner.setLastOpenStudioModelPath(openstudio.toPath(str(MODEL_IN_PATH_DEFAULT)))
+
+        # get the energyplus output requests, this will be done automatically by OS App and PAT
+        idf_output_requests = measure.energyPlusOutputRequests(runner, argument_map)
+        assert idf_output_requests.size() == 0
+
+        # mimic the process of running this measure in OS App or PAT. Optionally set custom model_in_path and custom epw_path.
+        epw_path = EPW_IN_PATH_DEFAULT
+        TestReportingMeasureName.setup_test(test_name=test_name, idf_output_requests=idf_output_requests)
+
+        model_out_path = TestReportingMeasureName.model_out_path(test_name)
+        assert model_out_path.exists()
+        assert epw_path.exists()
+        assert TestReportingMeasureName.sql_path(test_name).exists()
+
+        # set up runner, this will happen automatically when measure is run in PAT or OpenStudio
+        runner.setLastOpenStudioModelPath(openstudio.toPath(str(model_out_path)))
+        runner.setLastEpwFilePath(openstudio.toPath(str(epw_path)))
+        runner.setLastEnergyPlusSqlFilePath(openstudio.toPath(str(TestReportingMeasureName.sql_path(test_name))))
+
+        # delete the output if it exists
+        report_path = TestReportingMeasureName.report_path(test_name)
+        if report_path.exists():
+            report_path.unlink()
+
+        assert not report_path.exists()
+
+        # temporarily change directory to the run directory and run the measure
+        start_dir = Path.cwd()
+        #try:
+        os.chdir(TestReportingMeasureName.run_dir(test_name))
+
+        # run the measure
+        measure.run(runner, argument_map)
+        result = runner.result()
+        print(result)
+        assert result.value().valueName() == "Success"
+        assert len(result.warnings()) == 0
+        os.chdir(start_dir)
+        #except:
+        #    os.chdir(start_dir)
+
+        # make sure the report file exists
+        assert report_path.exists()
diff --git a/src/utilities/bcl/test/BCLMeasure_GTest.cpp b/src/utilities/bcl/test/BCLMeasure_GTest.cpp
index 27ef7d84953..433ed7408a5 100644
--- a/src/utilities/bcl/test/BCLMeasure_GTest.cpp
+++ b/src/utilities/bcl/test/BCLMeasure_GTest.cpp
@@ -803,6 +803,7 @@ TEST_F(BCLFixture, 4156_TweakXML) {
   boost::optional<BCLMeasure> measure = BCLMeasure::load(srcDir);
   ASSERT_TRUE(measure);
   EXPECT_EQ(numFiles + addedUntrackedFiles, measure->files().size());
+  EXPECT_EQ(MeasureLanguage(MeasureLanguage::Ruby), measure->measureLanguage());
 
   // Call CheckUpdateFiles(), it should detect the files that aren't allowed
   EXPECT_TRUE(measure->checkForUpdatesFiles());
@@ -952,3 +953,177 @@ TEST_F(BCLFixture, 4156_TestRecursive_OutdatedXML) {
   // Save, so we can inspect the measure.xml
   measure2->save();
 }
+
+TEST_F(BCLFixture, BCLMeasure_Ctor_PythonModelMeasure) {
+
+  openstudio::path testDir = openstudio::filesystem::system_complete(getApplicationBuildDirectory() / toPath("Testing"));
+  openstudio::path srcDir = testDir / toPath("TestPythonModelMeasure");
+  openstudio::path destDir = testDir / toPath("TestPythonModelMeasureClone");
+
+  for (const auto& dir : {srcDir, destDir}) {
+    if (exists(dir)) {
+      removeDirectory(dir);
+    }
+    ASSERT_FALSE(fs::exists(dir));
+  }
+
+  BCLMeasure{"My Python Measure",   "MyPythonMeasure",      srcDir, "Envelope.Fenestration", MeasureType::ModelMeasure, "Description",
+             "Modeler Description", MeasureLanguage::Python};
+  ASSERT_TRUE(exists(srcDir));
+
+  boost::optional<BCLMeasure> measure = BCLMeasure::load(srcDir);
+  ASSERT_TRUE(measure);
+  EXPECT_EQ(MeasureType(MeasureType::ModelMeasure), measure->measureType());
+  EXPECT_EQ(MeasureLanguage(MeasureLanguage::Python), measure->measureLanguage());
+  auto files = measure->files();
+  size_t numFiles = files.size();
+
+  // .
+  // ├── ./docs
+  // │   └── ./docs/.gitkeep
+  // ├── ./LICENSE.md
+  // ├── ./measure.py
+  // ├── ./measure.xml
+  // └── ./tests
+  //     ├── ./tests/test_my_python_measure.py
+  //     └── ./tests/example_model.osm
+  std::vector<fs::path> expectedInitialPaths = {
+    "docs/.gitkeep", "LICENSE.md", "measure.py", "tests/test_my_python_measure.py", "tests/example_model.osm",
+    // "measure.xml": it's not included in itself!
+  };
+
+  {
+
+    EXPECT_EQ(numFiles, expectedInitialPaths.size());
+    std::vector<fs::path> expectedInitialAbsolutePaths;
+    expectedInitialAbsolutePaths.reserve(expectedInitialPaths.size());
+    std::transform(expectedInitialPaths.cbegin(), expectedInitialPaths.cend(), std::back_inserter(expectedInitialAbsolutePaths),
+                   [&srcDir](const auto& p) { return srcDir / p; });
+    std::sort(expectedInitialAbsolutePaths.begin(), expectedInitialAbsolutePaths.end());
+
+    std::vector<fs::path> initialPaths;
+    initialPaths.reserve(files.size());
+    std::transform(files.cbegin(), files.cend(), std::back_inserter(initialPaths), [](const auto& fileRef) { return fileRef.path(); });
+    std::sort(initialPaths.begin(), initialPaths.end());
+
+    EXPECT_TRUE(std::equal(initialPaths.begin(), initialPaths.end(), expectedInitialAbsolutePaths.begin(), expectedInitialAbsolutePaths.end()));
+  }
+}
+
+TEST_F(BCLFixture, BCLMeasure_Ctor_PythonEnergyPlusMeasure) {
+
+  openstudio::path testDir = openstudio::filesystem::system_complete(getApplicationBuildDirectory() / toPath("Testing"));
+  openstudio::path srcDir = testDir / toPath("TestPythonEnergyPlusMeasure");
+  openstudio::path destDir = testDir / toPath("TestPythonEnergyPlusMeasureClone");
+
+  for (const auto& dir : {srcDir, destDir}) {
+    if (exists(dir)) {
+      removeDirectory(dir);
+    }
+    ASSERT_FALSE(fs::exists(dir));
+  }
+
+  BCLMeasure{"My Python Measure",   "MyPythonMeasure",      srcDir, "Envelope.Fenestration", MeasureType::EnergyPlusMeasure, "Description",
+             "Modeler Description", MeasureLanguage::Python};
+  ASSERT_TRUE(exists(srcDir));
+
+  boost::optional<BCLMeasure> measure = BCLMeasure::load(srcDir);
+  ASSERT_TRUE(measure);
+  EXPECT_EQ(MeasureType(MeasureType::EnergyPlusMeasure), measure->measureType());
+  EXPECT_EQ(MeasureLanguage(MeasureLanguage::Python), measure->measureLanguage());
+  auto files = measure->files();
+  size_t numFiles = files.size();
+
+  // .
+  // ├── ./docs
+  // │   └── ./docs/.gitkeep
+  // ├── ./LICENSE.md
+  // ├── ./measure.py
+  // ├── ./measure.xml
+  // └── ./tests
+  //     └── ./tests/test_my_python_measure.py
+  std::vector<fs::path> expectedInitialPaths = {
+    "docs/.gitkeep", "LICENSE.md", "measure.py", "tests/test_my_python_measure.py",
+    // "measure.xml": it's not included in itself!
+  };
+
+  {
+
+    EXPECT_EQ(numFiles, expectedInitialPaths.size());
+    std::vector<fs::path> expectedInitialAbsolutePaths;
+    expectedInitialAbsolutePaths.reserve(expectedInitialPaths.size());
+    std::transform(expectedInitialPaths.cbegin(), expectedInitialPaths.cend(), std::back_inserter(expectedInitialAbsolutePaths),
+                   [&srcDir](const auto& p) { return srcDir / p; });
+    std::sort(expectedInitialAbsolutePaths.begin(), expectedInitialAbsolutePaths.end());
+
+    std::vector<fs::path> initialPaths;
+    initialPaths.reserve(files.size());
+    std::transform(files.cbegin(), files.cend(), std::back_inserter(initialPaths), [](const auto& fileRef) { return fileRef.path(); });
+    std::sort(initialPaths.begin(), initialPaths.end());
+
+    EXPECT_TRUE(std::equal(initialPaths.begin(), initialPaths.end(), expectedInitialAbsolutePaths.begin(), expectedInitialAbsolutePaths.end()));
+  }
+}
+
+TEST_F(BCLFixture, BCLMeasure_Ctor_PythonReportingMeasure) {
+
+  openstudio::path testDir = openstudio::filesystem::system_complete(getApplicationBuildDirectory() / toPath("Testing"));
+  openstudio::path srcDir = testDir / toPath("TestPythonReportingMeasure");
+  openstudio::path destDir = testDir / toPath("TestPythonReportingMeasureClone");
+
+  for (const auto& dir : {srcDir, destDir}) {
+    if (exists(dir)) {
+      removeDirectory(dir);
+    }
+    ASSERT_FALSE(fs::exists(dir));
+  }
+
+  BCLMeasure{"My Python Measure",   "MyPythonMeasure",      srcDir, "Envelope.Fenestration", MeasureType::ReportingMeasure, "Description",
+             "Modeler Description", MeasureLanguage::Python};
+  ASSERT_TRUE(exists(srcDir));
+
+  boost::optional<BCLMeasure> measure = BCLMeasure::load(srcDir);
+  ASSERT_TRUE(measure);
+  EXPECT_EQ(MeasureType(MeasureType::ReportingMeasure), measure->measureType());
+  EXPECT_EQ(MeasureLanguage(MeasureLanguage::Python), measure->measureLanguage());
+  auto files = measure->files();
+  size_t numFiles = files.size();
+
+  // .
+  // ├── ./docs
+  // │   └── ./docs/.gitkeep
+  // ├── ./LICENSE.md
+  // ├── ./measure.py
+  // ├── ./measure.xml
+  // ├── resources
+  // └── ./tests
+  //     ├── ./tests/test_my_python_measure.py
+  //     ├── ./tests/example_model.osm
+  //     └── USA_CO_Golden-NREL.724666_TMY3.epw
+  std::vector<fs::path> expectedInitialPaths = {
+    "docs/.gitkeep",
+    "LICENSE.md",
+    "measure.py",
+    "tests/test_my_python_measure.py",
+    "tests/example_model.osm",
+    "tests/USA_CO_Golden-NREL.724666_TMY3.epw",
+    // "measure.xml": it's not included in itself!
+  };
+
+  {
+
+    EXPECT_EQ(numFiles, expectedInitialPaths.size());
+    std::vector<fs::path> expectedInitialAbsolutePaths;
+    expectedInitialAbsolutePaths.reserve(expectedInitialPaths.size());
+    std::transform(expectedInitialPaths.cbegin(), expectedInitialPaths.cend(), std::back_inserter(expectedInitialAbsolutePaths),
+                   [&srcDir](const auto& p) { return srcDir / p; });
+    std::sort(expectedInitialAbsolutePaths.begin(), expectedInitialAbsolutePaths.end());
+
+    std::vector<fs::path> initialPaths;
+    initialPaths.reserve(files.size());
+    std::transform(files.cbegin(), files.cend(), std::back_inserter(initialPaths), [](const auto& fileRef) { return fileRef.path(); });
+    std::sort(initialPaths.begin(), initialPaths.end());
+
+    EXPECT_TRUE(std::equal(initialPaths.begin(), initialPaths.end(), expectedInitialAbsolutePaths.begin(), expectedInitialAbsolutePaths.end()));
+  }
+}