Repository: NREL/OpenStudio (language: python)

Issue:
E+ 22.2: ZoneHVAC:PackagedTerminal AirConditionner/HeatPump requires a supply air fan operating mode schedule
Issue overview
--------------

In 22.2.0, there’s a new error in ZoneHVAC:PackagedTerminal:AC/HP if you don’t supply a Supply Air Fan Operating Mode Schedule when you have a Fan:ConstantVolume

```
   ** Severe  ** ZoneHVAC:PackagedTerminalAirConditioner = ZONE HVAC PACKAGED TERMINAL AIR CONDITIONER 4
   **   ~~~   ** Fan type must be Fan:OnOff or Fan:SystemModel when Supply Air Fan Operating Mode Schedule Name is blank
```

Except:
* if I pass a Always 0 schedule, there’s another error saying value must be > 0 for Fan:CV. 
* If I pass Always 1 schedule, my EUI changes quite dramatically. 
* If I use a Fan:OnOff with always 0, my EUI is closer but still different.

We need a way to handle this so we don't end up with too broken models (lots are failing on OS-resources right now) and ideally without changing EUI too drastically.

cf https://github.com/NREL/OpenStudio/commit/9abf9e42edbe75ab01a160f3ba61fa1dd25dea89#diff-6792272e2d22af45d561660c93bc735d74a30b2a15218ed60d1b05a91eeca176R310


## Current Behavior
<!--- Tell us what happens instead of the expected behavior -->

## Expected Behavior
<!--- Tell us what should happen -->

## Steps to Reproduce
<!-- Provide an unambiguous set of steps to reproduce this bug. -->
<!--- Provide a link to a model that has the bug (a minimum complete and verifiable example (MCVE) is preferred) and/or code to reproduce -->
1.
2.
3.
4.

## Possible Solution
<!--- Optional, but if you can, suggest a fix/reason for the bug, -->

## Details

### Environment

Some additional details about your environment for this issue (if relevant):
 - Platform (Operating system, version):
 - Version of OpenStudio (if using an intermediate build, include SHA):

### Context
<!--- How has this issue affected you? What are you trying to accomplish? -->

E+ 22.2: ZoneHVAC:PackagedTerminal AirConditionner/HeatPump requires a supply air fan operating mode schedule
Issue overview
--------------

In 22.2.0, there’s a new error in ZoneHVAC:PackagedTerminal:AC/HP if you don’t supply a Supply Air Fan Operating Mode Schedule when you have a Fan:ConstantVolume

```
   ** Severe  ** ZoneHVAC:PackagedTerminalAirConditioner = ZONE HVAC PACKAGED TERMINAL AIR CONDITIONER 4
   **   ~~~   ** Fan type must be Fan:OnOff or Fan:SystemModel when Supply Air Fan Operating Mode Schedule Name is blank
```

Except:
* if I pass a Always 0 schedule, there’s another error saying value must be > 0 for Fan:CV. 
* If I pass Always 1 schedule, my EUI changes quite dramatically. 
* If I use a Fan:OnOff with always 0, my EUI is closer but still different.

We need a way to handle this so we don't end up with too broken models (lots are failing on OS-resources right now) and ideally without changing EUI too drastically.

cf https://github.com/NREL/OpenStudio/commit/9abf9e42edbe75ab01a160f3ba61fa1dd25dea89#diff-6792272e2d22af45d561660c93bc735d74a30b2a15218ed60d1b05a91eeca176R310


## Current Behavior
<!--- Tell us what happens instead of the expected behavior -->

## Expected Behavior
<!--- Tell us what should happen -->

## Steps to Reproduce
<!-- Provide an unambiguous set of steps to reproduce this bug. -->
<!--- Provide a link to a model that has the bug (a minimum complete and verifiable example (MCVE) is preferred) and/or code to reproduce -->
1.
2.
3.
4.

## Possible Solution
<!--- Optional, but if you can, suggest a fix/reason for the bug, -->

## Details

### Environment

Some additional details about your environment for this issue (if relevant):
 - Platform (Operating system, version):
 - Version of OpenStudio (if using an intermediate build, include SHA):

### Context
<!--- How has this issue affected you? What are you trying to accomplish? -->

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

update gems for standard 0.2.17.rc1
Update ruby gems packages for standards 0.2.17.rc1 and tbd v3.0.2
resolves #4709 
resolves #4667




Discussion / hints:
The change is due to https://github.com/NREL/EnergyPlus/pull/9273

I took resources/EnergyPlus/Office_HVAC_With_Many_Types.idf and ran some combinations on both 22.1.0 and 22.2.0

<img width="759" alt="image" src="https://user-images.githubusercontent.com/5479063/192811493-23e2b83c-a513-47b8-bed2-8f599d5a8f70.png">

I also discussed it with some members of E+ dev team. Seems like a good idea would be to do

```
If a PTAC had a FanConstantVolume and the op schedule is blank:
    Replace PTAC Fan with FanSystemModel Discrete (=Model a Fan:OnOff) copying inputs where they map
    Set PTAC fan Op schedule to alwaysOff
```
Reg tests right now:
```
Success    250
Fail        67
Name: (22.2.0, 3.5.0-alpha, 515b977a2c), dtype: int64
```
The change is due to https://github.com/NREL/EnergyPlus/pull/9273

I took resources/EnergyPlus/Office_HVAC_With_Many_Types.idf and ran some combinations on both 22.1.0 and 22.2.0

<img width="759" alt="image" src="https://user-images.githubusercontent.com/5479063/192811493-23e2b83c-a513-47b8-bed2-8f599d5a8f70.png">

I also discussed it with some members of E+ dev team. Seems like a good idea would be to do

```
If a PTAC had a FanConstantVolume and the op schedule is blank:
    Replace PTAC Fan with FanSystemModel Discrete (=Model a Fan:OnOff) copying inputs where they map
    Set PTAC fan Op schedule to alwaysOff
```
Reg tests right now:
```
Success    250
Fail        67
Name: (22.2.0, 3.5.0-alpha, 515b977a2c), dtype: int64
```
CI Results for a948f15c574f1ddd472103a993d4d4cf99dcc408:

* Ubuntu 18.04:
* Ubuntu 20.04:
    * [OpenStudio-3.5.0-alpha+a948f15c57-Ubuntu-20.04.deb](http://openstudio-ci-builds.s3-website-us-west-2.amazonaws.com/PR-4666/OpenStudio-3.5.0-alpha%2Ba948f15c57-Ubuntu-20.04.deb)
    * [OpenStudio-3.5.0-alpha+a948f15c57-Ubuntu-20.04.tar.gz](http://openstudio-ci-builds.s3-website-us-west-2.amazonaws.com/PR-4666/OpenStudio-3.5.0-alpha%2Ba948f15c57-Ubuntu-20.04.tar.gz)
    * [perf_test/results.html](https://openstudio-ci-builds.s3-us-west-2.amazonaws.com/PR-4666/a948f15c574f1ddd472103a993d4d4cf99dcc408/perf_test/results.html)

* Darwin:
    * [OpenStudio-3.5.0-alpha+a948f15c57-Darwin-x86_64.dmg](http://openstudio-ci-builds.s3-website-us-west-2.amazonaws.com/PR-4666/OpenStudio-3.5.0-alpha+a948f15c57-Darwin-x86_64.dmg)
    * [OpenStudio-3.5.0-alpha+a948f15c57-Darwin-x86_64.tar.gz](http://openstudio-ci-builds.s3-website-us-west-2.amazonaws.com/PR-4666/OpenStudio-3.5.0-alpha+a948f15c57-Darwin-x86_64.tar.gz)

* Windows:
* Code Coverage:
@joseph-robertson I pushed a few changes. I put the CoilHeatingElectricMultiStageStageData and CoilHeatingGasMultiStageStageData back to ModelObject. I extended testing by quite a bit. I fixed the clone method for both. This can merge as far as I'm concerned. I also adjusted https://github.com/NREL/OpenStudio-resources/pull/176 (and added it to the the PR Author checklist  in your original post for clarity)


Fix patch:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index ae46365d02e..a7c7eed5d57 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -194,11 +194,14 @@ set(ENERGYPLUS_VERSION_MINOR 2)
 set(ENERGYPLUS_VERSION_PATCH 0)
 set(ENERGYPLUS_VERSION "${ENERGYPLUS_VERSION_MAJOR}.${ENERGYPLUS_VERSION_MINOR}.${ENERGYPLUS_VERSION_PATCH}")
 # Build SHA is not required to have a value, but if it does OpenStudio will require this build.
-set(ENERGYPLUS_BUILD_SHA "c249759bad")
+set(ENERGYPLUS_BUILD_SHA "aa78da9668")
 
 # ENERGYPLUS_RELEASE_NAME is used to locate the E+ download
 # from the github releases
-set(ENERGYPLUS_RELEASE_NAME "v22.2.0")
+set(ENERGYPLUS_RELEASE_NAME "v22.2.0-WithIHGFix")
+
+# TODO: Temporary change to a fork until a patched release is issued on NREL's
+set(ENERGYPLUS_REPO "jmarrec")
 
 # Radiance
 set(RADIANCE_VERSION "5.0.a.12")
@@ -262,11 +265,6 @@ endif()
 ###############################################################################
 # Build options
 
-if(UNIX AND NOT APPLE)
-  # the RPATH to be used when installing
-  set(CMAKE_INSTALL_RPATH "$ORIGIN/../lib")
-endif()
-
 # TODO: should this be only in OpenStudioApplication?
 option(BUILD_PAT "Build PAT" OFF)
 
@@ -457,6 +455,12 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/Products")
 
 set(LIBRARY_SEARCH_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}" "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/Release" "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/Debug")
 
+set(LIB_DESTINATION_DIR lib)
+if(WIN32)
+  # Windows does not have RPATH handling, so we just put everything next to the CLI in the bin/ folder
+  set(LIB_DESTINATION_DIR bin)
+endif()
+
 # TODO: move App macros to a local ProjectMacros.cmake
 # Include project specific macros
 include("${PROJECT_SOURCE_DIR}/ProjectMacros.cmake")
@@ -592,21 +596,21 @@ endif()
 if(UNIX)
   if(APPLE)
     if (ARCH MATCHES "arm64")
-      set(ENERGYPLUS_EXPECTED_HASH 48af06ee5b4b0393444e6e57f4adadd5)
+      set(ENERGYPLUS_EXPECTED_HASH 35ba21891e7059987391f1ab59958025)
       set(ENERGYPLUS_PLATFORM "Darwin-macOS12.1-arm64")
     else()
-      set(ENERGYPLUS_EXPECTED_HASH 92a76c5d60cf91536bdeacbe780d135a)
+      set(ENERGYPLUS_EXPECTED_HASH 481fafde9496735d809e810bd95b720f)
       set(ENERGYPLUS_PLATFORM "Darwin-macOS10.15-x86_64")
     endif()
   elseif(LSB_RELEASE_ID_SHORT MATCHES "CentOS")
     # TODO: There aren't any redhat releases anymore, see PR #3145 too
-    set(ENERGYPLUS_EXPECTED_HASH ebf23ac4f54a6c4f9ac0da561de4196e)
+    set(ENERGYPLUS_EXPECTED_HASH f70fb18cc613f129e5e22e790d191f8c)   # TODO: not updated yet
     set(ENERGYPLUS_PLATFORM "Centos7-x86_64")
   else()
     if(LSB_RELEASE_VERSION_SHORT MATCHES "20.04")
-      set(ENERGYPLUS_EXPECTED_HASH f8a45aa0a01523b5f904d5c2ae8302f8)
+      set(ENERGYPLUS_EXPECTED_HASH fd2b9e55a15227f872601e9e57ffe819)
     else() # 18.04
-      set(ENERGYPLUS_EXPECTED_HASH 58b8397f55c2a1ede7ed08c5892e62ed)
+      set(ENERGYPLUS_EXPECTED_HASH 05a97ef513fa3f2ac7678bd9eef026db)
     endif()
     set(ENERGYPLUS_PLATFORM "Linux${ENERGYPLUS_SYSTEM_VERSION}-x86_64")
   endif()
@@ -616,7 +620,7 @@ if(UNIX)
   endif()
   if(NOT EXISTS "${PROJECT_BINARY_DIR}/${ENERGYPLUS_PATH}.tar.gz" OR NOT "${ENERGYPLUS_HASH}" MATCHES "${ENERGYPLUS_EXPECTED_HASH}")
     message(STATUS "Downloading EnergyPlus ${ENERGYPLUS_VERSION} (${ENERGYPLUS_PLATFORM})")
-    message(STATUS "https://github.com/NREL/EnergyPlus/releases/download/${ENERGYPLUS_RELEASE_NAME}/${ENERGYPLUS_PATH}.tar.gz") # TODO: Temp: custom build on my fork
+    message(STATUS "https://github.com/${ENERGYPLUS_REPO}/EnergyPlus/releases/download/${ENERGYPLUS_RELEASE_NAME}/${ENERGYPLUS_PATH}.tar.gz")
 
     if(LSB_RELEASE_ID_SHORT MATCHES "CentOS")
       file(DOWNLOAD "http://openstudio-resources.s3.amazonaws.com/dependencies/${ENERGYPLUS_PATH}.tar.gz" "${PROJECT_BINARY_DIR}/${ENERGYPLUS_PATH}.tar.gz"
@@ -624,7 +628,7 @@ if(UNIX)
            SHOW_PROGRESS
            EXPECTED_MD5 ${ENERGYPLUS_EXPECTED_HASH})
     else()
-      file(DOWNLOAD "https://github.com/NREL/EnergyPlus/releases/download/${ENERGYPLUS_RELEASE_NAME}/${ENERGYPLUS_PATH}.tar.gz" "${PROJECT_BINARY_DIR}/${ENERGYPLUS_PATH}.tar.gz"
+      file(DOWNLOAD "https://github.com/${ENERGYPLUS_REPO}/EnergyPlus/releases/download/${ENERGYPLUS_RELEASE_NAME}/${ENERGYPLUS_PATH}.tar.gz" "${PROJECT_BINARY_DIR}/${ENERGYPLUS_PATH}.tar.gz"
            INACTIVITY_TIMEOUT 300 # 5-min timeout
            SHOW_PROGRESS
            EXPECTED_MD5 ${ENERGYPLUS_EXPECTED_HASH})
@@ -638,11 +642,11 @@ elseif(WIN32)
   if(CMAKE_SIZEOF_VOID_P EQUAL 8) # 64 bit
     set(ENERGYPLUS_PATH "EnergyPlus-${ENERGYPLUS_VERSION}-${ENERGYPLUS_BUILD_SHA}-Windows-x86_64")
     set(ENERGYPLUS_ARCH 64)
-    set(ENERGYPLUS_EXPECTED_HASH c6d296be45420d6e497f33727f044676)
+    set(ENERGYPLUS_EXPECTED_HASH 8611314c5056abedca59144627e79078)
   else()
     set(ENERGYPLUS_PATH "EnergyPlus-${ENERGYPLUS_VERSION}-${ENERGYPLUS_BUILD_SHA}-Windows-i386")
     set(ENERGYPLUS_ARCH 32)
-    set(ENERGYPLUS_EXPECTED_HASH 68108677fceb17bd58be1c9c7a68f418)
+    set(ENERGYPLUS_EXPECTED_HASH 76afec50c80fc4b53f2f4da572573688)
   endif()
   if(EXISTS "${PROJECT_BINARY_DIR}/${ENERGYPLUS_PATH}.zip")
     file(MD5 "${PROJECT_BINARY_DIR}/${ENERGYPLUS_PATH}.zip" ENERGYPLUS_HASH)
@@ -650,7 +654,7 @@ elseif(WIN32)
   if(NOT EXISTS "${PROJECT_BINARY_DIR}/${ENERGYPLUS_PATH}.zip" OR NOT "${ENERGYPLUS_HASH}" MATCHES "${ENERGYPLUS_EXPECTED_HASH}")
     message(STATUS "Downloading EnergyPlus ${ENERGYPLUS_VERSION} (${ENERGYPLUS_ARCH}-bit)")
 
-    file(DOWNLOAD "https://github.com/NREL/EnergyPlus/releases/download/${ENERGYPLUS_RELEASE_NAME}/${ENERGYPLUS_PATH}.zip" "${PROJECT_BINARY_DIR}/${ENERGYPLUS_PATH}.zip"
+    file(DOWNLOAD "https://github.com/${ENERGYPLUS_REPO}/EnergyPlus/releases/download/${ENERGYPLUS_RELEASE_NAME}/${ENERGYPLUS_PATH}.zip" "${PROJECT_BINARY_DIR}/${ENERGYPLUS_PATH}.zip"
          INACTIVITY_TIMEOUT 300 # 5 minute timeout
          SHOW_PROGRESS
          EXPECTED_MD5 ${ENERGYPLUS_EXPECTED_HASH})
@@ -796,20 +800,20 @@ if(BUILD_CLI)
          set(OPENSTUDIO_GEMS_ZIP_FILENAME "openstudio3-gems-20220420-darwin_arm64.tar.gz")
          set(OPENSTUDIO_GEMS_ZIP_EXPECTED_MD5 "abc2798729434f8835f824300a5fd420")
        else()
-         set(OPENSTUDIO_GEMS_ZIP_FILENAME "openstudio3-gems-20220901-darwin.tar.gz ")
-         set(OPENSTUDIO_GEMS_ZIP_EXPECTED_MD5 "1c4a738e36bd34e7626aef7116bf5076")
+         set(OPENSTUDIO_GEMS_ZIP_FILENAME "openstudio3-gems-20221007-darwin.tar.gz")
+         set(OPENSTUDIO_GEMS_ZIP_EXPECTED_MD5 "f1f9cb50a4ef36899fd22251a3efb80d")
        endif()
      else()
-       set(OPENSTUDIO_GEMS_ZIP_FILENAME "openstudio3-gems-20220831-linux.tar.gz")
-       set(OPENSTUDIO_GEMS_ZIP_EXPECTED_MD5 "b515d50b81fae65389b44f73456d4e65")
+       set(OPENSTUDIO_GEMS_ZIP_FILENAME "openstudio3-gems-20221007-linux.tar.gz")
+       set(OPENSTUDIO_GEMS_ZIP_EXPECTED_MD5 "2d53f66f68f17db32ffde75b5d6f97db")
        if (USE_OPENSTUDIO_GEMS_PR)
          set(OPENSTUDIO_GEMS_BASEURL "${OPENSTUDIO_GEMS_BASEURL}/openstudio-gems-linux/${OPENSTUDIO_GEMS_PR_NUMBER}")
        endif()
      endif()
   elseif(WIN32)
     # OpenStudio gems are only supported on 64 bit windows
-    set(OPENSTUDIO_GEMS_ZIP_FILENAME "openstudio3-gems-20220831-windows.tar.gz")
-    set(OPENSTUDIO_GEMS_ZIP_EXPECTED_MD5 "c45f2a323d654a0a1536c6076bf4dcc4")
+    set(OPENSTUDIO_GEMS_ZIP_FILENAME "openstudio3-gems-20221007-windows.tar.gz")
+    set(OPENSTUDIO_GEMS_ZIP_EXPECTED_MD5 "2c55f2dbf78453e30ea365d5a9a36a98")
     if (USE_OPENSTUDIO_GEMS_PR)
       set(OPENSTUDIO_GEMS_BASEURL "${OPENSTUDIO_GEMS_BASEURL}/openstudio-gems-windows/${OPENSTUDIO_GEMS_PR_NUMBER}")
     endif()
@@ -945,13 +949,6 @@ include_directories("${PROJECT_BINARY_DIR}/src/")
 
 ###############################################################################
 
-###############################################################################
-# RPATH settings
-# Conan set this to true, which is not the cmake default and not what we want
-# for the openstudio targets
-set(CMAKE_SKIP_RPATH FALSE)
-###############################################################################
-
 ###############################################################################
 # Add project sub directories
 
diff --git a/ConanInstall.cmake b/ConanInstall.cmake
index e3057a485d2..f6883c0df23 100644
--- a/ConanInstall.cmake
+++ b/ConanInstall.cmake
@@ -211,7 +211,7 @@ if(NOT CONAN_OPENSTUDIO_ALREADY_RUN)
   # Loads the conanbuildinfo.cmake / conanbuildinfo_multi.cmake
   conan_load_buildinfo()
   # conan_basic_setup in the conanbuildinfo.cmake. TARGETS => set cmake targets, NO_OUTPUT_DIRS => Don't modify the BIN / LIB folders etc
-  conan_basic_setup(TARGETS NO_OUTPUT_DIRS)
+  conan_basic_setup(TARGETS NO_OUTPUT_DIRS KEEP_RPATHS)
 
   set(CONAN_OPENSTUDIO_ALREADY_RUN TRUE)
 
diff --git a/ProjectMacros.cmake b/ProjectMacros.cmake
index a75475bb708..6712fff89b3 100644
--- a/ProjectMacros.cmake
+++ b/ProjectMacros.cmake
@@ -394,17 +394,34 @@ macro(MAKE_SWIG_TARGET NAME SIMPLENAME KEY_I_FILE I_FILES PARENT_TARGET PARENT_S
     )
     set_source_files_properties(${COPY_PYTHON_GENERATED_SRC} PROPERTIES GENERATED TRUE)
 
-    install(FILES "${PYTHON_GENERATED_SRC}" DESTINATION Python COMPONENT "Python")
-    install(TARGETS ${swig_target} DESTINATION Python COMPONENT "Python")
+    # In build dir it's inside Products/python and we want to find Products/libopenstudiolib
+    # It installs in Python/ and we want to find lib/libopenstudiolib
+    if(APPLE)
+      set_target_properties(${swig_target}
+        PROPERTIES
+          SUFFIX ".so"
+          BUILD_RPATH "@loader_path;@loader_path/../"
+          INSTALL_RPATH "@loader_path;@loader_path/../lib/"
+      )
+    elseif(UNIX)
+      set_target_properties(${swig_target}
+        PROPERTIES
+          BUILD_RPATH "$ORIGIN:$ORIGIN/../"
+          INSTALL_RPATH "$ORIGIN:$ORIGIN/../lib/"
+      )
+    endif()
 
     # TODO: really unsure what former PYTHON_Libraries was doing and I really doubt linking to the python libs is something we want...
     # We're not trying to make a CLI here
     target_link_libraries(${swig_target} PUBLIC ${${PARENT_TARGET}_depends})
     if (MSVC)
-      target_link_libraries(${swig_target} PRIVATE Python::Module)
+      target_link_libraries(${swig_target} PRIVATE Python::Module) # TODO: we're going to have to deal with this now, we'll need to copy python3.8.dll THRICE (E+, bin/ and now Python/)
     endif()
     add_dependencies(${swig_target} ${PARENT_TARGET})
 
+    install(FILES "${PYTHON_GENERATED_SRC}" DESTINATION Python COMPONENT "Python")
+    install(TARGETS ${swig_target} DESTINATION Python COMPONENT "Python")
+
     # add this target to a "global" variable so python tests can require these
     list(APPEND ALL_PYTHON_BINDING_TARGETS "${swig_target}")
     set(ALL_PYTHON_BINDING_TARGETS "${ALL_PYTHON_BINDING_TARGETS}" PARENT_SCOPE)
@@ -460,8 +477,7 @@ macro(MAKE_SWIG_TARGET NAME SIMPLENAME KEY_I_FILE I_FILES PARENT_TARGET PARENT_S
         set_target_properties(${swig_target} PROPERTIES COMPILE_FLAGS "/bigobj /wd4996 /wd4005") ## /wd4996 suppresses deprecated warning, /wd4005 suppresses macro redefinition warning
         set_target_properties(${swig_target} PROPERTIES SUFFIX ".pyd")
       elseif(UNIX)
-        # TODO: Probably something to be done here...
-        # note: macOS is APPLE and also UNIX !
+        # I'm copying libopenstudiolib inside the python_package folder already. We can use the same RPATH for build tree and install
         if(APPLE)
           set_target_properties(${swig_target}
            PROPERTIES
@@ -477,7 +493,7 @@ macro(MAKE_SWIG_TARGET NAME SIMPLENAME KEY_I_FILE I_FILES PARENT_TARGET PARENT_S
 
              # the RPATH to be used when installing
              INSTALL_RPATH $ORIGIN
-         )
+          )
         endif()
         # set_target_properties(${swig_target} PROPERTIES LINK_FLAGS "-Wl,-rpath,./")
         if(APPLE AND NOT CMAKE_COMPILER_IS_GNUCXX)
diff --git a/developer/doc/ReleaseNotes/OpenStudio_Release_Notes_3_4_1_TBD.md b/developer/doc/ReleaseNotes/OpenStudio_Release_Notes_3_5_0_TBD.md
similarity index 81%
rename from developer/doc/ReleaseNotes/OpenStudio_Release_Notes_3_4_1_TBD.md
rename to developer/doc/ReleaseNotes/OpenStudio_Release_Notes_3_5_0_TBD.md
index 80fa128c3a2..48b468d9b79 100644
--- a/developer/doc/ReleaseNotes/OpenStudio_Release_Notes_3_4_1_TBD.md
+++ b/developer/doc/ReleaseNotes/OpenStudio_Release_Notes_3_5_0_TBD.md
@@ -1,8 +1,8 @@
-# OpenStudio Version 3.4.1 (TDB)
+# OpenStudio Version 3.5.0 (TDB)
 
 _Release Notes_ -  _TDB_
 
-These release notes describe version 3.4.1 of the OpenStudio SDK developed by the National Renewable Energy Laboratory (NREL), Buildings and Thermal Sciences Center, Commercial Buildings Research Group, Tools Development Section, and associated collaborators. The notes are organized into the following sections:
+These release notes describe version 3.5.0 of the OpenStudio SDK developed by the National Renewable Energy Laboratory (NREL), Buildings and Thermal Sciences Center, Commercial Buildings Research Group, Tools Development Section, and associated collaborators. The notes are organized into the following sections:
 
 -  Overview
 -  Where to Find OpenStudio Documentation
@@ -15,7 +15,7 @@ As of April 2020, development and distribution of the OpenStudioApplication and
 
 Below is the list of components that is included in this SDK installer:
 
-__**OpenStudio SDK 3.4.1**__
+__**OpenStudio SDK 3.5.0**__
 - EnergyPlus
 - Command Line Interface (CLI)
 - Radiance
@@ -33,11 +33,11 @@ __**OpenStudio SDK 3.4.1**__
 
 # Installation Notes
 
-OpenStudio SDK 3.4.1 is supported on 64-bit Windows 7 – 10, OS X 10.15, and Ubuntu 18.04, 20.04
+OpenStudio SDK 3.5.0 is supported on 64-bit Windows 7 – 10, OS X 10.15, and Ubuntu 18.04, 20.04
 
-OpenStudio SDK 3.4.1 supports [EnergyPlus Release 22.1.0](https://github.com/NREL/EnergyPlus/releases/tag/v22.1.0), which is bundled with the OpenStudio installer. It is no longer necessary to download and install EnergyPlus separately. Other builds of EnergyPlus are not supported by OpenStudio SDK 3.4.1.
+OpenStudio SDK 3.5.0 supports [EnergyPlus Release 22.1.0](https://github.com/NREL/EnergyPlus/releases/tag/v22.1.0), which is bundled with the OpenStudio installer. It is no longer necessary to download and install EnergyPlus separately. Other builds of EnergyPlus are not supported by OpenStudio SDK 3.5.0.
 
-OpenStudio SDK 3.4.1 supports Radiance 5.0.a.12, which is bundled with the OpenStudio installer; users no longer must install Radiance separately, and OpenStudio will use the included Radiance version regardless of any other versions that may be installed on the system. Other builds of Radiance are not supported by OpenStudio SDK 3.4.1.
+OpenStudio SDK 3.5.0 supports Radiance 5.0.a.12, which is bundled with the OpenStudio installer; users no longer must install Radiance separately, and OpenStudio will use the included Radiance version regardless of any other versions that may be installed on the system. Other builds of Radiance are not supported by OpenStudio SDK 3.5.0.
 
 As usual, you can refer to the **[OpenStudio SDK Compatibility Matrix](https://github.com/NREL/OpenStudio/wiki/OpenStudio-SDK-Version-Compatibility-Matrix)** for more information.
 
@@ -52,7 +52,7 @@ For help with common installation problems please visit, http://nrel.github.io/O
 
 # OpenStudio SDK: Changelog
 
-The 3.4.1 is a minor release. This update includes several new features, performance improvements, and bug fixes.
+The 3.5.0 is a minor release. This update includes several new features, performance improvements, and bug fixes.
 You can find the list of Pull Requests that got into this release (**NEEDS UPDATE**) [here](https://github.com/NREL/OpenStudio/pulls?utf8=%E2%9C%93&q=is%3Apr+is%3Aclosed+created%3A2021-04-27..2022-09-26+).
 
 
@@ -60,7 +60,7 @@ You can find the list of Pull Requests that got into this release (**NEEDS UPDAT
 
 As of OpenStudio SDK 3.2.0, Python bindings are officially supported and distributed through Python Package Index (PyPI). To install, users will need to have Python3 installed along with pip and simply run the following command in a terminal window.
 
-`pip install openstudio==3.4.1`
+`pip install openstudio==3.5.0`
 
 Please see [openstudio on PyPi](https://pypi.org/project/openstudio/) for further instructions on how to install. Users can also visit the test channel at [openstudio on TestPyPi](https://test.pypi.org/project/openstudio/) to install development bindings.
 
@@ -83,7 +83,12 @@ You can also refer to the [OpenStudio SDK Python Binding Version Compatibility M
     * `Coil:Cooling:DX:SingleSpeed`, `Coil:Cooling:DX:MultiSpeed:StageData`, and `Coil:Cooling:DX:CurveFit:Speed`: `ratedEvaporatorFanPowerPerVolumeFlowRate` and `setRatedEvaporatorFanPowerPerVolumeFlowRate` in favor of `ratedEvaporatorFanPowerPerVolumeFlowRate2017` and `setRatedEvaporatorFanPowerPerVolumeFlowRate2017`
     * `Coil:Heating:DX:SingleSpeed` and `Coil:Heating:DX:MultiSpeed:StageData`: `ratedSupplyFanPowerPerVolumeFlowRate` and `setRatedSupplyFanPowerPerVolumeFlowRate` in favor of `ratedSupplyFanPowerPerVolumeFlowRate2017` and `setRatedSupplyFanPowerPerVolumeFlowRate2017`
 * [#4666](https://github.com/NREL/OpenStudio/pull/4666) - Changes related to availability schedule methods
-    * `Coil:Heating:Gas:MultiStage` has an API-breaking change related to its `availabilitySchedule` getter. It is now a required field that returns `Schedule` instead of `boost::optional<Schedule>`. Method `resetAvailabilitySchedule` is also removed.     
+    * `Coil:Heating:Gas:MultiStage` has an API-breaking change related to its `availabilitySchedule` getter. It is now a required field that returns `Schedule` instead of `boost::optional<Schedule>`. Method `resetAvailabilitySchedule` is also removed.
+* [#4701](https://github.com/NREL/OpenStudio/pull/4701) - `ZoneHVACPackagedTerminalAirConditioner` and `ZoneHVACPackagedTerminalHeatPump`
+    * `ZoneHVACPackagedTerminalAirConditioner` and `ZoneHVACPackagedTerminalHeatPump` have an API-breaking change related to its `supplyAirFanOperatingModeSchedule` getter. It is now a required field that returns `Schedule` instead of `boost::optional<Schedule>`. Method `resetSupplyAirFanOperatingModeSchedule` is also removed.
+        * It is set to `alwaysOnDiscreteSchedule` (=Constant) in the Constructor if you provide a `FanConstantVolume` (This is **required** by E+)
+        * It is set to `alwaysOffDiscreteSchedule` (=Cycling) in the Constructor if you provide any other fan types (E+ treats a blank schedule as always off)
+    * There are unusual `VersionTranslator` Rules for Packaged Systems (PTAC or PTHP) that use a `FanConstantVolume` and that do not have a `Supply Air Fan Operating Mode Schedule`. In 22.1.0 this would effectively, and mistakenly, function as a cycling fan, but this is now disallowed in E+ 22.2.0. In order to retain a similar functionality and energy usage, the `FanConstantVolume` will be replaced by a `FanSystemModel` with an Always Off Schedule (=cycling fan, similar to a `Fan:OnOff`), mapping inputs such as pressure rise and efficiency appropriately.
 
 
 ## Minor changes and bug fixes
diff --git a/python/SetupPython.cmake b/python/SetupPython.cmake
index 8fb4af23926..b46852047e2 100644
--- a/python/SetupPython.cmake
+++ b/python/SetupPython.cmake
@@ -1,5 +1,9 @@
 option(BUILD_PYTHON_PIP_PACKAGE "Prepare a folder to build a pip module" OFF)
 
+# TODO: Prefer static libraries (on Unix, no effect on Windows)
+# Can't make it work on Mac right now (untested on Ubuntu), I get issues building the swig bindings
+set(Python_USE_STATIC_LIBS 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)
@@ -10,6 +14,40 @@ endif()
 
 get_filename_component(Python_PROGRAM_NAME ${Python_EXECUTABLE} NAME)
 
+get_filename_component(RESOLVED_PYTHON_LIBRARY "${Python_LIBRARIES}" REALPATH)
+message("RESOLVED_PYTHON_LIBRARY=${RESOLVED_PYTHON_LIBRARY}")
+if(WIN32)
+
+  # In case you have both release and debug Python libraries on your system, Python_LIBRARIES might be "optimized;C:/.../python38.lib;debug;C:/.../python38_d.lib"
+  # so it can't be used directly, we could use a generator expression to find the $<TARGET_FILE:Python::Python> library used and that'd point directly
+  # to the DLL. But it won't work nicely for the install(code ...) command below... so we do a hack, get the optimized one always...
+  list(LENGTH Python_LIBRARIES _LEN)
+  if (_LEN GREATER 1)
+    set (_Python_DOING_RELEASE FALSE)
+    foreach(_currentArg ${Python_LIBRARIES})
+      if ("x${_currentArg}" STREQUAL "xoptimized")
+        set(_Python_DOING_RELEASE TRUE)
+      elseif(_Python_DOING_RELEASE)
+        get_filename_component(RESOLVED_PYTHON_LIBRARY "${_currentArg}" REALPATH)
+        break()
+      endif()
+    endforeach()
+  # else()
+    # No-op, already done above
+  endif()
+
+
+else()
+  get_target_property(PYTHON_LIB_TYPE Python::Python TYPE)
+  # Does NOT follow symlinks. Works fine on windows and mac, but on ubuntu it ships a libpython3.8.so which is just a symlink
+  #install(IMPORTED_RUNTIME_ARTIFACTS Python::Python LIBRARY DESTINATION lib)
+  #install(FILES ${RESOLVED_PYTHON_LIBRARY} DESTINATION lib)
+
+  # if(NOT PYTHON_LIB_TYPE STREQUAL STATIC_LIBRARY)
+  #   message(FATAL_ERROR "We expect to find a static libpython on Unix, but ${PYTHON_LIB_TYPE} found")
+  # endif()
+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
diff --git a/python/engine/CMakeLists.txt b/python/engine/CMakeLists.txt
index 1ba93d136fd..27d5cd211d0 100644
--- a/python/engine/CMakeLists.txt
+++ b/python/engine/CMakeLists.txt
@@ -44,4 +44,27 @@ target_compile_options(pythonengine PRIVATE
 
 target_compile_definitions(pythonengine PRIVATE openstudio_scriptengine_EXPORTS SHARED_OS_LIBS)
 
-install(TARGETS pythonengine EXPORT openstudio DESTINATION bin COMPONENT "CLI")
+install(TARGETS pythonengine EXPORT openstudio DESTINATION ${LIB_DESTINATION_DIR} COMPONENT "CLI")
+
+# it goes into lib/ and we want to find:
+# ../EnergyPlus/libpython3.8.dylib
+if(APPLE)
+  set_target_properties(pythonengine PROPERTIES
+    INSTALL_RPATH "@loader_path;@loader_path/../EnergyPlus/"
+  )
+
+  install(CODE [[
+    execute_process(COMMAND "install_name_tool" -change "$<TARGET_FILE:Python::Python>" "@rpath/$<TARGET_FILE_NAME:Python::Python>" "${CMAKE_INSTALL_PREFIX}/lib/$<TARGET_FILE_NAME:pythonengine>")
+  ]]
+    COMPONENT "CLI"
+  )
+
+elseif(UNIX)
+  set_target_properties(pythonengine PROPERTIES
+    INSTALL_RPATH "$ORIGIN:$ORIGIN/../EnergyPlus/"
+  )
+elseif(WIN32)
+  # Windows does not have RPATH handling, so just copy the DLL to the bin folder
+  install(IMPORTED_RUNTIME_ARTIFACTS Python::Python LIBRARY DESTINATION ${LIB_DESTINATION_DIR} COMPONENT "CLI")
+endif()
+
diff --git a/python/engine/PythonEngine.cpp b/python/engine/PythonEngine.cpp
index e7ead6941ed..d1c7f1ced7d 100644
--- a/python/engine/PythonEngine.cpp
+++ b/python/engine/PythonEngine.cpp
@@ -1,5 +1,6 @@
 #include "PythonEngine.hpp"
 #include <utilities/core/ApplicationPathHelpers.hpp>
+#include "../../src/utilities/core/Filesystem.hpp"
 #include <fmt/format.h>
 #include <stdexcept>
 #include <string>
@@ -20,18 +21,42 @@
 
 namespace openstudio {
 
-void PythonEngine::pyimport(const std::string& importName, const std::string& includePath) {
+void addToPythonPath(const openstudio::path& 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());
+    PyObject* unicodeIncludePath = PyUnicode_FromString(includePath.string().c_str());
     PyList_Insert(sysPath, 0, unicodeIncludePath);
   }
+}
+
+void PythonEngine::pyimport(const std::string& importName, const std::string& includePath) {
+  addToPythonPath(includePath);
   PyImport_ImportModule(importName.c_str());
 }
 
+void PythonEngine::setupPythonPath(const std::vector<openstudio::path>& includeDirs, const openstudio::path& pythonHomeDir) {
+  for (const auto& includeDir : includeDirs) {
+    addToPythonPath(includeDir);
+  }
+  if (!pythonHomeDir.empty()) {
+    wchar_t* a = Py_DecodeLocale(pythonHomeDir.generic_string().c_str(), nullptr);
+    Py_SetPythonHome(a);
+  }
+}
+
 PythonEngine::PythonEngine(int argc, char* argv[]) : ScriptEngine(argc, argv), program(Py_DecodeLocale(pythonProgramName, nullptr)) {
+  // this frozen flag tells Python that the package and library have been frozen for embedding, so it shouldn't warn about missing prefixes
+  Py_FrozenFlag = 1;
+
+  // Set the PYTHONPATH / PYTHONHOME to the E+ shipped standard library
+  // I think we need to set the python path before initializing the library
+  auto pathToPythonPackages = getEnergyPlusDirectory() / "python_standard_lib";
+  wchar_t* a = Py_DecodeLocale(pathToPythonPackages.make_preferred().string().c_str(), nullptr);
+  Py_SetPath(a);
+  Py_SetPythonHome(a);
+
   Py_SetProgramName(program);  // optional but recommended
 
   Py_Initialize();
diff --git a/python/engine/PythonEngine.hpp b/python/engine/PythonEngine.hpp
index 9bd5754b8a5..d98cdc73fc8 100644
--- a/python/engine/PythonEngine.hpp
+++ b/python/engine/PythonEngine.hpp
@@ -25,6 +25,8 @@ class PythonEngine final : public ScriptEngine
   ScriptObject eval(std::string_view sv) override;
   void exec(std::string_view sv) override;
 
+  virtual void setupPythonPath(const std::vector<openstudio::path>& includeDirs, const openstudio::path& pythonHomeDir) override;
+
  protected:
   void* getAs_impl(ScriptObject& obj, const std::type_info&) override;
   void importOpenStudio();
diff --git a/resources/model/OpenStudio.idd b/resources/model/OpenStudio.idd
index 0f3b1db0657..c1a9ace2d65 100644
--- a/resources/model/OpenStudio.idd
+++ b/resources/model/OpenStudio.idd
@@ -27545,6 +27545,7 @@ OS:ZoneHVAC:PackagedTerminalHeatPump,
        \note cycling fan operation (fan cycles with cooling or heating coil). Schedule Name values greater
        \note than 0 denote constant fan operation (fan runs continually regardless of coil operation).
        \note The fan operating mode defaults to cycling fan operation if this field is left blank.
+       \required-field
        \type object-list
        \object-list ScheduleNames
 
@@ -27659,6 +27660,7 @@ OS:ZoneHVAC:PackagedTerminalAirConditioner,
        \note Enter the name of a schedule that controls fan operation. Schedule Name values of 0 denote
        \note cycling fan operation (fan cycles with cooling or heating coil). Schedule Name values greater
        \note than 0 denote constant fan operation (fan runs continually regardless of coil operation).
+       \required-field
        \type object-list
        \object-list ScheduleNames
 
diff --git a/ruby/bindings/InitRubyBindings.cpp b/ruby/bindings/InitRubyBindings.cpp
index e48ecf0a669..4ae222c27b6 100644
--- a/ruby/bindings/InitRubyBindings.cpp
+++ b/ruby/bindings/InitRubyBindings.cpp
@@ -29,9 +29,30 @@
 
 #include "InitRubyBindings.hpp"
 #include "RubyEval.hpp"
+
+// #define HAVE_ISFINITE 1
 #include <ruby.h>
-#include <stdexcept>
+// #undef int128_t
+// #undef uint128_t
+// #undef isfinite
+// #undef memcpy
+#include "../../src/utilities/core/Filesystem.hpp"
+
+// You can't use fmt when you include ruby, awesome!
+// #include <fmt/format.h>
+// <conan_fmt>/include/fmt/core.h:405:7: error: expected unqualified-id
+// using int128_t = __int128_t;
+//       ^
+// <conan_openstudio_ruby>include/ruby-2.7.0/arm64-darwin21/ruby/config.h:202:18: note: expanded from macro 'int128_t'
+// #define int128_t __int128
+
+// #include <ranges>
+#include <boost/range/adaptor/reversed.hpp>
 #include <iostream>
+#include <string>
+#include <sstream>
+#include <string_view>
+#include <vector>
 
 extern "C"
 {
@@ -201,7 +222,7 @@ void initExtendedRubyBindings() {
 
   // "typedefs" for backwards compatibility
   // keep synchronized with \openstudiocore\src\utilities\core\RubyInterpreter.hpp
-  std::string ruby_typedef_script = R"END(
+  std::string ruby_typedef_script = R"ruby(
 module OpenStudio
 module Ruleset
 
@@ -368,11 +389,522 @@ module OpenStudio
   end
 end # module OpenStudio
 
-)END";
+)ruby";
 
   openstudio::evalString(ruby_typedef_script);
 }
 
+void initEmbeddedGems() {
+
+  std::string initScript = R"ruby(
+# This is the save puts to use to catch EPIPE. Uses `puts` on the given IO object and safely ignores any Errno::EPIPE
+#
+# @param [String] message Message to output
+# @param [Hash] opts Options hash
+#
+def safe_puts(message=nil, opts=nil)
+  message ||= ''
+  opts = {
+      io: $stdout,
+      printer: :puts
+  }.merge(opts || {})
+
+  begin
+    opts[:io].send(opts[:printer], message)
+  rescue Errno::EPIPE
+    # This is what makes this a `safe` puts
+    return
+  end
+end
+
+require 'logger'
+require 'rbconfig'
+
+$logger = Logger.new(STDOUT)
+#$logger.level = Logger::ERROR
+$logger.level = Logger::WARN
+
+# debug Gem::Resolver, must go before resolver is required
+#ENV['DEBUG_RESOLVER'] = "1"
+original_arch = nil
+if RbConfig::CONFIG['arch'] =~ /x64-mswin64/
+  # assume that system ruby of 'x64-mingw32' architecture was used to create bundle
+  original_arch = RbConfig::CONFIG['arch']
+  RbConfig::CONFIG['arch'] = 'x64-mingw32'
+end
+
+# load embedded ruby gems
+require 'rubygems'
+require 'rubygems/version'
+Gem::Platform.local
+
+if original_arch
+  RbConfig::CONFIG['arch'] = original_arch
+end
+
+module Gem
+class Specification < BasicSpecification
+
+  # This isn't ideal but there really is no available method to add specs for our use case.
+  # Using self.dirs=() works for ruby official gems but since it appends the dir paths with 'specifications' it breaks for bundled gem specs
+  def self.add_spec spec
+    warn "Gem::Specification.add_spec is deprecated and will be removed in RubyGems 3.0" unless Gem::Deprecate.skip
+    # TODO: find all extraneous adds
+    # puts
+    # p :add_spec => [spec.full_name, caller.reject { |s| s =~ /minitest/ }]
+
+    # TODO: flush the rest of the crap from the tests
+    # raise "no dupes #{spec.full_name} in #{all_names.inspect}" if
+    #   _all.include? spec
+
+    raise "nil spec!" unless spec # TODO: remove once we're happy with tests
+
+    return if _all.include? spec
+
+    _all << spec
+    stubs << spec
+    (@@stubs_by_name[spec.name] ||= []) << spec
+    sort_by!(@@stubs_by_name[spec.name]) { |s| s.version }
+    _resort!(_all)
+    _resort!(stubs)
+  end
+
+  def gem_dir
+    embedded = false
+    tmp_loaded_from = loaded_from.clone
+    if tmp_loaded_from.chars.first == ':'
+      tmp_loaded_from[0] = ''
+      embedded = true
+    end
+
+    joined = File.join(gems_dir, full_name)
+    if embedded
+      test = /bundler\/gems/.match(tmp_loaded_from)
+      if test
+        @gem_dir = ':' + (File.dirname tmp_loaded_from)
+      else
+        @gem_dir = joined
+      end
+    else
+      @gem_dir = File.expand_path joined
+    end
+  end
+
+  def full_gem_path
+    # TODO: This is a heavily used method by gems, so we'll need
+    # to aleast just alias it to #gem_dir rather than remove it.
+    embedded = false
+    tmp_loaded_from = loaded_from.clone
+    if tmp_loaded_from.chars.first == ':'
+      tmp_loaded_from[0] = ''
+      embedded = true
+    end
+
+    joined = File.join(gems_dir, full_name)
+    if embedded
+      test = /bundler\/gems/.match(tmp_loaded_from)
+      if test
+        @full_gem_path = ':' + (File.dirname tmp_loaded_from)
+        @full_gem_path.untaint
+        return @full_gem_path
+      else
+        @full_gem_path = joined
+        @full_gem_path.untaint
+        return @full_gem_path
+      end
+    else
+      @full_gem_path = File.expand_path joined
+      @full_gem_path.untaint
+    end
+    return @full_gem_path if File.directory? @full_gem_path
+
+    @full_gem_path = File.expand_path File.join(gems_dir, original_name)
+  end
+
+  def gems_dir
+    # TODO: this logic seems terribly broken, but tests fail if just base_dir
+    @gems_dir = File.join(loaded_from && base_dir || Gem.dir, "gems")
+  end
+
+  def base_dir
+    return Gem.dir unless loaded_from
+
+    embedded = false
+    tmp_loaded_from = loaded_from.clone
+    if tmp_loaded_from.chars.first == ':'
+      tmp_loaded_from[0] = ''
+      embedded = true
+    end
+
+    test = /bundler\/gems/.match(tmp_loaded_from)
+    result = if (default_gem? || test) then
+        File.dirname File.dirname File.dirname tmp_loaded_from
+      else
+        File.dirname File.dirname tmp_loaded_from
+      end
+
+    if embedded
+      result = ':' + result
+    end
+    @base_dir = result
+  end
+
+end
+end
+
+# have to do some forward declaration and pre-require to get around autoload cycles
+#module Bundler
+#end
+
+# This is the code chunk to allow for an embedded IRB shell. From Jason Roelofs, found on StackOverflow
+module IRB # :nodoc:
+  def self.start_session(binding)
+    unless @__initialized
+      args = ARGV
+      ARGV.replace(ARGV.dup)
+      IRB.setup(nil)
+      ARGV.replace(args)
+      @__initialized = true
+    end
+
+    workspace = WorkSpace.new(binding)
+
+    irb = Irb.new(workspace)
+
+    @CONF[:IRB_RC].call(irb.context) if @CONF[:IRB_RC]
+    @CONF[:MAIN_CONTEXT] = irb.context
+
+    catch(:IRB_EXIT) do
+      irb.eval_input
+    end
+  end
+end
+  )ruby";
+
+  openstudio::evalString(initScript);
+}
+
+void setupEmbeddedGemsClearEnvVars() {
+
+  // in hash.c there's lot of env-related functions, but all private, aside from rb_env_clear();
+  const std::string clearEnvs = R"ruby(
+ENV.delete('GEM_HOME') if ENV['GEM_HOME']
+ENV.delete('GEM_PATH') if ENV['GEM_PATH']
+ENV.delete('BUNDLE_GEMFILE') if ENV['BUNDLE_GEMFILE']
+ENV.delete('BUNDLE_PATH') if ENV['BUNDLE_PATH']
+ENV.delete('BUNDLE_WITHOUT') if ENV['BUNDLE_WITHOUT']
+  )ruby";
+
+  openstudio::evalString(clearEnvs);
+};
+
+void addIncludeDirsToLoadPaths(const std::vector<openstudio::path>& includeDirs) {
+  if (includeDirs.empty()) {
+    return;
+  }
+
+  VALUE load_path = rb_gv_get("$LOAD_PATH");
+
+  // Apparently this isn't exposed: `ary_resize_capa(rb_array_len(load_path) + includeDirs.size())``
+
+  // Can't use ranges! starts at clang 15.0, damn.
+  // for (const auto& dirPath : includeDirs | std::views::reverse) {
+  for (const auto& dirPath : includeDirs | boost::adaptors::reversed) {
+    // Insert at 0, in reverse order. Same a includeDirs.concat($LOAD_PATH) in ruby
+    rb_ary_unshift(load_path, rb_str_new2(dirPath.generic_string().c_str()));
+  }
+}
+
+void setRubyEnvVarStr(std::string_view name, std::string_view val) {
+
+  if (val.empty()) {
+    return;
+  }
+
+//   const auto envVarCmd = fmt::format(
+//     R"ruby(
+// $logger.info "Setting {0} to {1}"
+// ENV['{0}'] = '{1}'
+// )ruby",
+//     name, val);
+
+  std::stringstream ss;
+  ss << "$logger.info \"Setting " << name << " to " << val << "\"\n"
+    <<  "ENV['" << name << "'] = '" << val << "'\n";
+  openstudio::evalString(ss.str());
+}
+
+void setRubyEnvVarPath(std::string_view name, const openstudio::path& p) {
+
+  if (p.empty()) {
+    return;
+  }
+
+  setRubyEnvVarStr(name, p.generic_string());
+}
+
+void setGemPathDir(const std::vector<openstudio::path>& gemPathDirs) {
+  if (gemPathDirs.empty()) {
+    return;
+  }
+
+  std::string result;
+  for (const auto& gemDirPath : gemPathDirs) {
+    const auto& s = gemDirPath.generic_string();
+    result += result.empty() ? s : std::string{openstudio::path::preferred_separator} + s;
+  }
+  setRubyEnvVarStr("GEM_PATH", result);
+}
+
+void locateEmbeddedGems(bool use_bundler) {
+
+  std::string initCmd = R"ruby(
+
+  Gem.paths.path << ':/ruby/2.7.0/gems/'
+  Gem.paths.path << ':/ruby/2.7.0/bundler/gems/'
+  Gem::Deprecate.skip = true
+
+  # find all the embedded gems
+  original_embedded_gems = {}
+  begin
+    EmbeddedScripting::fileNames.each do |f|
+      if md = /specifications\/.*\.gemspec$/.match(f) ||
+         md = /bundler\/.*\.gemspec$/.match(f)
+        begin
+          spec = EmbeddedScripting::getFileAsString(f)
+          s = eval(spec)
+          s.loaded_from = f
+          # This is shenanigans because otherwise rubygems will think extensions are missing
+          # But we are initing them manually so they are not missing
+          # Here the missing_extensions? method is redefined for only this instance "s"
+          class << s
+            define_method(:missing_extensions?) { false }
+          end
+          original_embedded_gems[s.name] = s
+
+          init_count = 0
+          Gem::Specification.each {|x| init_count += 1}
+
+          # if already have an equivalent spec this will be a no-op
+          Gem::Specification.add_spec(s)
+
+          post_count = 0
+          Gem::Specification.each {|x| post_count += 1}
+
+          if post_count == init_count
+            $logger.debug "Found system gem #{s.name} #{s.version}, overrides embedded gem"
+          end
+
+        rescue LoadError => e
+          safe_puts e.message
+        rescue => e
+          safe_puts e.message
+        end
+      end
+    end
+  rescue NameError => e
+    # EmbeddedScripting not available
+  end
+
+  original_load_path = $LOAD_PATH.clone
+  embedded_gems_to_activate = []
+
+  # Identify the embedded gems (don't activate them yet)
+  current_dir = Dir.pwd
+  begin
+    # get a list of all the embedded gems
+    dependencies = []
+    original_embedded_gems.each_value do |spec|
+      $logger.debug "Adding dependency on #{spec.name} '~> #{spec.version}'"
+      dependencies << Gem::Dependency.new(spec.name, "~> #{spec.version}")
+    end
+
+    # resolve dependencies
+    activation_errors = false
+    resolver = Gem::Resolver.for_current_gems(dependencies)
+    activation_requests = resolver.resolve
+    $logger.debug "Processing #{activation_requests.size} activation requests"
+    activation_requests.each do |request|
+      do_activate = true
+      spec = request.spec
+
+      # check if this is one of our embedded gems
+      if original_embedded_gems[spec.name]
+        # check if gem can be loaded from RUBYLIB, this supports developer use case
+        original_load_path.each do |lp|
+          if File.exists?(File.join(lp, spec.name)) || File.exists?(File.join(lp, spec.name + '.rb')) || File.exists?(File.join(lp, spec.name + '.so'))
+            $logger.debug "Found #{spec.name} in '#{lp}', overrides gem #{spec.spec_file}"
+            do_activate = false
+            break
+          end
+        end
+      end
+      if do_activate
+        embedded_gems_to_activate << spec
+      end
+
+    end
+
+    if activation_errors
+      return false
+    end
+  ensure
+    Dir.chdir(current_dir)
+  end
+)ruby";
+
+  if (use_bundler) {
+    initCmd += R"ruby(
+  # Load the bundle before activating any embedded gems
+  if true # use_bundler
+
+    embedded_gems_to_activate.each do |spec|
+      if spec.name == "bundler"
+        $logger.debug "Activating gem #{spec.spec_file}"
+        begin
+          # Activate will manipulate the $LOAD_PATH to include the gem
+          spec.activate
+        rescue Gem::LoadError
+          # There may be conflicts between the bundle and the embedded gems,
+          # those will be logged here
+          $logger.error "Error activating gem #{spec.spec_file}"
+          activation_errors = true
+        end
+      end
+    end
+
+    current_dir = Dir.pwd
+
+    original_arch = nil
+    if RbConfig::CONFIG['arch'] =~ /x64-mswin64/
+      # assume that system ruby of 'x64-mingw32' architecture was used to create bundle
+      original_arch = RbConfig::CONFIG['arch']
+      $logger.info "Temporarily replacing arch '#{original_arch}' with 'x64-mingw32' for Bundle"
+      RbConfig::CONFIG['arch'] = 'x64-mingw32'
+    end
+
+    # require bundler
+    # have to do some forward declaration and pre-require to get around autoload cycles
+    require 'bundler/errors'
+    #require 'bundler/environment_preserver'
+    require 'bundler/plugin'
+    #require 'bundler/rubygems_ext'
+    require 'bundler/rubygems_integration'
+    require 'bundler/version'
+    require 'bundler/ruby_version'
+    #require 'bundler/constants'
+    #require 'bundler/current_ruby'
+    require 'bundler/gem_helpers'
+    #require 'bundler/plugin'
+    require 'bundler/source'
+    require 'bundler/definition'
+    require 'bundler/dsl'
+    require 'bundler/uri_credentials_filter'
+    require 'bundler'
+
+    begin
+      # activate bundled gems
+      # bundler will look in:
+      # 1) ENV["BUNDLE_GEMFILE"]
+      # 2) find_file("Gemfile", "gems.rb")
+      #require 'bundler/setup'
+
+      groups = Bundler.definition.groups
+      keep_groups = []
+      without_groups = ENV['BUNDLE_WITHOUT']
+      $logger.info "without_groups = #{without_groups}"
+      groups.each do |g|
+        $logger.info "g = #{g}"
+        if without_groups.include?(g.to_s)
+          $logger.info "Bundling without group '#{g}'"
+        else
+          keep_groups << g
+        end
+      end
+
+      $logger.info "Bundling with groups [#{keep_groups.join(',')}]"
+
+      remaining_specs = []
+      Bundler.definition.specs_for(keep_groups).each {|s| remaining_specs << s.name}
+
+      $logger.info "Specs to be included [#{remaining_specs.join(',')}]"
+
+
+      Bundler.setup(*keep_groups)
+    ensure
+
+      if original_arch
+        $logger.info "Restoring arch '#{original_arch}'"
+        RbConfig::CONFIG['arch'] = original_arch
+      end
+
+      Dir.chdir(current_dir)
+    end
+  end)ruby";
+  }
+
+  initCmd += R"ruby(
+
+  original_load_path = $LOAD_PATH.clone
+
+  embedded_gems_to_activate.each do |spec|
+    $logger.debug "Activating gem #{spec.spec_file}"
+    begin
+      # Activate will manipulate the $LOAD_PATH to include the gem
+      spec.activate
+    rescue Gem::LoadError
+      # There may be conflicts between the bundle and the embedded gems,
+      # those will be logged here
+      $logger.error "Error activating gem #{spec.spec_file}"
+      activation_errors = true
+    end
+  end
+
+  # Get all of the embedded gem paths which were added by activating the embedded gems
+  # This is used by embedded_help::require
+  $EMBEDDED_GEM_PATH = $LOAD_PATH - original_load_path
+  # Make sure no non embedded paths snuck in
+  $EMBEDDED_GEM_PATH = $EMBEDDED_GEM_PATH.select {|p| p.to_s.chars.first == ':'}
+  # Restore LOAD_PATH
+  $LOAD_PATH.reject! {|p| not original_load_path.any? p}
+  )ruby";
+
+  openstudio::evalString(initCmd);
+}
+
+void setupEmbeddedGems(const std::vector<openstudio::path>& includeDirs, const std::vector<openstudio::path>& gemPathDirs,
+                       const openstudio::path& gemHomeDir, const openstudio::path& bundleGemFilePath, const openstudio::path& bundleGemDirPath,
+                       const std::string& bundleWithoutGroups) {
+
+  initEmbeddedGems();
+  setupEmbeddedGemsClearEnvVars();
+
+  addIncludeDirsToLoadPaths(includeDirs);
+  setGemPathDir(gemPathDirs);
+  setRubyEnvVarPath("GEM_HOME", gemHomeDir);
+  bool use_bundler = false;
+  if (!bundleGemFilePath.empty()) {
+    setRubyEnvVarPath("BUNDLE_GEMFILE", bundleGemFilePath);
+    use_bundler = true;
+  }
+  if (!bundleGemDirPath.empty()) {
+    setRubyEnvVarPath("BUNDLE_PATH", bundleGemDirPath);
+  } else if (use_bundler) {
+    // bundle was requested but bundle_path was not provided
+    std::cout << "Warn: Bundle activated but ENV['BUNDLE_PATH'] is not set" << '\n' << "Info: Setting BUNDLE_PATH to ':/ruby/2.7.0/'" << std::endl;
+  }
+
+  if (!bundleWithoutGroups.empty()) {
+    setRubyEnvVarPath("BUNDLE_WITHOUT", bundleWithoutGroups);
+  } else if (use_bundler) {
+    // bundle was requested but bundle_path was not provided
+    std::cout << "Warn: Bundle activated but ENV['BUNDLE_WITHOUT'] is not set" << '\n' << "Info: Setting BUNDLE_WITHOUT to 'test'" << std::endl;
+    setRubyEnvVarPath("BUNDLE_WITHOUT", "test");
+  }
+
+  locateEmbeddedGems(use_bundler);
+}
+
 void initRubyBindings() {
   initBasicRubyBindings();
   initExtendedRubyBindings();
diff --git a/ruby/bindings/InitRubyBindings.hpp b/ruby/bindings/InitRubyBindings.hpp
index 419c389e8b7..176ce6ad95b 100644
--- a/ruby/bindings/InitRubyBindings.hpp
+++ b/ruby/bindings/InitRubyBindings.hpp
@@ -1,12 +1,19 @@
 #ifndef INITRUBYBINDINGS_HPP
 #define INITRUBYBINDINGS_HPP
 
+#include "../../src/utilities/core/Filesystem.hpp"
+
+#include <string>
+
 namespace openstudio {
 namespace ruby {
 
 void initRubyBindings();
 void initBasicRubyBindings();
 void initExtendedRubyBindings();
+void setupEmbeddedGems(const std::vector<openstudio::path>& includeDirs, const std::vector<openstudio::path>& gemPathDirs,
+                       const openstudio::path& gemHomeDir, const openstudio::path& bundleGemFilePath, const openstudio::path& bundleGemDirPath,
+                       const std::string& bundleWithoutGroups);
 
 }  // namespace ruby
 }  // namespace openstudio
diff --git a/ruby/engine/CMakeLists.txt b/ruby/engine/CMakeLists.txt
index 5667857b6d7..3bcf087656e 100644
--- a/ruby/engine/CMakeLists.txt
+++ b/ruby/engine/CMakeLists.txt
@@ -269,4 +269,15 @@ endif()
 
 target_compile_definitions(rubyengine PRIVATE openstudio_scriptengine_EXPORTS SHARED_OS_LIBS)
 
-install(TARGETS rubyengine EXPORT openstudio DESTINATION bin COMPONENT "CLI")
+# This is statically linked to libruby.a, so no need to tweak the rpath stuff for that, but we still need to add $ORIGIN so that it finds libopenstudiolib
+if(APPLE)
+  set_target_properties(rubyengine PROPERTIES
+    INSTALL_RPATH "@loader_path"
+  )
+elseif(UNIX)
+  set_target_properties(rubyengine PROPERTIES
+    INSTALL_RPATH "$ORIGIN"
+  )
+endif()
+
+install(TARGETS rubyengine EXPORT openstudio DESTINATION ${LIB_DESTINATION_DIR} COMPONENT "CLI")
diff --git a/ruby/engine/RubyEngine.cpp b/ruby/engine/RubyEngine.cpp
index 6a0d698bebb..ab53dc5c643 100644
--- a/ruby/engine/RubyEngine.cpp
+++ b/ruby/engine/RubyEngine.cpp
@@ -1,7 +1,6 @@
 #include "RubyEngine.hpp"
 #include "InitRubyBindings.hpp"
 #include <embedded_files.hxx>
-#include <rubyengine_export.h>
 #include <csignal>
 #include <stdexcept>
 #include <string>
@@ -17,6 +16,9 @@
 #  pragma GCC diagnostic pop
 #endif
 
+#include <fmt/format.h>
+
+// TODO: We need to revisit this static initialization stuff
 static int argc = 0;
 static char** argv = nullptr;
 
@@ -41,7 +43,10 @@ VALUE initRestOfOpenStudio(...) {
 };
 
 RubyEngine::RubyEngine(int argc, char* argv[]) : ScriptEngine(argc, argv) {
+
+  ruby_set_argv(argc, argv);
   Init_EmbeddedScripting();
+
   initRubyEngine();
 
   openstudio::ruby::initBasicRubyBindings();
@@ -57,6 +62,12 @@ RubyEngine::RubyEngine(int argc, char* argv[]) : ScriptEngine(argc, argv) {
   rb_enc_set_default_external(rb_enc_from_encoding(rb_utf8_encoding()));
 }
 
+void RubyEngine::setupEmbeddedGems(const std::vector<openstudio::path>& includeDirs, const std::vector<openstudio::path>& gemPathDirs,
+                                   const openstudio::path& gemHomeDir, const openstudio::path& bundleGemFilePath,
+                                   const openstudio::path& bundleGemDirPath, const std::string& bundleWithoutGroups) {
+  openstudio::ruby::setupEmbeddedGems(includeDirs, gemPathDirs, gemHomeDir, bundleGemFilePath, bundleGemDirPath, bundleWithoutGroups);
+}
+
 RubyEngine::~RubyEngine() {
   ruby_finalize();
 }
@@ -105,12 +116,12 @@ 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);
+    std::string expandedName = fmt::format(":/ruby/2.7.0/{}.rb", t_filename);
+    return embedded_files::hasFile(expandedName) ? 1 : 0;
   }
 
   int rb_require_embedded(const char* t_filename) {
-    std::string require_script = R"(require ')" + std::string(t_filename) + R"(')";
+    std::string require_script = fmt::format("require '{}'", t_filename);
     openstudio::evalString(require_script);
     return 0;
   }
diff --git a/ruby/engine/RubyEngine.hpp b/ruby/engine/RubyEngine.hpp
index 19af788039e..00cea9410dd 100644
--- a/ruby/engine/RubyEngine.hpp
+++ b/ruby/engine/RubyEngine.hpp
@@ -1,6 +1,9 @@
 #ifndef RUBYENGINE_included
 #define RUBYENGINE_included
 
+// TODO:
+// #include <rubyengine_export.h>  // from generate_export_header(rubyengine)
+
 #include <ScriptEngine.hpp>
 #include <ScriptEngineAPI.hpp>
 #include <RubyInterpreter.hpp>
@@ -21,6 +24,10 @@ class RubyEngine final : public ScriptEngine
   ScriptObject eval(std::string_view sv) override;
   void exec(std::string_view sv) override;
 
+  virtual void setupEmbeddedGems(const std::vector<openstudio::path>& includeDirs, const std::vector<openstudio::path>& gemPathDirs,
+                                 const openstudio::path& gemHomeDir, const openstudio::path& bundleGemFilePath,
+                                 const openstudio::path& bundleGemDirPath, const std::string& bundleWithoutGroups) 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.
diff --git a/ruby/engine/embedded_gems.rb b/ruby/engine/embedded_gems.rb
new file mode 100644
index 00000000000..4fe37d532d1
--- /dev/null
+++ b/ruby/engine/embedded_gems.rb
@@ -0,0 +1,281 @@
+#(const std::vector<openstudio::path>& includeDirs, std::vector<openstudio::path> gemPathDirs,
+#                               const openstudio::path& gemHomeDir, const openstudio::path& bundleGemFilePath,
+#                               const openstudio::path& bundleGemDirPath, const std::string& bundleWithoutGroups) {
+
+def setup_embedded_gems() #includeDirs, gemPathDirs, gemHomeDir, bundleGemFilePath, bundleGemDirPath, use_bundler, bundleWithoutGroups)
+
+  # Unset RUBY ENVs if previously set (e.g. rvm sets these in shell)
+
+  ENV.delete('GEM_HOME') if ENV['GEM_HOME']
+  ENV.delete('GEM_PATH') if ENV['GEM_PATH']
+  ENV.delete('BUNDLE_GEMFILE') if ENV['BUNDLE_GEMFILE']
+  ENV.delete('BUNDLE_PATH') if ENV['BUNDLE_PATH']
+  ENV.delete('BUNDLE_WITHOUT') if ENV['BUNDLE_WITHOUT']
+
+  if !includeDirs.empty?
+
+    includeDirs = includeDirs.concat($LOAD_PATH)
+
+    $logger.info "Setting $LOAD_PATH to #{includeDirs}"
+    $LOAD_PATH.clear
+
+    includeDirs.each {|p| $LOAD_PATH << p}
+  end
+
+  if !gemPathDirs.empty?
+
+    gemPathDirs = gemPathDirs.join(File::PATH_SEPARATOR)
+
+    $logger.info "Setting GEM_PATH to #{gemPathDirs}"
+    ENV['GEM_PATH'] = gemDirs
+  end
+
+  # Operate on the gem_home option to set GEM_HOME
+  if !gemHomeDir.empty?
+    $logger.info "Setting GEM_HOME to #{gemHomeDir}"
+    ENV['GEM_HOME'] = gemHome
+  end
+
+  # Operate on the bundle option to set BUNDLE_GEMFILE
+  use_bundler = false
+  if !bundleGemFilePath.empty?
+    $logger.info "Setting BUNDLE_GEMFILE to #{bundleGemFilePath}"
+    ENV['BUNDLE_GEMFILE'] = bundleGemFilePath
+    use_bundler = true
+
+  end
+
+  if !bundleGemDirPath.empty?
+    $logger.info "Setting BUNDLE_PATH to #{bundleGemDirPath}"
+    ENV['BUNDLE_PATH'] = bundleGemDirPath
+
+  elsif use_bundler
+    # bundle was requested but bundle_path was not provided
+    $logger.warn "Bundle activated but ENV['BUNDLE_PATH'] is not set"
+
+    $logger.info "Setting BUNDLE_PATH to ':/ruby/2.7.0/'"
+    ENV['BUNDLE_PATH'] = ':/ruby/2.7.0/'
+
+  end
+
+  if !bundleWithoutGroups.empty?
+    $logger.info "Setting BUNDLE_WITHOUT to #{bundleWithoutGroups}"
+    ENV['BUNDLE_WITHOUT'] = bundleWithoutGroups
+
+  elsif use_bundler
+    # bundle was requested but bundle_path was not provided
+    $logger.info "Bundle activated but ENV['BUNDLE_WITHOUT'] is not set"
+
+    # match configuration in build_openstudio_gems
+    $logger.info "Setting BUNDLE_WITHOUT to 'test'"
+    ENV['BUNDLE_WITHOUT'] = 'test'
+
+    # ignore any local config on disk
+    #DLM: this would be correct if the bundle was created here
+    #it would not be correct if the bundle was transfered from another computer
+    #ENV['BUNDLE_IGNORE_CONFIG'] = 'true'
+
+  end
+
+  Gem.paths.path << ':/ruby/2.7.0/gems/'
+  Gem.paths.path << ':/ruby/2.7.0/bundler/gems/'
+  Gem::Deprecate.skip = true
+
+  # find all the embedded gems
+  original_embedded_gems = {}
+  begin
+    EmbeddedScripting::fileNames.each do |f|
+      if md = /specifications\/.*\.gemspec$/.match(f) ||
+         md = /bundler\/.*\.gemspec$/.match(f)
+        begin
+          spec = EmbeddedScripting::getFileAsString(f)
+          s = eval(spec)
+          s.loaded_from = f
+          # This is shenanigans because otherwise rubygems will think extensions are missing
+          # But we are initing them manually so they are not missing
+          # Here the missing_extensions? method is redefined for only this instance "s"
+          class << s
+            define_method(:missing_extensions?) { false }
+          end
+          original_embedded_gems[s.name] = s
+
+          init_count = 0
+          Gem::Specification.each {|x| init_count += 1}
+
+          # if already have an equivalent spec this will be a no-op
+          Gem::Specification.add_spec(s)
+
+          post_count = 0
+          Gem::Specification.each {|x| post_count += 1}
+
+          if post_count == init_count
+            $logger.debug "Found system gem #{s.name} #{s.version}, overrides embedded gem"
+          end
+
+        rescue LoadError => e
+          safe_puts e.message
+        rescue => e
+          safe_puts e.message
+        end
+      end
+    end
+  rescue NameError => e
+    # EmbeddedScripting not available
+  end
+
+  original_load_path = $LOAD_PATH.clone
+  embedded_gems_to_activate = []
+
+  # Identify the embedded gems (don't activate them yet)
+  current_dir = Dir.pwd
+  begin
+    # get a list of all the embedded gems
+    dependencies = []
+    original_embedded_gems.each_value do |spec|
+      $logger.debug "Adding dependency on #{spec.name} '~> #{spec.version}'"
+      dependencies << Gem::Dependency.new(spec.name, "~> #{spec.version}")
+    end
+
+    # resolve dependencies
+    activation_errors = false
+    resolver = Gem::Resolver.for_current_gems(dependencies)
+    activation_requests = resolver.resolve
+    $logger.debug "Processing #{activation_requests.size} activation requests"
+    activation_requests.each do |request|
+      do_activate = true
+      spec = request.spec
+
+      # check if this is one of our embedded gems
+      if original_embedded_gems[spec.name]
+        # check if gem can be loaded from RUBYLIB, this supports developer use case
+        original_load_path.each do |lp|
+          if File.exists?(File.join(lp, spec.name)) || File.exists?(File.join(lp, spec.name + '.rb')) || File.exists?(File.join(lp, spec.name + '.so'))
+            $logger.debug "Found #{spec.name} in '#{lp}', overrides gem #{spec.spec_file}"
+            do_activate = false
+            break
+          end
+        end
+      end
+      if do_activate
+        embedded_gems_to_activate << spec
+      end
+
+    end
+
+    if activation_errors
+      return false
+    end
+  ensure
+    Dir.chdir(current_dir)
+  end
+
+  # Load the bundle before activating any embedded gems
+  if use_bundler
+
+    embedded_gems_to_activate.each do |spec|
+      if spec.name == "bundler"
+        $logger.debug "Activating gem #{spec.spec_file}"
+        begin
+          # Activate will manipulate the $LOAD_PATH to include the gem
+          spec.activate
+        rescue Gem::LoadError
+          # There may be conflicts between the bundle and the embedded gems,
+          # those will be logged here
+          $logger.error "Error activating gem #{spec.spec_file}"
+          activation_errors = true
+        end
+      end
+    end
+
+    current_dir = Dir.pwd
+
+    original_arch = nil
+    if RbConfig::CONFIG['arch'] =~ /x64-mswin64/
+      # assume that system ruby of 'x64-mingw32' architecture was used to create bundle
+      original_arch = RbConfig::CONFIG['arch']
+      $logger.info "Temporarily replacing arch '#{original_arch}' with 'x64-mingw32' for Bundle"
+      RbConfig::CONFIG['arch'] = 'x64-mingw32'
+    end
+
+    # require bundler
+    # have to do some forward declaration and pre-require to get around autoload cycles
+    require 'bundler/errors'
+    #require 'bundler/environment_preserver'
+    require 'bundler/plugin'
+    #require 'bundler/rubygems_ext'
+    require 'bundler/rubygems_integration'
+    require 'bundler/version'
+    require 'bundler/ruby_version'
+    #require 'bundler/constants'
+    #require 'bundler/current_ruby'
+    require 'bundler/gem_helpers'
+    #require 'bundler/plugin'
+    require 'bundler/source'
+    require 'bundler/definition'
+    require 'bundler/dsl'
+    require 'bundler/uri_credentials_filter'
+    require 'bundler'
+
+    begin
+      # activate bundled gems
+      # bundler will look in:
+      # 1) ENV["BUNDLE_GEMFILE"]
+      # 2) find_file("Gemfile", "gems.rb")
+      #require 'bundler/setup'
+
+      groups = Bundler.definition.groups
+      keep_groups = []
+      without_groups = ENV['BUNDLE_WITHOUT']
+      $logger.info "without_groups = #{without_groups}"
+      groups.each do |g|
+        $logger.info "g = #{g}"
+        if without_groups.include?(g.to_s)
+          $logger.info "Bundling without group '#{g}'"
+        else
+          keep_groups << g
+        end
+      end
+
+      $logger.info "Bundling with groups [#{keep_groups.join(',')}]"
+
+      remaining_specs = []
+      Bundler.definition.specs_for(keep_groups).each {|s| remaining_specs << s.name}
+
+      $logger.info "Specs to be included [#{remaining_specs.join(',')}]"
+
+
+      Bundler.setup(*keep_groups)
+    ensure
+
+      if original_arch
+        $logger.info "Restoring arch '#{original_arch}'"
+        RbConfig::CONFIG['arch'] = original_arch
+      end
+
+      Dir.chdir(current_dir)
+    end
+  end
+
+  original_load_path = $LOAD_PATH.clone
+
+  embedded_gems_to_activate.each do |spec|
+    $logger.debug "Activating gem #{spec.spec_file}"
+    begin
+      # Activate will manipulate the $LOAD_PATH to include the gem
+      spec.activate
+    rescue Gem::LoadError
+      # There may be conflicts between the bundle and the embedded gems,
+      # those will be logged here
+      $logger.error "Error activating gem #{spec.spec_file}"
+      activation_errors = true
+    end
+  end
+
+  # Get all of the embedded gem paths which were added by activating the embedded gems
+  # This is used by embedded_help::require
+  $EMBEDDED_GEM_PATH = $LOAD_PATH - original_load_path
+  # Make sure no non embedded paths snuck in
+  $EMBEDDED_GEM_PATH = $EMBEDDED_GEM_PATH.select {|p| p.to_s.chars.first == ':'}
+  # Restore LOAD_PATH
+  $LOAD_PATH.reject! {|p| not original_load_path.any? p}
+end
diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt
index db25b2c259b..5afa31d089e 100644
--- a/src/cli/CMakeLists.txt
+++ b/src/cli/CMakeLists.txt
@@ -50,6 +50,24 @@ if( APPLE )
   target_link_libraries(openstudio PRIVATE ${COREFOUNDATION_LIBRARY})
 endif()
 
+
+
+# it goes into bin/ and we want to find:
+# ../lib/libopenstudiolib.dylib
+if(APPLE)
+  set_target_properties(openstudio PROPERTIES
+    # MACOSX_RPATH ON
+    SKIP_RPATH FALSE
+    SKIP_BUILD_RPATH FALSE
+    CMAKE_BUILD_WITH_INSTALL_RPATH FALSE
+    INSTALL_RPATH "@executable_path;@executable_path/../lib/"
+    INSTALL_RPATH_USE_LINK_PATH FALSE
+  )
+elseif(UNIX)
+  set_target_properties(openstudio PROPERTIES
+    INSTALL_RPATH "$ORIGIN:$ORIGIN/../lib/")
+endif()
+
 install(TARGETS openstudio EXPORT openstudio COMPONENT "CLI")
 
 if( BUILD_PAT )
@@ -92,11 +110,22 @@ if(BUILD_TESTING)
   add_test(NAME OpenStudioCLI.Labs.python_version
     COMMAND $<TARGET_FILE:openstudio> labs python_version
   )
+  set_tests_properties(OpenStudioCLI.Labs.python_version PROPERTIES
+    PASS_REGULAR_EXPRESSION "3\.8\.1[0-9]"
+  )
 
   add_test(NAME OpenStudioCLI.Labs.ruby_execute_line
     COMMAND $<TARGET_FILE:openstudio> labs -e "puts(OpenStudio::Model::Model.new())"
   )
 
+  add_test(NAME OpenStudioCLI.Labs.ruby_execute_line.embedded_gem
+    COMMAND $<TARGET_FILE:openstudio> labs -e "require 'minitest'"
+  )
+
+  add_test(NAME OpenStudioCLI.Labs.ruby_execute_line.embedded_native_gem
+    COMMAND $<TARGET_FILE:openstudio> labs -e "require 'oga'"
+  )
+
   add_test(NAME OpenStudioCLI.Labs.python_execute_line
     COMMAND $<TARGET_FILE:openstudio> labs -c "print(openstudio.model.Model())"
   )
diff --git a/src/cli/MeasureUpdateCommand.hpp b/src/cli/MeasureUpdateCommand.hpp
index cfc4dd9c295..3bfb663317d 100644
--- a/src/cli/MeasureUpdateCommand.hpp
+++ b/src/cli/MeasureUpdateCommand.hpp
@@ -63,4 +63,3 @@ namespace cli {
 }  // namespace openstudio
 
 #endif  // CLI_MEASUREUPDATECOMMAND_HPP
-
diff --git a/src/cli/RunCommand.cpp b/src/cli/RunCommand.cpp
index ee698e0f4ee..11648641451 100644
--- a/src/cli/RunCommand.cpp
+++ b/src/cli/RunCommand.cpp
@@ -41,7 +41,8 @@ namespace cli {
 
   void setupRunFtOptions(CLI::App* app, FtOptions& ftOptions);
 
-  void setupRunOptions(CLI::App* parentApp, ScriptEngineInstance& ruby, ScriptEngineInstance& python) {
+  void setupRunOptions(CLI::App* parentApp, ScriptEngineInstance& ruby, ScriptEngineInstance& python, std::function<void()>& runSetupEmbeddedGems,
+                       std::function<void()>& runSetupPythonPath) {
     /// 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,
@@ -83,7 +84,11 @@ namespace cli {
     // FT options
     setupRunFtOptions(app, opt->ft_options);
 
-    app->callback([opt, &ruby, &python] {
+    app->callback([opt, &ruby, &python, &runSetupEmbeddedGems, &runSetupPythonPath] {
+      // TODO: eventually we want to delay the call to runSetupEmbeddedGems until we KNOW we need ruby measures, so probably want to forward to
+      // OSWorkflow and deal with it there. Same for runSetupPythonPath
+      runSetupEmbeddedGems();
+      runSetupPythonPath();
       openstudio::OSWorkflow workflow(*opt, ruby, python);
       workflow.run();
     });
diff --git a/src/cli/RunCommand.hpp b/src/cli/RunCommand.hpp
index bf133a7403b..01020d54420 100644
--- a/src/cli/RunCommand.hpp
+++ b/src/cli/RunCommand.hpp
@@ -32,13 +32,16 @@
 
 #include <CLI/App.hpp>
 
+#include <functional>
+
 namespace openstudio {
 
 class ScriptEngineInstance;
 
 namespace cli {
 
-  void setupRunOptions(CLI::App* parentApp, ScriptEngineInstance& ruby, ScriptEngineInstance& python);
+  void setupRunOptions(CLI::App* parentApp, ScriptEngineInstance& ruby, ScriptEngineInstance& python, std::function<void()>& runSetupEmbeddedGems,
+                       std::function<void()>& runSetupPythonPath);
   // void setupRunFtOptions(CLI::App* app, FtOptions& ftOptions);
 
 }  // namespace cli
diff --git a/src/cli/UpdateCommand.cpp b/src/cli/UpdateCommand.cpp
index ba574bb3780..f96a96f3c49 100644
--- a/src/cli/UpdateCommand.cpp
+++ b/src/cli/UpdateCommand.cpp
@@ -43,27 +43,6 @@
 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;
 
@@ -132,7 +111,7 @@ sys.argv.append("{}")
       cmd += fmt::format("sys.argv.append(\"{}\")\n", arg);
     }
     cmd += fmt::format(R"python(
-import importlib
+import importlib.util
 spec = importlib.util.spec_from_file_location('__main__', r'{}')
 module = importlib.util.module_from_spec(spec)
 spec.loader.exec_module(module)
@@ -141,5 +120,36 @@ spec.loader.exec_module(module)
     fmt::print("{}\n", cmd);
     pythonEngine->exec(cmd);
   }
+
+  void executeGemListCommand(ScriptEngineInstance& rubyEngine) {
+
+    std::string cmd = R"ruby(
+begin
+  embedded = []
+  user = []
+  Gem::Specification.find_all.each do |spec|
+    if spec.gem_dir.chars.first == ':'
+      embedded << spec
+    else
+      user << spec
+    end
+  end
+
+  embedded.each do |spec|
+    safe_puts "#{spec.name} (#{spec.version}) '#{spec.gem_dir}'"
+  end
+
+  user.each do |spec|
+    safe_puts "#{spec.name} (#{spec.version}) '#{spec.gem_dir}'"
+  end
+
+rescue => e
+  $logger.error "Error listing gems: #{e.message} in #{e.backtrace.join("\n")}"
+  exit e.exit_code
+end
+    )ruby";
+    rubyEngine->exec(cmd);
+  }
+
 }  // namespace cli
 }  // namespace openstudio
diff --git a/src/cli/UpdateCommand.hpp b/src/cli/UpdateCommand.hpp
index 5f31cfbf0cc..924bfd59886 100644
--- a/src/cli/UpdateCommand.hpp
+++ b/src/cli/UpdateCommand.hpp
@@ -39,12 +39,13 @@ 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);
 
+  void executeGemListCommand(ScriptEngineInstance& rubyEngine);
+
 }  // namespace cli
 }  // namespace openstudio
 
diff --git a/src/cli/main.cpp b/src/cli/main.cpp
index d3086644068..b8e7cb123b8 100644
--- a/src/cli/main.cpp
+++ b/src/cli/main.cpp
@@ -10,11 +10,19 @@
 
 #include <OpenStudio.hxx>
 
-#include <iostream>
+#include <fmt/format.h>
+#include <string_view>
 
 #include <CLI/CLI.hpp>
 
 int main(int argc, char* argv[]) {
+
+  constexpr auto rubySpecificOptionsGroupName = "Ruby Options";
+  constexpr auto pythonSpecificOptionsGroupName = "Python Options";
+
+  // constexpr auto executeGroupName = "Script Execution";
+  constexpr auto versionGroupname = "Version Info";
+
   int result = 0;
 
   // ScriptEngineInstance will delay load the engines
@@ -24,6 +32,8 @@ int main(int argc, char* argv[]) {
   if ((argc > 1) && (std::string_view(argv[1]) == "labs")) {
     CLI::App app{"openstudio"};
 
+    app.get_formatter()->column_width(35);
+
     auto* const experimentalApp = app.add_subcommand("labs");
 
     experimentalApp->add_flag_function(
@@ -39,11 +49,6 @@ int main(int argc, char* argv[]) {
       },
       "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,
@@ -57,21 +62,66 @@ int main(int argc, char* argv[]) {
                      "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");
+    // ========================== R U B Y    O P T I O N S ==========================
+    std::vector<openstudio::path> 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")
+      ->group(rubySpecificOptionsGroupName);
+
+    std::vector<openstudio::path> gemPathDirs;
+    experimentalApp
+      ->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")
+      ->group(rubySpecificOptionsGroupName);
+
+    openstudio::path gemHomeDir;
+    experimentalApp->add_option("--gem_home", gemHomeDir, "Set GEM_HOME environment variable")
+      ->option_text("DIR")
+      ->group(rubySpecificOptionsGroupName);
+
+    openstudio::path bundleGemFilePath;
+    experimentalApp->add_option("--bundle", bundleGemFilePath, "Use bundler for GEMFILE")
+      ->option_text("GEMFILE")
+      ->group(rubySpecificOptionsGroupName);
+
+    openstudio::path bundleGemDirPath;
+    experimentalApp->add_option("--bundle_path", bundleGemDirPath, "Use bundler installed gems in BUNDLE_PATH")
+      ->option_text("BUNDLE_PATH")
+      ->group(rubySpecificOptionsGroupName);
+
+    // std::vector<std::string>
+    std::string bundleWithoutGroups;
+    experimentalApp
+      ->add_option(
+        "--bundle_without", bundleWithoutGroups,
+        "Space separated list of groups for bundler to exclude in WITHOUT_GROUPS.  Surround multiple groups with quotes like \"test development\"")
+      ->option_text("WITHOUT_GROUPS")
+      ->group(rubySpecificOptionsGroupName);  // ->delimiter(' ');
+
+    std::function<void()> runSetupEmbeddedGems = [&rubyEngine, &includeDirs, &gemPathDirs, &gemHomeDir, &bundleGemFilePath, &bundleGemDirPath,
+                                                  &bundleWithoutGroups]() {
+      rubyEngine->setupEmbeddedGems(includeDirs, gemPathDirs, gemHomeDir, bundleGemFilePath, bundleGemDirPath, bundleWithoutGroups);
+    };
 
-    std::string gemHomeDir;
-    experimentalApp->add_option("--gem_home", gemHomeDir, "Set GEM_HOME environment variable")->option_text("DIR");
+    // ========================== P Y T H O N    O P T I O N S ==========================
 
-    std::string gemFile;
-    experimentalApp->add_option("--bundle", gemFile, "Use bundler for GEMFILE'")->option_text("GEMFILE");
+    std::vector<openstudio::path> pythonPathDirs;
+    experimentalApp
+      ->add_option("--python_path", pythonPathDirs,
+                   "Add additional directory to add to front of PYTHONPATH environment variable (may be used more than once)")
+      ->option_text("DIR")
+      ->group(pythonSpecificOptionsGroupName);
 
-    [[maybe_unused]] auto* energyplus_versionCommand =
-      experimentalApp->add_subcommand("energyplus_version", "Returns the EnergyPlus version used by the CLI")->callback([]() {
-        fmt::print("{}+{}\n", energyPlusVersion(), energyPlusBuildSHA());
-      });
+    openstudio::path pythonHomeDir;
+    experimentalApp->add_option("--python_home", pythonHomeDir, "Set PYTHONHOME environment variable")
+      ->option_text("DIR")
+      ->group(pythonSpecificOptionsGroupName);
+
+    std::function<void()> runSetupPythonPath = [&pythonEngine, &pythonPathDirs, &pythonHomeDir]() {
+      pythonEngine->setupPythonPath(pythonPathDirs, pythonHomeDir);
+    };
 
     {
       auto* execute_ruby_scriptCommand = experimentalApp->add_subcommand("execute_ruby_script", "Executes a ruby file");
@@ -81,7 +131,8 @@ int main(int argc, char* argv[]) {
       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] {
+      execute_ruby_scriptCommand->callback([&rubyScriptPath, &rubyEngine, &executeRubyScriptCommandArgs, &runSetupEmbeddedGems] {
+        runSetupEmbeddedGems();
         openstudio::cli::executeRubyScriptCommand(rubyScriptPath, rubyEngine, executeRubyScriptCommandArgs);
       });
     }
@@ -94,28 +145,23 @@ int main(int argc, char* argv[]) {
       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] {
+      execute_python_scriptCommand->callback([&pythonScriptPath, &pythonEngine, &executePythonScriptCommandArgs, &runSetupPythonPath] {
+        runSetupPythonPath();
         openstudio::cli::executePythonScriptCommand(pythonScriptPath, pythonEngine, executePythonScriptCommandArgs);
       });
     }
 
-    [[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)");
+    [[maybe_unused]] auto* gem_listCommand =
+      experimentalApp->add_subcommand("gem_list", "Lists the set gems available to openstudio")->callback([&rubyEngine, &runSetupEmbeddedGems]() {
+        runSetupEmbeddedGems();
+        openstudio::cli::executeGemListCommand(rubyEngine);
       });
 
+    // Not hidding any commands right now
+    // [[maybe_unused]] auto* list_commandsCommand = experimentalApp->add_subcommand("list_commands", "Lists the entire set of available commands");
+
     // run command
-    openstudio::cli::setupRunOptions(experimentalApp, rubyEngine, pythonEngine);
+    openstudio::cli::setupRunOptions(experimentalApp, rubyEngine, pythonEngine, runSetupEmbeddedGems, runSetupPythonPath);
 
     // update (model) command
     // openstudio::cli::setupUpdateCommand(experimentalApp);
@@ -137,10 +183,31 @@ int main(int argc, char* argv[]) {
 
     openstudio::cli::MeasureUpdateOptions::setupMeasureUpdateOptions(experimentalApp, rubyEngine);
 
+    // ==========================  V E R S I O N ==========================
+    [[maybe_unused]] auto* openstudio_versionCommand =
+      experimentalApp->add_subcommand("openstudio_version", "Returns the OpenStudio version used by the CLI")
+        ->group(versionGroupname)
+        ->callback([]() { fmt::print("{}\n", openStudioLongVersion()); });
+
+    [[maybe_unused]] auto* energyplus_versionCommand =
+      experimentalApp->add_subcommand("energyplus_version", "Returns the EnergyPlus version used by the CLI")
+        ->group(versionGroupname)
+        ->callback([]() { fmt::print("{}+{}\n", energyPlusVersion(), energyPlusBuildSHA()); });
+    [[maybe_unused]] auto* ruby_versionCommand =
+      experimentalApp->add_subcommand("ruby_version", "Returns the Ruby version used by the CLI")->group(versionGroupname)->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")
+                                                     ->group(versionGroupname)
+                                                     ->callback([&pythonEngine]() { pythonEngine->exec("import sys; print(sys.version)"); });
+
+    // ====================================================================
+
     CLI11_PARSE(app, argc, argv);
 
     if (*execRubyOption) {
       fmt::print("--execute Flag received {} times.\n", execRubyOption->count());
+      runSetupEmbeddedGems();
       rubyEngine->exec("OpenStudio::init_rest_of_openstudio()");
       for (auto& cmd : executeRubyCmds) {
         fmt::print("{}\n", cmd);
@@ -149,6 +216,7 @@ int main(int argc, char* argv[]) {
     }
     if (*execPythonOption) {
       fmt::print("--pyexecute Flag received {} times.\n", execPythonOption->count());
+      runSetupPythonPath();
       for (auto& cmd : executePythonCmds) {
         fmt::print("{}\n", cmd);
         pythonEngine->exec(cmd);
diff --git a/src/energyplus/ForwardTranslator/ForwardTranslateZoneHVACPackagedTerminalAirConditioner.cpp b/src/energyplus/ForwardTranslator/ForwardTranslateZoneHVACPackagedTerminalAirConditioner.cpp
index e68fb23ef27..975d3dda6c3 100644
--- a/src/energyplus/ForwardTranslator/ForwardTranslateZoneHVACPackagedTerminalAirConditioner.cpp
+++ b/src/energyplus/ForwardTranslator/ForwardTranslateZoneHVACPackagedTerminalAirConditioner.cpp
@@ -359,10 +359,9 @@ namespace energyplus {
 
     // SupplyAirFanOperatingModeScheduleName
 
-    if (boost::optional<Schedule> schedule = modelObject.supplyAirFanOperatingModeSchedule()) {
-      if (boost::optional<IdfObject> _schedule = translateAndMapModelObject(schedule.get())) {
-        idfObject.setString(ZoneHVAC_PackagedTerminalAirConditionerFields::SupplyAirFanOperatingModeScheduleName, _schedule->name().get());
-      }
+    Schedule fanOpSchedule = modelObject.supplyAirFanOperatingModeSchedule();
+    if (boost::optional<IdfObject> _schedule = translateAndMapModelObject(fanOpSchedule)) {
+      idfObject.setString(ZoneHVAC_PackagedTerminalAirConditionerFields::SupplyAirFanOperatingModeScheduleName, _schedule->name().get());
     }
 
     return idfObject;
diff --git a/src/energyplus/ForwardTranslator/ForwardTranslateZoneHVACPackagedTerminalHeatPump.cpp b/src/energyplus/ForwardTranslator/ForwardTranslateZoneHVACPackagedTerminalHeatPump.cpp
index 17864f9b676..394d02a51a3 100644
--- a/src/energyplus/ForwardTranslator/ForwardTranslateZoneHVACPackagedTerminalHeatPump.cpp
+++ b/src/energyplus/ForwardTranslator/ForwardTranslateZoneHVACPackagedTerminalHeatPump.cpp
@@ -433,10 +433,9 @@ namespace energyplus {
 
     // SupplyAirFanOperatingModeScheduleName
 
-    if (boost::optional<Schedule> schedule = modelObject.supplyAirFanOperatingModeSchedule()) {
-      if (boost::optional<IdfObject> _schedule = translateAndMapModelObject(schedule.get())) {
-        idfObject.setString(ZoneHVAC_PackagedTerminalHeatPumpFields::SupplyAirFanOperatingModeScheduleName, _schedule->name().get());
-      }
+    Schedule fanOpSchedule = modelObject.supplyAirFanOperatingModeSchedule();
+    if (boost::optional<IdfObject> _schedule = translateAndMapModelObject(fanOpSchedule)) {
+      idfObject.setString(ZoneHVAC_PackagedTerminalHeatPumpFields::SupplyAirFanOperatingModeScheduleName, _schedule->name().get());
     }
 
     return idfObject;
diff --git a/src/lib/CMakeLists.txt b/src/lib/CMakeLists.txt
index 01453aebc64..e9c574eb8bc 100644
--- a/src/lib/CMakeLists.txt
+++ b/src/lib/CMakeLists.txt
@@ -58,7 +58,7 @@ target_include_directories(
 
 install(TARGETS openstudiolib
         EXPORT openstudio
-        DESTINATION lib
+        DESTINATION ${LIB_DESTINATION_DIR}
         COMPONENT "CPPAPI"
         INCLUDES DESTINATION include
           include/openstudio
diff --git a/src/model/ZoneHVACPackagedTerminalAirConditioner.cpp b/src/model/ZoneHVACPackagedTerminalAirConditioner.cpp
index 5851405525b..367231d6662 100644
--- a/src/model/ZoneHVACPackagedTerminalAirConditioner.cpp
+++ b/src/model/ZoneHVACPackagedTerminalAirConditioner.cpp
@@ -69,15 +69,15 @@ namespace model {
       : ZoneHVACComponent_Impl(other, model, keepHandle) {}
 
     ModelObject ZoneHVACPackagedTerminalAirConditioner_Impl::clone(Model model) const {
-      ZoneHVACPackagedTerminalAirConditioner ptacClone = ZoneHVACComponent_Impl::clone(model).cast<ZoneHVACPackagedTerminalAirConditioner>();
+      auto ptacClone = ZoneHVACComponent_Impl::clone(model).cast<ZoneHVACPackagedTerminalAirConditioner>();
 
-      HVACComponent supplyFanClone = this->supplyAirFan().clone(model).cast<HVACComponent>();
+      auto supplyFanClone = this->supplyAirFan().clone(model).cast<HVACComponent>();
 
       auto t_heatingCoil = heatingCoil();
-      HVACComponent heatingCoilClone = t_heatingCoil.clone(model).cast<HVACComponent>();
+      auto heatingCoilClone = t_heatingCoil.clone(model).cast<HVACComponent>();
 
       auto t_coolingCoil = coolingCoil();
-      HVACComponent coolingCoilClone = t_coolingCoil.clone(model).cast<HVACComponent>();
+      auto coolingCoilClone = t_coolingCoil.clone(model).cast<HVACComponent>();
 
       ptacClone.setSupplyAirFan(supplyFanClone);
 
@@ -132,12 +132,13 @@ namespace model {
     std::vector<ScheduleTypeKey> ZoneHVACPackagedTerminalAirConditioner_Impl::getScheduleTypeKeys(const Schedule& schedule) const {
       std::vector<ScheduleTypeKey> result;
       UnsignedVector fieldIndices = getSourceIndices(schedule.handle());
-      UnsignedVector::const_iterator b(fieldIndices.begin()), e(fieldIndices.end());
+      UnsignedVector::const_iterator b(fieldIndices.begin());
+      UnsignedVector::const_iterator e(fieldIndices.end());
       if (std::find(b, e, OS_ZoneHVAC_PackagedTerminalAirConditionerFields::AvailabilityScheduleName) != e) {
-        result.push_back(ScheduleTypeKey("ZoneHVACPackagedTerminalAirConditioner", "Availability"));
+        result.emplace_back("ZoneHVACPackagedTerminalAirConditioner", "Availability");
       }
       if (std::find(b, e, OS_ZoneHVAC_PackagedTerminalAirConditionerFields::SupplyAirFanOperatingModeScheduleName) != e) {
-        result.push_back(ScheduleTypeKey("ZoneHVACPackagedTerminalAirConditioner", "Supply Air Fan Operating Mode"));
+        result.emplace_back("ZoneHVACPackagedTerminalAirConditioner", "Supply Air Fan Operating Mode");
       }
       return result;
     }
@@ -172,7 +173,7 @@ namespace model {
         LOG(Error, "Required availability schedule not set, using 'Always On' schedule");
         value = this->model().alwaysOnDiscreteSchedule();
         OS_ASSERT(value);
-        const_cast<ZoneHVACPackagedTerminalAirConditioner_Impl*>(this)->setAvailabilitySchedule(*value);
+        const_cast<ZoneHVACPackagedTerminalAirConditioner_Impl*>(this)->setAvailabilitySchedule(*value);  // NOLINT
         value = optionalAvailabilitySchedule();
       }
       OS_ASSERT(value);
@@ -301,9 +302,19 @@ namespace model {
       return isEmpty(OS_ZoneHVAC_PackagedTerminalAirConditionerFields::FanPlacement);
     }
 
-    boost::optional<Schedule> ZoneHVACPackagedTerminalAirConditioner_Impl::supplyAirFanOperatingModeSchedule() const {
-      return getObject<ModelObject>().getModelObjectTarget<Schedule>(
-        OS_ZoneHVAC_PackagedTerminalAirConditionerFields::SupplyAirFanOperatingModeScheduleName);
+    Schedule ZoneHVACPackagedTerminalAirConditioner_Impl::supplyAirFanOperatingModeSchedule() const {
+      boost::optional<Schedule> value = optionalSupplyAirFanOperatingModeSchedule();
+      if (!value) {
+        // it is an error if we get here, however we don't want to crash
+        // so we hook up to global always on schedule
+        LOG(Error, "Required availability schedule not set, using 'Always Off' schedule");
+        value = this->model().alwaysOffDiscreteSchedule();
+        OS_ASSERT(value);
+        const_cast<ZoneHVACPackagedTerminalAirConditioner_Impl*>(this)->setSupplyAirFanOperatingModeSchedule(*value);  // NOLINT
+        value = optionalSupplyAirFanOperatingModeSchedule();
+      }
+      OS_ASSERT(value);
+      return value.get();
     }
 
     bool ZoneHVACPackagedTerminalAirConditioner_Impl::setAvailabilitySchedule(Schedule& schedule) {
@@ -312,12 +323,12 @@ namespace model {
       return result;
     }
 
-    bool ZoneHVACPackagedTerminalAirConditioner_Impl::setOutdoorAirMixerObjectType(std::string outdoorAirMixerObjectType) {
+    bool ZoneHVACPackagedTerminalAirConditioner_Impl::setOutdoorAirMixerObjectType(const std::string& outdoorAirMixerObjectType) {
       bool result = setString(OS_ZoneHVAC_PackagedTerminalAirConditionerFields::OutdoorAirMixerObjectType, outdoorAirMixerObjectType);
       return result;
     }
 
-    bool ZoneHVACPackagedTerminalAirConditioner_Impl::setOutdoorAirMixerName(std::string outdoorAirMixerName) {
+    bool ZoneHVACPackagedTerminalAirConditioner_Impl::setOutdoorAirMixerName(const std::string& outdoorAirMixerName) {
       bool result = setString(OS_ZoneHVAC_PackagedTerminalAirConditionerFields::OutdoorAirMixerName, outdoorAirMixerName);
       OS_ASSERT(result);
       return result;
@@ -449,11 +460,9 @@ namespace model {
     bool ZoneHVACPackagedTerminalAirConditioner_Impl::setHeatingCoil(HVACComponent& heatingCoil) {
       bool isAllowedType = false;
 
-      if (heatingCoil.iddObjectType() == IddObjectType::OS_Coil_Heating_Gas) {
-        isAllowedType = true;
-      } else if (heatingCoil.iddObjectType() == IddObjectType::OS_Coil_Heating_Electric) {
-        isAllowedType = true;
-      } else if (heatingCoil.iddObjectType() == IddObjectType::OS_Coil_Heating_Water) {
+      auto iddObjectType = heatingCoil.iddObjectType();
+      if ((iddObjectType == IddObjectType::OS_Coil_Heating_Gas) || (iddObjectType == IddObjectType::OS_Coil_Heating_Electric)
+          || (iddObjectType == IddObjectType::OS_Coil_Heating_Water)) {
         isAllowedType = true;
       }
 
@@ -469,10 +478,10 @@ namespace model {
     bool ZoneHVACPackagedTerminalAirConditioner_Impl::setCoolingCoil(HVACComponent& coolingCoil) {
       bool isAllowedType = false;
 
-      if ((coolingCoil.iddObjectType() == IddObjectType::OS_Coil_Cooling_DX_SingleSpeed)
-          || (coolingCoil.iddObjectType() == IddObjectType::OS_Coil_Cooling_DX_VariableSpeed)
-          || (coolingCoil.iddObjectType() == IddObjectType::OS_CoilSystem_Cooling_DX_HeatExchangerAssisted)
-          || (coolingCoil.iddObjectType() == IddObjectType::OS_Coil_Cooling_DX)) {
+      auto iddObjectType = coolingCoil.iddObjectType();
+      if ((iddObjectType == IddObjectType::OS_Coil_Cooling_DX_SingleSpeed) || (iddObjectType == IddObjectType::OS_Coil_Cooling_DX_VariableSpeed)
+          || (iddObjectType == IddObjectType::OS_CoilSystem_Cooling_DX_HeatExchangerAssisted)
+          || (iddObjectType == IddObjectType::OS_Coil_Cooling_DX)) {
         isAllowedType = true;
       }
 
@@ -486,7 +495,7 @@ namespace model {
       }
     }
 
-    bool ZoneHVACPackagedTerminalAirConditioner_Impl::setFanPlacement(std::string fanPlacement) {
+    bool ZoneHVACPackagedTerminalAirConditioner_Impl::setFanPlacement(const std::string& fanPlacement) {
       bool result = setString(OS_ZoneHVAC_PackagedTerminalAirConditionerFields::FanPlacement, fanPlacement);
       return result;
     }
@@ -502,11 +511,6 @@ namespace model {
       return result;
     }
 
-    void ZoneHVACPackagedTerminalAirConditioner_Impl::resetSupplyAirFanOperatingModeSchedule() {
-      bool result = setString(OS_ZoneHVAC_PackagedTerminalAirConditionerFields::SupplyAirFanOperatingModeScheduleName, "");
-      OS_ASSERT(result);
-    }
-
     boost::optional<Schedule> ZoneHVACPackagedTerminalAirConditioner_Impl::optionalAvailabilitySchedule() const {
       return getObject<ModelObject>().getModelObjectTarget<Schedule>(OS_ZoneHVAC_PackagedTerminalAirConditionerFields::AvailabilityScheduleName);
     }
@@ -520,6 +524,11 @@ namespace model {
       return getObject<ModelObject>().getModelObjectTarget<HVACComponent>(OS_ZoneHVAC_PackagedTerminalAirConditionerFields::CoolingCoilName);
     }
 
+    boost::optional<Schedule> ZoneHVACPackagedTerminalAirConditioner_Impl::optionalSupplyAirFanOperatingModeSchedule() const {
+      return getObject<ModelObject>().getModelObjectTarget<Schedule>(
+        OS_ZoneHVAC_PackagedTerminalAirConditionerFields::SupplyAirFanOperatingModeScheduleName);
+    }
+
     boost::optional<ModelObject> ZoneHVACPackagedTerminalAirConditioner_Impl::availabilityScheduleAsModelObject() const {
       OptionalModelObject result = availabilitySchedule();
       return result;
@@ -600,13 +609,9 @@ namespace model {
         if (intermediate) {
           Schedule schedule(*intermediate);
           return setSupplyAirFanOperatingModeSchedule(schedule);
-        } else {
-          return false;
         }
-      } else {
-        resetSupplyAirFanOperatingModeSchedule();
       }
-      return true;
+      return false;
     }
 
     boost::optional<double> ZoneHVACPackagedTerminalAirConditioner_Impl::autosizedSupplyAirFlowRateDuringCoolingOperation() const {
@@ -693,7 +698,16 @@ namespace model {
     setHeatingCoil(heatingCoil);
     setCoolingCoil(coolingCoil);
     setFanPlacement("DrawThrough");
-    resetSupplyAirFanOperatingModeSchedule();
+    // When Blank, E+ defaults to 0 (cycling). If it's a FanConstantVolume though, you must provide an always On or E+ will Fatal.
+    if (supplyAirFan.iddObjectType() == IddObjectType::OS_Fan_ConstantVolume) {
+      auto alwaysOn = model.alwaysOnDiscreteSchedule();
+      ok = setSupplyAirFanOperatingModeSchedule(alwaysOn);
+      OS_ASSERT(ok);
+    } else {
+      auto alwaysOff = model.alwaysOffDiscreteSchedule();
+      ok = setSupplyAirFanOperatingModeSchedule(alwaysOff);
+      OS_ASSERT(ok);
+    }
 
     autosizeSupplyAirFlowRateDuringCoolingOperation();
     autosizeSupplyAirFlowRateDuringHeatingOperation();
@@ -704,8 +718,7 @@ namespace model {
   }
 
   IddObjectType ZoneHVACPackagedTerminalAirConditioner::iddObjectType() {
-    IddObjectType result(IddObjectType::OS_ZoneHVAC_PackagedTerminalAirConditioner);
-    return result;
+    return IddObjectType::OS_ZoneHVAC_PackagedTerminalAirConditioner;
   }
 
   std::vector<std::string> ZoneHVACPackagedTerminalAirConditioner::outdoorAirMixerObjectTypeValues() {
@@ -789,11 +802,11 @@ namespace model {
     return getImpl<detail::ZoneHVACPackagedTerminalAirConditioner_Impl>()->isFanPlacementDefaulted();
   }
 
-  bool ZoneHVACPackagedTerminalAirConditioner::setOutdoorAirMixerObjectType(std::string outdoorAirMixerObjectType) {
+  bool ZoneHVACPackagedTerminalAirConditioner::setOutdoorAirMixerObjectType(const std::string& outdoorAirMixerObjectType) {
     return getImpl<detail::ZoneHVACPackagedTerminalAirConditioner_Impl>()->setOutdoorAirMixerObjectType(outdoorAirMixerObjectType);
   }
 
-  bool ZoneHVACPackagedTerminalAirConditioner::setOutdoorAirMixerName(std::string outdoorAirMixerName) {
+  bool ZoneHVACPackagedTerminalAirConditioner::setOutdoorAirMixerName(const std::string& outdoorAirMixerName) {
     return getImpl<detail::ZoneHVACPackagedTerminalAirConditioner_Impl>()->setOutdoorAirMixerName(outdoorAirMixerName);
   }
 
@@ -861,7 +874,7 @@ namespace model {
     getImpl<detail::ZoneHVACPackagedTerminalAirConditioner_Impl>()->autosizeOutdoorAirFlowRateWhenNoCoolingorHeatingisNeeded();
   }
 
-  bool ZoneHVACPackagedTerminalAirConditioner::setFanPlacement(std::string fanPlacement) {
+  bool ZoneHVACPackagedTerminalAirConditioner::setFanPlacement(const std::string& fanPlacement) {
     return getImpl<detail::ZoneHVACPackagedTerminalAirConditioner_Impl>()->setFanPlacement(fanPlacement);
   }
 
@@ -873,7 +886,7 @@ namespace model {
     std::shared_ptr<detail::ZoneHVACPackagedTerminalAirConditioner_Impl> impl)
     : ZoneHVACComponent(std::move(impl)) {}
 
-  boost::optional<Schedule> ZoneHVACPackagedTerminalAirConditioner::supplyAirFanOperatingModeSchedule() const {
+  Schedule ZoneHVACPackagedTerminalAirConditioner::supplyAirFanOperatingModeSchedule() const {
     return getImpl<detail::ZoneHVACPackagedTerminalAirConditioner_Impl>()->supplyAirFanOperatingModeSchedule();
   }
 
@@ -881,10 +894,6 @@ namespace model {
     return getImpl<detail::ZoneHVACPackagedTerminalAirConditioner_Impl>()->setSupplyAirFanOperatingModeSchedule(schedule);
   }
 
-  void ZoneHVACPackagedTerminalAirConditioner::resetSupplyAirFanOperatingModeSchedule() {
-    getImpl<detail::ZoneHVACPackagedTerminalAirConditioner_Impl>()->resetSupplyAirFanOperatingModeSchedule();
-  }
-
   Schedule ZoneHVACPackagedTerminalAirConditioner::availabilitySchedule() const {
     return getImpl<detail::ZoneHVACPackagedTerminalAirConditioner_Impl>()->availabilitySchedule();
   }
diff --git a/src/model/ZoneHVACPackagedTerminalAirConditioner.hpp b/src/model/ZoneHVACPackagedTerminalAirConditioner.hpp
index a4299f0c668..9c9092711cf 100644
--- a/src/model/ZoneHVACPackagedTerminalAirConditioner.hpp
+++ b/src/model/ZoneHVACPackagedTerminalAirConditioner.hpp
@@ -56,7 +56,7 @@ namespace model {
     ZoneHVACPackagedTerminalAirConditioner(const Model& model, Schedule& availabilitySchedule, HVACComponent& supplyAirFan,
                                            HVACComponent& heatingCoil, HVACComponent& coolingCoil);
 
-    virtual ~ZoneHVACPackagedTerminalAirConditioner() {}
+    virtual ~ZoneHVACPackagedTerminalAirConditioner() = default;
 
     //@}
 
@@ -121,7 +121,7 @@ namespace model {
 
     bool isFanPlacementDefaulted() const;
 
-    boost::optional<Schedule> supplyAirFanOperatingModeSchedule() const;
+    Schedule supplyAirFanOperatingModeSchedule() const;
 
     //@}
     /** @name Setters */
@@ -129,9 +129,9 @@ namespace model {
 
     bool setAvailabilitySchedule(Schedule& schedule);
 
-    bool setOutdoorAirMixerObjectType(std::string outdoorAirMixerObjectType);
+    bool setOutdoorAirMixerObjectType(const std::string& outdoorAirMixerObjectType);
 
-    bool setOutdoorAirMixerName(std::string outdoorAirMixerName);
+    bool setOutdoorAirMixerName(const std::string& outdoorAirMixerName);
 
     bool setSupplyAirFlowRateDuringCoolingOperation(double supplyAirFlowRateDuringCoolingOperation);
 
@@ -167,7 +167,7 @@ namespace model {
 
     bool setCoolingCoil(HVACComponent& coolingCoil);
 
-    bool setFanPlacement(std::string fanPlacement);
+    bool setFanPlacement(const std::string& fanPlacement);
 
     void resetFanPlacement();
 
@@ -176,8 +176,6 @@ namespace model {
    *  fan continuously. */
     bool setSupplyAirFanOperatingModeSchedule(Schedule& schedule);
 
-    void resetSupplyAirFanOperatingModeSchedule();
-
     //@}
     /** @name Other */
     //@{
@@ -197,7 +195,7 @@ namespace model {
     //@}
    protected:
     /// @cond
-    typedef detail::ZoneHVACPackagedTerminalAirConditioner_Impl ImplType;
+    using ImplType = detail::ZoneHVACPackagedTerminalAirConditioner_Impl;
 
     explicit ZoneHVACPackagedTerminalAirConditioner(std::shared_ptr<detail::ZoneHVACPackagedTerminalAirConditioner_Impl> impl);
 
@@ -212,10 +210,10 @@ namespace model {
   };
 
   /** \relates ZoneHVACPackagedTerminalAirConditioner*/
-  typedef boost::optional<ZoneHVACPackagedTerminalAirConditioner> OptionalZoneHVACPackagedTerminalAirConditioner;
+  using OptionalZoneHVACPackagedTerminalAirConditioner = boost::optional<ZoneHVACPackagedTerminalAirConditioner>;
 
   /** \relates ZoneHVACPackagedTerminalAirConditioner*/
-  typedef std::vector<ZoneHVACPackagedTerminalAirConditioner> ZoneHVACPackagedTerminalAirConditionerVector;
+  using ZoneHVACPackagedTerminalAirConditionerVector = std::vector<ZoneHVACPackagedTerminalAirConditioner>;
 
 }  // namespace model
 }  // namespace openstudio
diff --git a/src/model/ZoneHVACPackagedTerminalAirConditioner_Impl.hpp b/src/model/ZoneHVACPackagedTerminalAirConditioner_Impl.hpp
index 86fd0e8c319..9524483fd43 100644
--- a/src/model/ZoneHVACPackagedTerminalAirConditioner_Impl.hpp
+++ b/src/model/ZoneHVACPackagedTerminalAirConditioner_Impl.hpp
@@ -56,7 +56,7 @@ namespace model {
 
       ZoneHVACPackagedTerminalAirConditioner_Impl(const ZoneHVACPackagedTerminalAirConditioner_Impl& other, Model_Impl* model, bool keepHandle);
 
-      virtual ~ZoneHVACPackagedTerminalAirConditioner_Impl() {}
+      virtual ~ZoneHVACPackagedTerminalAirConditioner_Impl() = default;
 
       //@}
       /** @name Virtual Methods */
@@ -122,7 +122,7 @@ namespace model {
 
       bool isFanPlacementDefaulted() const;
 
-      boost::optional<Schedule> supplyAirFanOperatingModeSchedule() const;
+      Schedule supplyAirFanOperatingModeSchedule() const;
 
       boost::optional<double> autosizedSupplyAirFlowRateDuringCoolingOperation() const;
 
@@ -146,9 +146,9 @@ namespace model {
 
       bool setAvailabilitySchedule(Schedule& schedule);
 
-      bool setOutdoorAirMixerObjectType(std::string outdoorAirMixerObjectType);
+      bool setOutdoorAirMixerObjectType(const std::string& outdoorAirMixerObjectType);
 
-      bool setOutdoorAirMixerName(std::string outdoorAirMixerName);
+      bool setOutdoorAirMixerName(const std::string& outdoorAirMixerName);
 
       bool setSupplyAirFlowRateDuringCoolingOperation(boost::optional<double> supplyAirFlowRateDuringCoolingOperation);
 
@@ -184,14 +184,12 @@ namespace model {
 
       bool setCoolingCoil(HVACComponent& coolingCoil);
 
-      bool setFanPlacement(std::string fanPlacement);
+      bool setFanPlacement(const std::string& fanPlacement);
 
       void resetFanPlacement();
 
       bool setSupplyAirFanOperatingModeSchedule(Schedule& schedule);
 
-      void resetSupplyAirFanOperatingModeSchedule();
-
       //@}
       /** @name Other */
       //@{
@@ -206,6 +204,7 @@ namespace model {
       boost::optional<HVACComponent> optionalSupplyAirFan() const;
       boost::optional<HVACComponent> optionalHeatingCoil() const;
       boost::optional<HVACComponent> optionalCoolingCoil() const;
+      boost::optional<Schedule> optionalSupplyAirFanOperatingModeSchedule() const;
 
       boost::optional<ModelObject> availabilityScheduleAsModelObject() const;
       boost::optional<ModelObject> supplyAirFanAsModelObject() const;
diff --git a/src/model/ZoneHVACPackagedTerminalHeatPump.cpp b/src/model/ZoneHVACPackagedTerminalHeatPump.cpp
index 8979b27759f..486e400484a 100644
--- a/src/model/ZoneHVACPackagedTerminalHeatPump.cpp
+++ b/src/model/ZoneHVACPackagedTerminalHeatPump.cpp
@@ -67,19 +67,19 @@ namespace model {
       : ZoneHVACComponent_Impl(other, model, keepHandle) {}
 
     ModelObject ZoneHVACPackagedTerminalHeatPump_Impl::clone(Model model) const {
-      ZoneHVACPackagedTerminalHeatPump pthpClone = ZoneHVACComponent_Impl::clone(model).cast<ZoneHVACPackagedTerminalHeatPump>();
+      auto pthpClone = ZoneHVACComponent_Impl::clone(model).cast<ZoneHVACPackagedTerminalHeatPump>();
 
       //Schedule scheduleClone = this->availabilitySchedule().clone(model).cast<Schedule>();
 
-      HVACComponent supplyFanClone = this->supplyAirFan().clone(model).cast<HVACComponent>();
+      auto supplyFanClone = this->supplyAirFan().clone(model).cast<HVACComponent>();
 
       auto t_heatingCoil = heatingCoil();
-      HVACComponent heatingCoilClone = t_heatingCoil.clone(model).cast<HVACComponent>();
+      auto heatingCoilClone = t_heatingCoil.clone(model).cast<HVACComponent>();
 
       auto t_coolingCoil = coolingCoil();
-      HVACComponent coolingCoilClone = t_coolingCoil.clone(model).cast<HVACComponent>();
+      auto coolingCoilClone = t_coolingCoil.clone(model).cast<HVACComponent>();
 
-      HVACComponent supplementalHeatingCoilClone = this->supplementalHeatingCoil().clone(model).cast<HVACComponent>();
+      auto supplementalHeatingCoilClone = this->supplementalHeatingCoil().clone(model).cast<HVACComponent>();
 
       //pthpClone.setAvailabilitySchedule(scheduleClone);
 
@@ -128,12 +128,13 @@ namespace model {
     std::vector<ScheduleTypeKey> ZoneHVACPackagedTerminalHeatPump_Impl::getScheduleTypeKeys(const Schedule& schedule) const {
       std::vector<ScheduleTypeKey> result;
       UnsignedVector fieldIndices = getSourceIndices(schedule.handle());
-      UnsignedVector::const_iterator b(fieldIndices.begin()), e(fieldIndices.end());
+      UnsignedVector::const_iterator b(fieldIndices.begin());
+      UnsignedVector::const_iterator e(fieldIndices.end());
       if (std::find(b, e, OS_ZoneHVAC_PackagedTerminalHeatPumpFields::AvailabilityScheduleName) != e) {
-        result.push_back(ScheduleTypeKey("ZoneHVACPackagedTerminalHeatPump", "Availability"));
+        result.emplace_back("ZoneHVACPackagedTerminalHeatPump", "Availability");
       }
       if (std::find(b, e, OS_ZoneHVAC_PackagedTerminalHeatPumpFields::SupplyAirFanOperatingModeScheduleName) != e) {
-        result.push_back(ScheduleTypeKey("ZoneHVACPackagedTerminalHeatPump", "Supply Air Fan Operating Mode"));
+        result.emplace_back("ZoneHVACPackagedTerminalHeatPump", "Supply Air Fan Operating Mode");
       }
       return result;
     }
@@ -171,7 +172,7 @@ namespace model {
         LOG(Error, "Required availability schedule not set, using 'Always On' schedule");
         value = this->model().alwaysOnDiscreteSchedule();
         OS_ASSERT(value);
-        const_cast<ZoneHVACPackagedTerminalHeatPump_Impl*>(this)->setAvailabilitySchedule(*value);
+        const_cast<ZoneHVACPackagedTerminalHeatPump_Impl*>(this)->setAvailabilitySchedule(*value);  // NOLINT
         value = optionalAvailabilitySchedule();
       }
       OS_ASSERT(value);
@@ -360,9 +361,19 @@ namespace model {
       return isEmpty(OS_ZoneHVAC_PackagedTerminalHeatPumpFields::FanPlacement);
     }
 
-    boost::optional<Schedule> ZoneHVACPackagedTerminalHeatPump_Impl::supplyAirFanOperatingModeSchedule() const {
-      return getObject<ModelObject>().getModelObjectTarget<Schedule>(
-        OS_ZoneHVAC_PackagedTerminalHeatPumpFields::SupplyAirFanOperatingModeScheduleName);
+    Schedule ZoneHVACPackagedTerminalHeatPump_Impl::supplyAirFanOperatingModeSchedule() const {
+      boost::optional<Schedule> value = optionalSupplyAirFanOperatingModeSchedule();
+      if (!value) {
+        // it is an error if we get here, however we don't want to crash
+        // so we hook up to global always on schedule
+        LOG(Error, "Required availability schedule not set, using 'Always Off' schedule");
+        value = this->model().alwaysOffDiscreteSchedule();
+        OS_ASSERT(value);
+        const_cast<ZoneHVACPackagedTerminalHeatPump_Impl*>(this)->setSupplyAirFanOperatingModeSchedule(*value);  // NOLINT
+        value = optionalSupplyAirFanOperatingModeSchedule();
+      }
+      OS_ASSERT(value);
+      return value.get();
     }
 
     bool ZoneHVACPackagedTerminalHeatPump_Impl::setAvailabilitySchedule(Schedule& schedule) {
@@ -371,12 +382,12 @@ namespace model {
       return result;
     }
 
-    bool ZoneHVACPackagedTerminalHeatPump_Impl::setOutdoorAirMixerObjectType(std::string outdoorAirMixerObjectType) {
+    bool ZoneHVACPackagedTerminalHeatPump_Impl::setOutdoorAirMixerObjectType(const std::string& outdoorAirMixerObjectType) {
       bool result = setString(OS_ZoneHVAC_PackagedTerminalHeatPumpFields::OutdoorAirMixerObjectType, outdoorAirMixerObjectType);
       return result;
     }
 
-    bool ZoneHVACPackagedTerminalHeatPump_Impl::setOutdoorAirMixerName(std::string outdoorAirMixerName) {
+    bool ZoneHVACPackagedTerminalHeatPump_Impl::setOutdoorAirMixerName(const std::string& outdoorAirMixerName) {
       bool result = setString(OS_ZoneHVAC_PackagedTerminalHeatPumpFields::OutdoorAirMixerName, outdoorAirMixerName);
       OS_ASSERT(result);
       return result;
@@ -609,7 +620,7 @@ namespace model {
       OS_ASSERT(result);
     }
 
-    bool ZoneHVACPackagedTerminalHeatPump_Impl::setFanPlacement(std::string fanPlacement) {
+    bool ZoneHVACPackagedTerminalHeatPump_Impl::setFanPlacement(const std::string& fanPlacement) {
       bool result = setString(OS_ZoneHVAC_PackagedTerminalHeatPumpFields::FanPlacement, fanPlacement);
       return result;
     }
@@ -625,11 +636,6 @@ namespace model {
       return result;
     }
 
-    void ZoneHVACPackagedTerminalHeatPump_Impl::resetSupplyAirFanOperatingModeSchedule() {
-      bool result = setString(OS_ZoneHVAC_PackagedTerminalHeatPumpFields::SupplyAirFanOperatingModeScheduleName, "");
-      OS_ASSERT(result);
-    }
-
     boost::optional<Schedule> ZoneHVACPackagedTerminalHeatPump_Impl::optionalAvailabilitySchedule() const {
       return getObject<ModelObject>().getModelObjectTarget<Schedule>(OS_ZoneHVAC_PackagedTerminalHeatPumpFields::AvailabilityScheduleName);
     }
@@ -646,6 +652,11 @@ namespace model {
       return getObject<ModelObject>().getModelObjectTarget<HVACComponent>(OS_ZoneHVAC_PackagedTerminalHeatPumpFields::SupplementalHeatingCoilName);
     }
 
+    boost::optional<Schedule> ZoneHVACPackagedTerminalHeatPump_Impl::optionalSupplyAirFanOperatingModeSchedule() const {
+      return getObject<ModelObject>().getModelObjectTarget<Schedule>(
+        OS_ZoneHVAC_PackagedTerminalHeatPumpFields::SupplyAirFanOperatingModeScheduleName);
+    }
+
     boost::optional<ModelObject> ZoneHVACPackagedTerminalHeatPump_Impl::availabilityScheduleAsModelObject() const {
       OptionalModelObject result = availabilitySchedule();
       return result;
@@ -741,13 +752,9 @@ namespace model {
         if (intermediate) {
           Schedule schedule(*intermediate);
           return setSupplyAirFanOperatingModeSchedule(schedule);
-        } else {
-          return false;
         }
-      } else {
-        resetSupplyAirFanOperatingModeSchedule();
       }
-      return true;
+      return false;
     }
 
     boost::optional<double> ZoneHVACPackagedTerminalHeatPump_Impl::autosizedSupplyAirFlowRateDuringCoolingOperation() const {
@@ -844,6 +851,16 @@ namespace model {
     setHeatingCoil(heatingCoil);
     setCoolingCoil(coolingCoil);
     setSupplementalHeatingCoil(supplementalHeatingCoil);
+    // When Blank, E+ defaults to 0 (cycling). If it's a FanConstantVolume though, you must provide an always On or E+ will Fatal.
+    if (supplyAirFan.iddObjectType() == IddObjectType::OS_Fan_ConstantVolume) {
+      auto alwaysOn = model.alwaysOnDiscreteSchedule();
+      ok = setSupplyAirFanOperatingModeSchedule(alwaysOn);
+      OS_ASSERT(ok);
+    } else {
+      auto alwaysOff = model.alwaysOffDiscreteSchedule();
+      ok = setSupplyAirFanOperatingModeSchedule(alwaysOff);
+      OS_ASSERT(ok);
+    }
 
     autosizeSupplyAirFlowRateDuringCoolingOperation();
     autosizeSupplyAirFlowRateDuringHeatingOperation();
@@ -980,11 +997,11 @@ namespace model {
     return getImpl<detail::ZoneHVACPackagedTerminalHeatPump_Impl>()->isFanPlacementDefaulted();
   }
 
-  bool ZoneHVACPackagedTerminalHeatPump::setOutdoorAirMixerObjectType(std::string outdoorAirMixerObjectType) {
+  bool ZoneHVACPackagedTerminalHeatPump::setOutdoorAirMixerObjectType(const std::string& outdoorAirMixerObjectType) {
     return getImpl<detail::ZoneHVACPackagedTerminalHeatPump_Impl>()->setOutdoorAirMixerObjectType(outdoorAirMixerObjectType);
   }
 
-  bool ZoneHVACPackagedTerminalHeatPump::setOutdoorAirMixerName(std::string outdoorAirMixerName) {
+  bool ZoneHVACPackagedTerminalHeatPump::setOutdoorAirMixerName(const std::string& outdoorAirMixerName) {
     return getImpl<detail::ZoneHVACPackagedTerminalHeatPump_Impl>()->setOutdoorAirMixerName(outdoorAirMixerName);
   }
 
@@ -1097,7 +1114,7 @@ namespace model {
     getImpl<detail::ZoneHVACPackagedTerminalHeatPump_Impl>()->resetMaximumOutdoorDryBulbTemperatureforSupplementalHeaterOperation();
   }
 
-  bool ZoneHVACPackagedTerminalHeatPump::setFanPlacement(std::string fanPlacement) {
+  bool ZoneHVACPackagedTerminalHeatPump::setFanPlacement(const std::string& fanPlacement) {
     return getImpl<detail::ZoneHVACPackagedTerminalHeatPump_Impl>()->setFanPlacement(fanPlacement);
   }
 
@@ -1113,10 +1130,6 @@ namespace model {
     return getImpl<detail::ZoneHVACPackagedTerminalHeatPump_Impl>()->setSupplyAirFanOperatingModeSchedule(schedule);
   }
 
-  void ZoneHVACPackagedTerminalHeatPump::resetSupplyAirFanOperatingModeSchedule() {
-    getImpl<detail::ZoneHVACPackagedTerminalHeatPump_Impl>()->resetSupplyAirFanOperatingModeSchedule();
-  }
-
   bool ZoneHVACPackagedTerminalHeatPump::setSupplyAirFan(HVACComponent& hvacComponent) {
     return getImpl<detail::ZoneHVACPackagedTerminalHeatPump_Impl>()->setSupplyAirFan(hvacComponent);
   }
@@ -1137,7 +1150,7 @@ namespace model {
     return getImpl<detail::ZoneHVACPackagedTerminalHeatPump_Impl>()->availabilitySchedule();
   }
 
-  boost::optional<Schedule> ZoneHVACPackagedTerminalHeatPump::supplyAirFanOperatingModeSchedule() const {
+  Schedule ZoneHVACPackagedTerminalHeatPump::supplyAirFanOperatingModeSchedule() const {
     return getImpl<detail::ZoneHVACPackagedTerminalHeatPump_Impl>()->supplyAirFanOperatingModeSchedule();
   }
 
diff --git a/src/model/ZoneHVACPackagedTerminalHeatPump.hpp b/src/model/ZoneHVACPackagedTerminalHeatPump.hpp
index 9c01ef8540e..401a76e20ce 100644
--- a/src/model/ZoneHVACPackagedTerminalHeatPump.hpp
+++ b/src/model/ZoneHVACPackagedTerminalHeatPump.hpp
@@ -57,7 +57,7 @@ namespace model {
     ZoneHVACPackagedTerminalHeatPump(const Model& model, Schedule& availabilitySchedule, HVACComponent& supplyAirFan, HVACComponent& heatingCoil,
                                      HVACComponent& coolingCoil, HVACComponent& supplementalHeatingCoil);
 
-    virtual ~ZoneHVACPackagedTerminalHeatPump() {}
+    virtual ~ZoneHVACPackagedTerminalHeatPump() = default;
 
     //@}
 
@@ -145,7 +145,7 @@ namespace model {
 
     bool isFanPlacementDefaulted() const;
 
-    boost::optional<Schedule> supplyAirFanOperatingModeSchedule() const;
+    Schedule supplyAirFanOperatingModeSchedule() const;
 
     //@}
     /** @name Setters */
@@ -153,9 +153,9 @@ namespace model {
 
     bool setAvailabilitySchedule(Schedule& schedule);
 
-    bool setOutdoorAirMixerObjectType(std::string outdoorAirMixerObjectType);
+    bool setOutdoorAirMixerObjectType(const std::string& outdoorAirMixerObjectType);
 
-    bool setOutdoorAirMixerName(std::string outdoorAirMixerName);
+    bool setOutdoorAirMixerName(const std::string& outdoorAirMixerName);
 
     bool setSupplyAirFlowRateDuringCoolingOperation(double supplyAirFlowRateDuringCoolingOperation);
 
@@ -214,7 +214,7 @@ namespace model {
 
     void resetMaximumOutdoorDryBulbTemperatureforSupplementalHeaterOperation();
 
-    bool setFanPlacement(std::string fanPlacement);
+    bool setFanPlacement(const std::string& fanPlacement);
 
     void resetFanPlacement();
 
@@ -223,8 +223,6 @@ namespace model {
    *  fan continuously. */
     bool setSupplyAirFanOperatingModeSchedule(Schedule& schedule);
 
-    void resetSupplyAirFanOperatingModeSchedule();
-
     //@}
     /** @name Other */
     //@{
@@ -246,7 +244,7 @@ namespace model {
     //@}
    protected:
     /// @cond
-    typedef detail::ZoneHVACPackagedTerminalHeatPump_Impl ImplType;
+    using ImplType = detail::ZoneHVACPackagedTerminalHeatPump_Impl;
 
     explicit ZoneHVACPackagedTerminalHeatPump(std::shared_ptr<detail::ZoneHVACPackagedTerminalHeatPump_Impl> impl);
 
@@ -261,10 +259,10 @@ namespace model {
   };
 
   /** \relates ZoneHVACPackagedTerminalHeatPump*/
-  typedef boost::optional<ZoneHVACPackagedTerminalHeatPump> OptionalZoneHVACPackagedTerminalHeatPump;
+  using OptionalZoneHVACPackagedTerminalHeatPump = boost::optional<ZoneHVACPackagedTerminalHeatPump>;
 
   /** \relates ZoneHVACPackagedTerminalHeatPump*/
-  typedef std::vector<ZoneHVACPackagedTerminalHeatPump> ZoneHVACPackagedTerminalHeatPumpVector;
+  using ZoneHVACPackagedTerminalHeatPumpVector = std::vector<ZoneHVACPackagedTerminalHeatPump>;
 
 }  // namespace model
 }  // namespace openstudio
diff --git a/src/model/ZoneHVACPackagedTerminalHeatPump_Impl.hpp b/src/model/ZoneHVACPackagedTerminalHeatPump_Impl.hpp
index f24120de566..ecd38604e7d 100644
--- a/src/model/ZoneHVACPackagedTerminalHeatPump_Impl.hpp
+++ b/src/model/ZoneHVACPackagedTerminalHeatPump_Impl.hpp
@@ -57,7 +57,7 @@ namespace model {
 
       ZoneHVACPackagedTerminalHeatPump_Impl(const ZoneHVACPackagedTerminalHeatPump_Impl& other, Model_Impl* model, bool keepHandle);
 
-      virtual ~ZoneHVACPackagedTerminalHeatPump_Impl() {}
+      virtual ~ZoneHVACPackagedTerminalHeatPump_Impl() = default;
 
       //@}
       /** @name Virtual Methods */
@@ -143,7 +143,7 @@ namespace model {
 
       bool isFanPlacementDefaulted() const;
 
-      boost::optional<Schedule> supplyAirFanOperatingModeSchedule() const;
+      Schedule supplyAirFanOperatingModeSchedule() const;
 
       boost::optional<double> autosizedSupplyAirFlowRateDuringCoolingOperation() const;
 
@@ -169,9 +169,9 @@ namespace model {
 
       bool setAvailabilitySchedule(Schedule& schedule);
 
-      bool setOutdoorAirMixerObjectType(std::string outdoorAirMixerObjectType);
+      bool setOutdoorAirMixerObjectType(const std::string& outdoorAirMixerObjectType);
 
-      bool setOutdoorAirMixerName(std::string outdoorAirMixerName);
+      bool setOutdoorAirMixerName(const std::string& outdoorAirMixerName);
 
       bool setSupplyAirFlowRateDuringCoolingOperation(boost::optional<double> supplyAirFlowRateDuringCoolingOperation);
 
@@ -229,14 +229,12 @@ namespace model {
 
       void resetMaximumOutdoorDryBulbTemperatureforSupplementalHeaterOperation();
 
-      bool setFanPlacement(std::string fanPlacement);
+      bool setFanPlacement(const std::string& fanPlacement);
 
       void resetFanPlacement();
 
       bool setSupplyAirFanOperatingModeSchedule(Schedule& schedule);
 
-      void resetSupplyAirFanOperatingModeSchedule();
-
       //@}
       /** @name Other */
       //@{
@@ -252,6 +250,7 @@ namespace model {
       boost::optional<HVACComponent> optionalHeatingCoil() const;
       boost::optional<HVACComponent> optionalCoolingCoil() const;
       boost::optional<HVACComponent> optionalSupplementalHeatingCoil() const;
+      boost::optional<Schedule> optionalSupplyAirFanOperatingModeSchedule() const;
 
       boost::optional<ModelObject> availabilityScheduleAsModelObject() const;
       boost::optional<ModelObject> supplyAirFanAsModelObject() const;
diff --git a/src/osversion/VersionTranslator.cpp b/src/osversion/VersionTranslator.cpp
index 2e4a0eeea58..5e540275475 100644
--- a/src/osversion/VersionTranslator.cpp
+++ b/src/osversion/VersionTranslator.cpp
@@ -63,9 +63,11 @@
 
 #include <OpenStudio.hxx>
 
-#include <thread>
+#include <algorithm>
+#include <iterator>
 #include <map>
 #include <sstream>
+#include <thread>
 
 #include <boost/regex.hpp>
 #include <boost/lexical_cast.hpp>
@@ -6920,6 +6922,99 @@ namespace osversion {
     IdfFile targetIdf(idd_3_5_0.iddFile());
     ss << targetIdf.versionObject().get();
 
+    // ZoneHVAC:Packaged AirConditionner / HeatPump: prescan
+    // In E+ 22.1.0, you could have a Packaged system with a Fan:ConstantVolume and a blank schedule for Supply Air Fan Operating Mode Schedule Name
+    // and it would behave like a cycling fan (which it shouldn't have done). This is now disallowed in E+ 22.2.0, and you also cannot set a Schedule
+    // with 0 values with a Fan:ConstantVolume. So we must replace these fans with a Fan:SystemModel (that mimics a Fan:OnOff really, just planning
+    // for the future + avoid having to create 2 curves for FanOnOff)
+    std::vector<std::string> packagedFanCVHandleStrs;
+    {
+      std::vector<IdfObject> fanCVs = idf_3_4_0.getObjectsByType(idf_3_4_0.iddFile().getObject("OS:Fan:ConstantVolume").get());
+      std::vector<std::string> fanCVHandleStrs;
+      fanCVHandleStrs.reserve(fanCVs.size());
+      packagedFanCVHandleStrs.reserve(fanCVs.size());
+      std::transform(fanCVs.cbegin(), fanCVs.cend(), std::back_inserter(fanCVHandleStrs),
+                     [](const auto& idfObject) { return idfObject.getString(0).get(); });
+      for (const auto& ptac : idf_3_4_0.getObjectsByType(idf_3_4_0.iddFile().getObject("OS:ZoneHVAC:PackagedTerminalAirConditioner").get())) {
+        // If the Supply Air Fan Operating Mode Schedule is empty
+        if (ptac.isEmpty(17)) {
+          if (auto fanHandleStr_ = ptac.getString(13, false, true)) {
+            if (std::find(fanCVHandleStrs.cbegin(), fanCVHandleStrs.cend(), fanHandleStr_.get()) != fanCVHandleStrs.cend()) {
+              packagedFanCVHandleStrs.push_back(fanHandleStr_.get());
+            }
+          }
+        }
+      }
+      for (const auto& pthp : idf_3_4_0.getObjectsByType(idf_3_4_0.iddFile().getObject("OS:ZoneHVAC:PackagedTerminalHeatPump").get())) {
+        // If the Supply Air Fan Operating Mode Schedule is empty
+        if (pthp.isEmpty(23)) {
+          if (auto fanName_ = pthp.getString(13, false, true)) {
+            if (std::find(fanCVHandleStrs.cbegin(), fanCVHandleStrs.cend(), fanName_.get()) != fanCVHandleStrs.cend()) {
+              packagedFanCVHandleStrs.push_back(fanName_.get());
+            }
+          }
+        }
+      }
+    }
+
+    std::string alwaysOnDiscreteScheduleHandleStr;
+    std::string alwaysOffDiscreteScheduleHandleStr;
+
+    auto getOrCreateAlwaysDiscreteScheduleHandleStr = [this, &ss, &idf_3_4_0, &idd_3_5_0, &alwaysOnDiscreteScheduleHandleStr,
+                                                       &alwaysOffDiscreteScheduleHandleStr](bool isAlwaysOn) -> std::string {
+      auto& discreteSchHandleStr = isAlwaysOn ? alwaysOnDiscreteScheduleHandleStr : alwaysOffDiscreteScheduleHandleStr;
+      if (!discreteSchHandleStr.empty()) {
+        return discreteSchHandleStr;
+      }
+      std::string name = isAlwaysOn ? "Always On Discrete" : "Always Off Discrete";
+      double val = isAlwaysOn ? 1.0 : 0.0;
+      // Add an alwaysOnDiscreteSchedule if one does not already exist
+      for (const IdfObject& object : idf_3_4_0.getObjectsByType(idf_3_4_0.iddFile().getObject("OS:Schedule:Constant").get())) {
+        if (boost::optional<std::string> name_ = object.getString(1)) {
+          if (istringEqual(name_.get(), name)) {
+            if (boost::optional<double> value = object.getDouble(3)) {
+              if (equal<double>(value.get(), val)) {
+                discreteSchHandleStr = object.getString(0).get();
+                break;
+              }
+            }
+          }
+        }
+      }
+
+      if (discreteSchHandleStr.empty()) {
+        auto discreteSch = IdfObject(idd_3_5_0.getObject("OS:Schedule:Constant").get());
+
+        discreteSchHandleStr = toString(createUUID());
+        discreteSch.setString(0, discreteSchHandleStr);
+        discreteSch.setString(1, name);
+        discreteSch.setDouble(3, val);
+
+        IdfObject typeLimits(idd_3_5_0.getObject("OS:ScheduleTypeLimits").get());
+        typeLimits.setString(0, toString(createUUID()));
+        typeLimits.setString(1, name + " Limits");
+        typeLimits.setDouble(2, 0.0);
+        typeLimits.setDouble(3, 1.0);
+        typeLimits.setString(4, "Discrete");
+        typeLimits.setString(5, "Availability");
+
+        discreteSch.setString(2, typeLimits.getString(0).get());
+
+        ss << discreteSch;
+        ss << typeLimits;
+
+        // Register new objects
+        m_new.push_back(discreteSch);
+        m_new.push_back(typeLimits);
+      }
+
+      return discreteSchHandleStr;
+    };
+
+    if (!packagedFanCVHandleStrs.empty()) {
+      alwaysOffDiscreteScheduleHandleStr = getOrCreateAlwaysDiscreteScheduleHandleStr(false);
+    }  // End locating or creating alwaysOnDiscreteSchedule
+
     for (const IdfObject& object : idf_3_4_0.objects()) {
       auto iddname = object.iddObject().name();
 
@@ -7339,50 +7434,171 @@ namespace osversion {
         // ------------------------------------------------
         // * Availability Schedule * 2
 
-        auto iddObject = idd_3_5_0.getObject(iddname);
-        IdfObject newObject(iddObject.get());
+        if (!object.isEmpty(2)) {
+          ss << object;
+        } else {
 
-        if (object.isEmpty(2)) {
-          // Add an always on discrete schedule if one does not already exist
-          boost::optional<IdfObject> alwaysOnSchedule;
-          for (const IdfObject& object : idf_3_4_0.objects()) {
-            if (object.iddObject().name() == "OS:Schedule:Constant") {
-              if (boost::optional<std::string> name = object.getString(1)) {
-                if (istringEqual(name.get(), "Always On Discrete")) {
-                  if (boost::optional<double> value = object.getDouble(3)) {
-                    if (equal<double>(value.get(), 1.0)) {
-                      alwaysOnSchedule = object;
-                    }
-                  }
-                }
-              }
+          auto iddObject = idd_3_5_0.getObject(iddname);
+          IdfObject newObject(iddObject.get());
+          for (size_t i = 0; i < object.numFields(); ++i) {
+            if ((value = object.getString(i))) {
+              newObject.setString(i, value.get());
+            }
+          }
+          newObject.setString(2, getOrCreateAlwaysDiscreteScheduleHandleStr(true));
+
+          m_refactored.push_back(RefactoredObjectData(object, newObject));
+          ss << newObject;
+        }
+
+      } else if (iddname == "OS:ZoneHVAC:PackagedTerminalHeatPump") {
+
+        if (!object.isEmpty(23)) {
+          ss << object;
+
+        } else {
+          auto iddObject = idd_3_5_0.getObject(iddname);
+          IdfObject newObject(iddObject.get());
+
+          for (size_t i = 0; i < object.numFields(); ++i) {
+            if ((value = object.getString(i))) {
+              newObject.setString(i, value.get());
             }
           }
+          // alwaysOffDiscreteScheduleHandleStr has already been initialized above
+          newObject.setString(23, alwaysOffDiscreteScheduleHandleStr);
+
+          m_refactored.push_back(RefactoredObjectData(object, newObject));
+          ss << newObject;
+        }
+      } else if (iddname == "OS:ZoneHVAC:PackagedTerminalAirConditioner") {
+
+        if (!object.isEmpty(17)) {
+          ss << object;
+
+        } else {
+          auto iddObject = idd_3_5_0.getObject(iddname);
+          IdfObject newObject(iddObject.get());
 
-          if (!alwaysOnSchedule) {
-            alwaysOnSchedule = IdfObject(idd_3_5_0.getObject("OS:Schedule:Constant").get());
-            alwaysOnSchedule->setString(0, toString(createUUID()));
-            alwaysOnSchedule->setString(1, "Always On Discrete");
-            alwaysOnSchedule->setDouble(3, 1.0);
-            IdfObject typeLimits(idd_3_5_0.getObject("OS:ScheduleTypeLimits").get());
-            typeLimits.setString(0, toString(createUUID()));
-            typeLimits.setString(1, "Always On Discrete Limits");
-            typeLimits.setDouble(2, 0.0);
-            typeLimits.setDouble(3, 1.0);
-            typeLimits.setString(4, "Discrete");
-            typeLimits.setString(5, "Availability");
-            alwaysOnSchedule->setString(2, typeLimits.getString(0).get());
-            ss << alwaysOnSchedule.get();
-            ss << typeLimits;
-            m_new.push_back(alwaysOnSchedule.get());
-            m_new.push_back(typeLimits);
+          for (size_t i = 0; i < object.numFields(); ++i) {
+            if ((value = object.getString(i))) {
+              newObject.setString(i, value.get());
+            }
           }
+          // alwaysOffDiscreteScheduleHandleStr has already been initialized above
+          newObject.setString(17, alwaysOffDiscreteScheduleHandleStr);
 
-          newObject.setString(2, alwaysOnSchedule->getString(0).get());
+          m_refactored.push_back(RefactoredObjectData(object, newObject));
+          ss << newObject;
         }
 
-        m_refactored.push_back(RefactoredObjectData(object, newObject));
-        ss << newObject;
+      } else if (iddname == "OS:Fan:ConstantVolume") {
+        std::string fanHandleStr = object.getString(0).get();
+        if (std::find(packagedFanCVHandleStrs.cbegin(), packagedFanCVHandleStrs.cend(), fanHandleStr) == packagedFanCVHandleStrs.cend()) {
+          ss << object;
+        } else {
+          LOG(Warn, "Fan:ConstantVolume "
+                      << object.nameString()
+                      << " is used in a Packaged System (PTAC or PTHP) that does not have a Supply Air Fan Operating Mode Schedule. "
+                         "In 22.1.0 this would effectively, and mistakenly, function as a cycling fan, but this is now disallowed in E+ 22.2.0. "
+                         "In order to retain a similar functionality and energy usage, this will be replaced by a Fan:SystemModel "
+                         "with an always Off Schedule (cycling fan, similar to a Fan:OnOff)");
+          IdfObject newObject(idd_3_5_0.getObject("OS:Fan:SystemModel").get());
+          // Handle
+          if ((value = object.getString(0))) {
+            newObject.setString(0, value.get());
+          }
+
+          // Name
+          if ((value = object.getString(1))) {
+            newObject.setString(1, value.get());
+          }
+
+          // Availability Schedule Name
+          if ((value = object.getString(2))) {
+            newObject.setString(2, value.get());
+          }
+
+          // Note the use of "true" to return default. Fan:ConstantVolume has way more defaulted fields, while Fan:SystemModel adheres to the newer
+          // "make \required-field and hardcode in ctor" doctrine, AND has different defaults than Fan:ConstantVolume. So we do this to fulfil two
+          // goals: Put a value in all required-fields AND set the same values
+          // Fan Total Efficiency
+          if ((value = object.getString(3, true))) {
+            newObject.setString(15, value.get());
+          }
+
+          // Pressure Rise
+          if ((value = object.getString(4, true))) {
+            newObject.setString(8, value.get());
+          }
+
+          // Maximum Flow Rate
+          if ((value = object.getString(5, true))) {
+            newObject.setString(5, value.get());
+          }
+
+          // Motor Efficiency
+          if ((value = object.getString(6, true))) {
+            newObject.setString(9, value.get());
+          }
+
+          // Motor In Airstream Fraction
+          if ((value = object.getString(7, true))) {
+            newObject.setString(10, value.get());
+          }
+
+          // Nodes should be blank since it's inside a containing ZoneHVAC
+          // Air Inlet Node Name
+          if ((value = object.getString(8))) {
+            newObject.setString(3, value.get());
+          }
+
+          // Air Outlet Node Name
+          if ((value = object.getString(9))) {
+            newObject.setString(4, value.get());
+          }
+
+          // End-Use Subcategory
+          if ((value = object.getString(10, true))) {
+            newObject.setString(21, value.get());
+          }
+
+          // Speed Control Method
+          newObject.setString(6, "Discrete");
+
+          // Electric Power Minimum Flow Rate Fraction
+          newObject.setDouble(7, 0.0);
+
+          // Design Electric Power Consumption
+          newObject.setString(11, "Autosize");
+
+          // Design Power Sizing Method
+          newObject.setString(12, "TotalEfficiencyAndPressure");
+
+          // Electric Power Per Unit Flow Rate (not used given Power Sizing Method, but required-field & set in Ctor)
+          newObject.setDouble(13, 840.0);
+
+          // Electric Power Per Unit Flow Rate Per Unit Pressure (not used given Power Sizing Method, but required-field & set in Ctor)
+          newObject.setDouble(14, 1.66667);
+
+          // Electric Power Function of Flow Fraction Curve Name
+          newObject.setString(16, "");
+
+          // Night Ventilation Mode Pressure Rise
+          newObject.setString(17, "");
+
+          // Night Ventilation Mode Flow Fraction
+          newObject.setString(18, "");
+
+          // Motor Loss Zone Name
+          newObject.setString(19, "");
+
+          // Motor Loss Radiative Fraction
+          newObject.setDouble(20, 0.0);
+
+          m_refactored.push_back(RefactoredObjectData(object, newObject));
+          ss << newObject;
+        }
 
         // No-op
       } else {
diff --git a/src/scriptengine/ScriptEngine.hpp b/src/scriptengine/ScriptEngine.hpp
index e8c4e7e058a..2541ae4963f 100644
--- a/src/scriptengine/ScriptEngine.hpp
+++ b/src/scriptengine/ScriptEngine.hpp
@@ -51,6 +51,13 @@ class ScriptEngine
   // execute string without expecting a return value
   virtual void exec(std::string_view sv) = 0;
 
+  // TODO: this is totally the wrong place to put it, but I'm trying to see if it works
+  virtual void setupEmbeddedGems(const std::vector<openstudio::path>& includeDirs, const std::vector<openstudio::path>& gemPathDirs,
+                                 const openstudio::path& gemHomeDir, const openstudio::path& bundleGemFilePath,
+                                 const openstudio::path& bundleGemDirPath, const std::string& bundleWithoutGroups){};
+
+  virtual void setupPythonPath(const std::vector<openstudio::path>& includeDirs, const openstudio::path& pythonHomeDir){};
+
   template <typename T>
   T getAs(ScriptObject& obj) {
     void* result = getAs_impl(obj, typeid(T));
diff --git a/src/utilities/core/ApplicationPathHelpers.cxx.in b/src/utilities/core/ApplicationPathHelpers.cxx.in
index fbb01556dd0..0619d383284 100644
--- a/src/utilities/core/ApplicationPathHelpers.cxx.in
+++ b/src/utilities/core/ApplicationPathHelpers.cxx.in
@@ -41,443 +41,441 @@
 #include <iostream>
 
 #if defined _WIN32
-#include <windows.h>
+#  include <windows.h>
 #else
-#include <stdio.h>
-#include <dlfcn.h>
+#  include <stdio.h>
+#  include <dlfcn.h>
 #endif
 
-
 namespace openstudio {
 
-  openstudio::path getApplicationSourceDirectory() {
-    return openstudio::toPath("${CMAKE_SOURCE_DIR}");
+openstudio::path getApplicationSourceDirectory() {
+  return openstudio::toPath("${CMAKE_SOURCE_DIR}");
+}
+
+openstudio::path getApplicationBuildDirectory() {
+  return openstudio::toPath("${CMAKE_BINARY_DIR}");
+}
+
+openstudio::path getApplicationPath() {
+  openstudio::path path;
+#if defined _WIN32
+  TCHAR szPath[MAX_PATH];
+  if (GetModuleFileName(nullptr, szPath, MAX_PATH)) {
+    LOG_FREE(Debug, "ApplicationPathHelpers", "getApplicationPath, szPath = '" << szPath << "'");
+    path = toPath(szPath);
+  }
+#else
+  Dl_info info;
+  if (dladdr("main", &info)) {
+    LOG_FREE(Debug, "ApplicationPathHelpers", "getApplicationPath, info.dli_fname = '" << info.dli_fname << "'");
+    path = toPath(info.dli_fname);
   }
+#endif
 
-  openstudio::path getApplicationBuildDirectory() {
-    return openstudio::toPath("${CMAKE_BINARY_DIR}");
+  // Note JM 2019-04-24:
+  // Let's call assume the GetModuleFileName (windows, though that doesn't really happen)
+  // or (more likely) dladdr (Unix) returns just 'OpenStudioApplicationSymlink' (and not '/path/to/OpenStudioApplicationSymlink' or './OpenStudioApplicationSymlink')
+  // You first try to locate that OpenStudioApplicationSymlink in the current directory
+
+  // (Another option would be to always start by calling findInSystemPath and modify the findInSystemPath to prepend the PATH list with '.')
+
+  openstudio::path applicationPath = completeAndNormalize(path);
+  if (!openstudio::filesystem::exists(applicationPath) || openstudio::filesystem::is_directory(applicationPath)) {
+    // It that didn't work, it means it's in the system PATH. So you have to look for it in your PATH by calling findInSystemPath
+    // There's no way around it IMHO.
+    // Note: This wouldn't be a concern unless you are launching a symlink that lives in your PATH, and outside the directory where the symlink is
+    // * ./OpenStudioApplicationSymlink will work
+    // * /home/OpenStudioApplicationSymlink will work
+    // * OpenStudioApplicationSymlink itself will not work (if you aren't in the same folder as the symlink)
+
+    LOG_FREE(Debug, "ApplicationPathHelpers", "getApplicationPath failed as is, trying in to locate in systemPath");
+    path = findInSystemPath(path);
+    applicationPath = completeAndNormalize(path);
+  }
+  if (!openstudio::filesystem::exists(applicationPath) || openstudio::filesystem::is_directory(applicationPath)) {
+    LOG_FREE_AND_THROW("ApplicationPathHelpers", "getApplicationPath failed!");
   }
 
-  openstudio::path getApplicationPath() {
-    openstudio::path path;
-    #if defined _WIN32
-      TCHAR szPath[MAX_PATH];
-      if( GetModuleFileName( nullptr, szPath, MAX_PATH ) ) {
-        LOG_FREE(Debug, "ApplicationPathHelpers", "getApplicationPath, szPath = '" << szPath << "'");
-        path = toPath(szPath);
-      }
-    #else
-      Dl_info info;
-      if (dladdr("main", &info)) {
-        LOG_FREE(Debug, "ApplicationPathHelpers", "getApplicationPath, info.dli_fname = '" << info.dli_fname << "'");
-        path = toPath(info.dli_fname);
-      }
-    #endif
-
-    // Note JM 2019-04-24:
-    // Let's call assume the GetModuleFileName (windows, though that doesn't really happen)
-    // or (more likely) dladdr (Unix) returns just 'OpenStudioApplicationSymlink' (and not '/path/to/OpenStudioApplicationSymlink' or './OpenStudioApplicationSymlink')
-    // You first try to locate that OpenStudioApplicationSymlink in the current directory
-
-    // (Another option would be to always start by calling findInSystemPath and modify the findInSystemPath to prepend the PATH list with '.')
-
-    openstudio::path applicationPath = completeAndNormalize(path);
-    if( !openstudio::filesystem::exists( applicationPath ) || openstudio::filesystem::is_directory( applicationPath ) ) {
-        // It that didn't work, it means it's in the system PATH. So you have to look for it in your PATH by calling findInSystemPath
-        // There's no way around it IMHO.
-        // Note: This wouldn't be a concern unless you are launching a symlink that lives in your PATH, and outside the directory where the symlink is
-        // * ./OpenStudioApplicationSymlink will work
-        // * /home/OpenStudioApplicationSymlink will work
-        // * OpenStudioApplicationSymlink itself will not work (if you aren't in the same folder as the symlink)
-
-
-        LOG_FREE(Debug, "ApplicationPathHelpers", "getApplicationPath failed as is, trying in to locate in systemPath");
-        path = findInSystemPath(path);
-        applicationPath = completeAndNormalize(path);
-    }
-    if( !openstudio::filesystem::exists( applicationPath ) || openstudio::filesystem::is_directory( applicationPath ) ) {
-        LOG_FREE_AND_THROW("ApplicationPathHelpers", "getApplicationPath failed!");
-    }
+  LOG_FREE(Debug, "ApplicationPathHelpers", "getApplicationPath, found '" << applicationPath << "'");
 
-    LOG_FREE(Debug, "ApplicationPathHelpers", "getApplicationPath, found '" << applicationPath << "'");
+  return applicationPath;
+}
 
-    return applicationPath;
+openstudio::path getApplicationDirectory() {
+  openstudio::path p = getApplicationPath();
+  if (!p.empty()) {
+    return p.parent_path();
   }
+  return openstudio::path();
+}
 
-  openstudio::path getApplicationDirectory() {
-    openstudio::path p = getApplicationPath();
-    if (!p.empty()){
-      return p.parent_path();
-    }
-    return openstudio::path();
-  }
-
-  bool applicationIsRunningFromBuildDirectory()
-  {
+bool applicationIsRunningFromBuildDirectory() {
 #ifdef _WIN32
-    openstudio::path buildDir = openstudio::toPath(boost::algorithm::to_upper_copy(openstudio::toString(getApplicationBuildDirectory())));
-    openstudio::path runDir = openstudio::toPath(boost::algorithm::to_upper_copy(openstudio::toString(getApplicationDirectory())));
+  openstudio::path buildDir = openstudio::toPath(boost::algorithm::to_upper_copy(openstudio::toString(getApplicationBuildDirectory())));
+  openstudio::path runDir = openstudio::toPath(boost::algorithm::to_upper_copy(openstudio::toString(getApplicationDirectory())));
+  std::string package_dir = "_CPACK_PACKAGES";
 #else
-    openstudio::path buildDir = getApplicationBuildDirectory();
-    openstudio::path runDir = getApplicationDirectory();
+  openstudio::path buildDir = getApplicationBuildDirectory();
+  openstudio::path runDir = getApplicationDirectory();
+  std::string package_dir = "_CPack_Packages";
 #endif
 
-    return pathBeginsWith(buildDir, runDir);
-  }
+  bool isInsideCpackDir = openstudio::toString(runDir).find(package_dir) != std::string::npos;
+  // LOG_FREE(Debug, "ApplicationPathHelpers", "buildDir=" << buildDir << ", runDir=" << runDir << ", isInsideCpackDir=" << isInsideCpackDir);
+  return pathBeginsWith(buildDir, runDir) && !isInsideCpackDir;
+}
 
-  openstudio::path getEnergyPlusDirectory() {
-    return getEnergyPlusExecutable().parent_path();
-  }
+openstudio::path getEnergyPlusDirectory() {
+  return getEnergyPlusExecutable().parent_path();
+}
+
+// define the function GetCurrentModule so we can get its address
+#if defined _WIN32
+HMODULE GetCurrentModule() {
+  HMODULE hModule = NULL;
+  // hModule is NULL if GetModuleHandleEx fails.
+  GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, (LPCTSTR)GetCurrentModule, &hModule);
+  return hModule;
+}
+#else
+bool GetCurrentModule() {
+  return true;
+}
+#endif
 
-  // define the function GetCurrentModule so we can get its address
-  #if defined _WIN32
-  HMODULE GetCurrentModule()
-  {
-      HMODULE hModule = NULL;
-      // hModule is NULL if GetModuleHandleEx fails.
-      GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS
-                          | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
-                          (LPCTSTR)GetCurrentModule, &hModule);
-      return hModule;
+openstudio::path getOpenStudioModule() {
+  openstudio::path path;
+#if defined _WIN32
+  TCHAR szPath[MAX_PATH];
+  if (GetModuleFileName(GetCurrentModule(), szPath, MAX_PATH)) {
+    LOG_FREE(Debug, "ApplicationPathHelpers", "getOpenStudioModule, szPath = '" << szPath << "'");
+    path = toPath(szPath);
   }
-  #else
-  bool GetCurrentModule()
-  {
-    return true;
+#else
+  Dl_info info;
+  if (dladdr("GetCurrentModule", &info)) {
+    LOG_FREE(Debug, "ApplicationPathHelpers", "getOpenStudioModule, info.dli_fname = '" << info.dli_fname << "'");
+    path = toPath(info.dli_fname);
   }
-  #endif
-
-  openstudio::path getOpenStudioModule()
-  {
-    openstudio::path path;
-    #if defined _WIN32
-      TCHAR szPath[MAX_PATH];
-      if( GetModuleFileName( GetCurrentModule(), szPath, MAX_PATH ) ) {
-        LOG_FREE(Debug, "ApplicationPathHelpers", "getOpenStudioModule, szPath = '" << szPath << "'");
-        path = toPath(szPath);
-      }
-    #else
-      Dl_info info;
-      if (dladdr("GetCurrentModule", &info)) {
-        LOG_FREE(Debug, "ApplicationPathHelpers", "getOpenStudioModule, info.dli_fname = '" << info.dli_fname << "'");
-        path = toPath(info.dli_fname);
-      }
-    #endif
-    openstudio::path openstudioPath = completeAndNormalize(path);
-    if( !openstudio::filesystem::exists( openstudioPath ) || openstudio::filesystem::is_directory( openstudioPath ) ) {
-        LOG_FREE(Debug, "ApplicationPathHelpers", "getOpenStudioModule failed as is, trying in to locate in systemPath");
-        path = findInSystemPath(path);
-        openstudioPath = completeAndNormalize(path);
-    }
-    if( !openstudio::filesystem::exists( openstudioPath ) || openstudio::filesystem::is_directory( openstudioPath ) ) {
-        LOG_FREE_AND_THROW("ApplicationPathHelpers", "getOpenStudioModule failed!");
-    }
-
-    LOG_FREE(Debug, "ApplicationPathHelpers", "getOpenStudioModule, found '" << openstudioPath << "'");
-    return openstudioPath;
+#endif
+  openstudio::path openstudioPath = completeAndNormalize(path);
+  if (!openstudio::filesystem::exists(openstudioPath) || openstudio::filesystem::is_directory(openstudioPath)) {
+    LOG_FREE(Debug, "ApplicationPathHelpers", "getOpenStudioModule failed as is, trying in to locate in systemPath");
+    path = findInSystemPath(path);
+    openstudioPath = completeAndNormalize(path);
   }
-
-  openstudio::path getOpenStudioModuleDirectory()
-  {
-    return getOpenStudioModule().parent_path();
+  if (!openstudio::filesystem::exists(openstudioPath) || openstudio::filesystem::is_directory(openstudioPath)) {
+    LOG_FREE_AND_THROW("ApplicationPathHelpers", "getOpenStudioModule failed!");
   }
 
-  bool moduleIsRunningFromBuildDirectory()
-  {
+  LOG_FREE(Debug, "ApplicationPathHelpers", "getOpenStudioModule, found '" << openstudioPath << "'");
+  return openstudioPath;
+}
+
+openstudio::path getOpenStudioModuleDirectory() {
+  return getOpenStudioModule().parent_path();
+}
+
+bool moduleIsRunningFromBuildDirectory() {
 #ifdef _WIN32
-    openstudio::path buildDir = openstudio::toPath(boost::algorithm::to_upper_copy(openstudio::toString(getApplicationBuildDirectory())));
-    openstudio::path runDir = openstudio::toPath(boost::algorithm::to_upper_copy(openstudio::toString(getOpenStudioModuleDirectory())));
+  openstudio::path buildDir = openstudio::toPath(boost::algorithm::to_upper_copy(openstudio::toString(getApplicationBuildDirectory())));
+  openstudio::path runDir = openstudio::toPath(boost::algorithm::to_upper_copy(openstudio::toString(getOpenStudioModuleDirectory())));
+  std::string package_dir = "_CPACK_PACKAGES";
 #else
-    openstudio::path buildDir = getApplicationBuildDirectory();
-    openstudio::path runDir = getOpenStudioModuleDirectory();
+  openstudio::path buildDir = getApplicationBuildDirectory();
+  openstudio::path runDir = getOpenStudioModuleDirectory();
+  std::string package_dir = "_CPack_Packages";
 #endif
 
-    return pathBeginsWith(buildDir, runDir);
+  bool isInsideCpackDir = openstudio::toString(runDir).find(package_dir) != std::string::npos;
+  // LOG_FREE(Debug, "ApplicationPathHelpers", "buildDir=" << buildDir << ", runDir=" << runDir << ", isInsideCpackDir=" << isInsideCpackDir);
+  return pathBeginsWith(buildDir, runDir) && !isInsideCpackDir;
+}
+
+openstudio::path getOpenStudioCLI() {
+  openstudio::path cliPath;
+#if defined _WIN32
+
+  // Windows
+  // . ( build )
+  // ├── Products
+  // │   ├── ruby
+  // │   │   ├── Debug
+  // │   │   │   └── openstudio.lib
+  // │   │   └── Release
+  // │   │       └── openstudio.lib
+  // │   ├── Debug
+  // │   │   └── openstudio.exe *
+  // │   ├── Release
+  // │   │   └── openstudio.exe *
+
+  cliPath = getOpenStudioModuleDirectory() / toPath("openstudio.exe");
+  if (!openstudio::filesystem::exists(cliPath)) {
+// Assume it's the ruby one
+#  if _DEBUG
+    cliPath = getOpenStudioModuleDirectory() / toPath("../../Debug/openstudio.exe");
+#  else
+    cliPath = getOpenStudioModuleDirectory() / toPath("../../Release/openstudio.exe");
+#  endif
   }
+#else
 
-  openstudio::path getOpenStudioCLI(){
-    openstudio::path cliPath;
-    #if defined _WIN32
-
-      // Windows
-      // . ( build )
-      // ├── Products
-      // │   ├── ruby
-      // │   │   ├── Debug
-      // │   │   │   └── openstudio.lib
-      // │   │   └── Release
-      // │   │       └── openstudio.lib
-      // │   ├── Debug
-      // │   │   └── openstudio.exe *
-      // │   ├── Release
-      // │   │   └── openstudio.exe *
-
-      cliPath = getOpenStudioModuleDirectory() / toPath("openstudio.exe");
-      if (!openstudio::filesystem::exists(cliPath)) {
-        // Assume it's the ruby one
-        #if _DEBUG
-          cliPath = getOpenStudioModuleDirectory() / toPath("../../Debug/openstudio.exe");
-        #else
-          cliPath = getOpenStudioModuleDirectory() / toPath("../../Release/openstudio.exe");
-        #endif
-      }
-    #else
-
-      // Unix
-      // . ( build )
-      // ├── Products
-      // │   ├── ruby
-      // │   │   ├── openstudio.so
-      // │   └── openstudio *
-
-      cliPath = getOpenStudioModuleDirectory() / toPath("openstudio");
-      // If it doesn't exist or it's a directory, try one level above
-      if (!openstudio::filesystem::exists(cliPath) || openstudio::filesystem::is_directory(cliPath)) {
-        cliPath = getOpenStudioModuleDirectory() / toPath("../openstudio");
-      }
-    #endif
-
-    LOG_FREE(Debug, "ApplicationPathHelpers", "Searching for CLI in '" << cliPath << "'");
-    if( openstudio::filesystem::exists(cliPath) && !openstudio::filesystem::is_directory(cliPath) ){
-      LOG_FREE(Debug, "ApplicationPathHelpers", "Found CLI at '" << completeAndNormalize(cliPath) << "'");
-      return completeAndNormalize(cliPath);
-    }
+  // Unix
+  // . ( build )
+  // ├── Products
+  // │   ├── ruby
+  // │   │   ├── openstudio.so
+  // │   └── openstudio *
+
+  cliPath = getOpenStudioModuleDirectory() / toPath("openstudio");
+  // If it doesn't exist or it's a directory, try one level above
+  if (!openstudio::filesystem::exists(cliPath) || openstudio::filesystem::is_directory(cliPath)) {
+    cliPath = getOpenStudioModuleDirectory() / toPath("../openstudio");
+  }
+#endif
 
-    // Regardless of platform, this is the installed structure
-    // . (installed)
-    // ├── bin
-    // │   ├── openstudio
-    // ├── Ruby
-    // │   └── openstudio.so
-
-    // If at this point we still haven't found it, assume it's the installed ruby that's used
-    #if defined _WIN32
-      cliPath = getOpenStudioModuleDirectory() / toPath("../bin/openstudio.exe");
-    #else
-      cliPath = getOpenStudioModuleDirectory() / toPath("../bin/openstudio");
-    #endif
-
-    LOG_FREE(Debug, "ApplicationPathHelpers", "Searching for CLI in '" << cliPath << "'");
-    if( openstudio::filesystem::exists(cliPath) && !openstudio::filesystem::is_directory(cliPath) ){
-      LOG_FREE(Debug, "ApplicationPathHelpers", "Found CLI at '" << completeAndNormalize(cliPath) << "'");
-      return completeAndNormalize(cliPath);
-    }
+  LOG_FREE(Debug, "ApplicationPathHelpers", "Searching for CLI in '" << cliPath << "'");
+  if (openstudio::filesystem::exists(cliPath) && !openstudio::filesystem::is_directory(cliPath)) {
+    LOG_FREE(Debug, "ApplicationPathHelpers", "Found CLI at '" << completeAndNormalize(cliPath) << "'");
+    return completeAndNormalize(cliPath);
+  }
+
+// Regardless of platform, this is the installed structure
+// . (installed)
+// ├── bin
+// │   ├── openstudio
+// ├── Ruby
+// │   └── openstudio.so
 
-    return openstudio::path();
+// If at this point we still haven't found it, assume it's the installed ruby that's used
+#if defined _WIN32
+  cliPath = getOpenStudioModuleDirectory() / toPath("../bin/openstudio.exe");
+#else
+  cliPath = getOpenStudioModuleDirectory() / toPath("../bin/openstudio");
+#endif
+
+  LOG_FREE(Debug, "ApplicationPathHelpers", "Searching for CLI in '" << cliPath << "'");
+  if (openstudio::filesystem::exists(cliPath) && !openstudio::filesystem::is_directory(cliPath)) {
+    LOG_FREE(Debug, "ApplicationPathHelpers", "Found CLI at '" << completeAndNormalize(cliPath) << "'");
+    return completeAndNormalize(cliPath);
   }
 
-  openstudio::path getEnergyPlusExecutable() {
+  return openstudio::path();
+}
+
+openstudio::path getEnergyPlusExecutable() {
 
-    openstudio::path cmakeEnergyPlusPath = openstudio::toPath("${ENERGYPLUS_EXE}");
-    openstudio::path cmakeEnergyPlusDirectory = cmakeEnergyPlusPath.parent_path();
+  openstudio::path cmakeEnergyPlusPath = openstudio::toPath("${ENERGYPLUS_EXE}");
+  openstudio::path cmakeEnergyPlusDirectory = cmakeEnergyPlusPath.parent_path();
 
-    // handle developer case first
-    if (moduleIsRunningFromBuildDirectory()){
-      LOG_FREE(Debug, "ApplicationPathHelpers", "Searching for EnergyPlus in '" << cmakeEnergyPlusPath << "'");
-      if( openstudio::filesystem::exists(cmakeEnergyPlusPath) ){
-        LOG_FREE(Debug, "ApplicationPathHelpers", "Found EnergyPlus at '" << completeAndNormalize(cmakeEnergyPlusPath) << "'");
-        return completeAndNormalize(cmakeEnergyPlusPath);
-      }
+  // handle developer case first
+  if (moduleIsRunningFromBuildDirectory()) {
+    LOG_FREE(Debug, "ApplicationPathHelpers", "Searching for EnergyPlus in '" << cmakeEnergyPlusPath << "'");
+    if (openstudio::filesystem::exists(cmakeEnergyPlusPath)) {
+      LOG_FREE(Debug, "ApplicationPathHelpers", "Found EnergyPlus at '" << completeAndNormalize(cmakeEnergyPlusPath) << "'");
+      return completeAndNormalize(cmakeEnergyPlusPath);
     }
+  }
 
-    // next check environment variable
-    auto envPathString = std::getenv("ENERGYPLUS_EXE_PATH");
-    if( envPathString ) {
-      openstudio::path envPath = openstudio::toPath(envPathString);
+  // next check environment variable
+  auto envPathString = std::getenv("ENERGYPLUS_EXE_PATH");
+  if (envPathString) {
+    openstudio::path envPath = openstudio::toPath(envPathString);
 
-      LOG_FREE(Debug, "ApplicationPathHelpers", "Searching for EnergyPlus in '" << envPath << "'");
-      if( openstudio::filesystem::exists(envPath) ) {
-        LOG_FREE(Debug, "ApplicationPathHelpers", "Found EnergyPlus at '" << completeAndNormalize(envPath) << "'");
-        return completeAndNormalize(envPath);
-      }
+    LOG_FREE(Debug, "ApplicationPathHelpers", "Searching for EnergyPlus in '" << envPath << "'");
+    if (openstudio::filesystem::exists(envPath)) {
+      LOG_FREE(Debug, "ApplicationPathHelpers", "Found EnergyPlus at '" << completeAndNormalize(envPath) << "'");
+      return completeAndNormalize(envPath);
     }
+  }
 
-    // next check for installed energyplus
+  // next check for installed energyplus
 #ifdef _WIN32
-    openstudio::path installPath = getOpenStudioModuleDirectory() / toPath("/../EnergyPlus/energyplus.exe");
+  openstudio::path installPath = getOpenStudioModuleDirectory() / toPath("/../EnergyPlus/energyplus.exe");
 #else
-    openstudio::path installPath = getOpenStudioModuleDirectory() / toPath("/../EnergyPlus/energyplus");
+  openstudio::path installPath = getOpenStudioModuleDirectory() / toPath("/../EnergyPlus/energyplus");
 #endif
 
-    LOG_FREE(Debug, "ApplicationPathHelpers", "Searching for EnergyPlus in '" << installPath << "'");
-    if( openstudio::filesystem::exists(installPath) && !openstudio::filesystem::is_directory(installPath) ) {
-      LOG_FREE(Debug, "ApplicationPathHelpers", "Found EnergyPlus at '" << completeAndNormalize(installPath) << "'");
-      return completeAndNormalize(installPath);
-    }
+  LOG_FREE(Debug, "ApplicationPathHelpers", "Searching for EnergyPlus in '" << installPath << "'");
+  if (openstudio::filesystem::exists(installPath) && !openstudio::filesystem::is_directory(installPath)) {
+    LOG_FREE(Debug, "ApplicationPathHelpers", "Found EnergyPlus at '" << completeAndNormalize(installPath) << "'");
+    return completeAndNormalize(installPath);
+  }
+
+  // next check default location
+  openstudio::path energyPlusExeName = cmakeEnergyPlusPath.filename();
+  openstudio::path energyPlusDirectoryName = cmakeEnergyPlusDirectory.filename();
+
+#ifdef Q_OS_LINUX
+  auto systemPath = openstudio::toPath("/usr/local") / energyPlusDirectoryName / energyPlusExeName;
+#elif defined(Q_OS_MAC)
+  auto systemPath = openstudio::toPath("/Applications") / energyPlusDirectoryName / energyPlusExeName;
+#else
+  auto systemPath = openstudio::toPath("C:/") / energyPlusDirectoryName / energyPlusExeName;
+#endif
 
-    // next check default location
-    openstudio::path energyPlusExeName = cmakeEnergyPlusPath.filename();
-    openstudio::path energyPlusDirectoryName = cmakeEnergyPlusDirectory.filename();
+  LOG_FREE(Debug, "ApplicationPathHelpers", "Searching for EnergyPlus in '" << systemPath << "'");
+  if (openstudio::filesystem::exists(systemPath) && !openstudio::filesystem::is_directory(systemPath)) {
+    LOG_FREE(Debug, "ApplicationPathHelpers", "Found EnergyPlus at '" << completeAndNormalize(systemPath) << "'");
+    return completeAndNormalize(systemPath);
+  }
 
-    #ifdef Q_OS_LINUX
-      auto systemPath = openstudio::toPath("/usr/local") / energyPlusDirectoryName / energyPlusExeName;
-    #elif defined(Q_OS_MAC)
-      auto systemPath = openstudio::toPath("/Applications") / energyPlusDirectoryName / energyPlusExeName;
-    #else
-      auto systemPath = openstudio::toPath("C:/") / energyPlusDirectoryName / energyPlusExeName;
-    #endif
+  std::string energyPlusDirectoryNameStr = toString(energyPlusDirectoryName);
+  std::string energyPlusDirectoryNameStr2 = boost::replace_first_copy(energyPlusDirectoryNameStr, "EnergyPlus-", "EnergyPlusV");
+  if (energyPlusDirectoryNameStr != energyPlusDirectoryNameStr2) {
+#ifdef Q_OS_LINUX
+    systemPath = openstudio::toPath("/usr/local") / toPath(energyPlusDirectoryNameStr2) / energyPlusExeName;
+#elif defined(Q_OS_MAC)
+    systemPath = openstudio::toPath("/Applications") / toPath(energyPlusDirectoryNameStr2) / energyPlusExeName;
+#else
+    systemPath = openstudio::toPath("C:/") / toPath(energyPlusDirectoryNameStr2) / energyPlusExeName;
+#endif
 
     LOG_FREE(Debug, "ApplicationPathHelpers", "Searching for EnergyPlus in '" << systemPath << "'");
-    if( openstudio::filesystem::exists(systemPath) && !openstudio::filesystem::is_directory(systemPath) ) {
+    if (openstudio::filesystem::exists(systemPath) && !openstudio::filesystem::is_directory(systemPath)) {
       LOG_FREE(Debug, "ApplicationPathHelpers", "Found EnergyPlus at '" << completeAndNormalize(systemPath) << "'");
       return completeAndNormalize(systemPath);
     }
+  }
+
+  // next check user path
+  //QString userPath = QStandardPaths::findExecutable("energyplus");
+  //if (!userPath.isEmpty()){
+  //  openstudio::path path = openstudio::toPath(userPath);
+  //  if( openstudio::filesystem::exists(path) ) {
+  //    return path;
+  //  }
+  //}
+
+  return openstudio::path();
+}
+
+openstudio::path getRadianceDirectory() {
+  // handle developer case first
+  if (moduleIsRunningFromBuildDirectory()) {
+    openstudio::path cmakeRadianceDirectory = openstudio::toPath("${RADIANCE_LOCATION}");
+    if (openstudio::filesystem::exists(cmakeRadianceDirectory)) {
+      LOG_FREE(Debug, "ApplicationPathHelpers", "Found Radiance at '" << completeAndNormalize(cmakeRadianceDirectory) << "'");
+      return cmakeRadianceDirectory;
+    }
+  }
 
-    std::string energyPlusDirectoryNameStr = toString(energyPlusDirectoryName);
-    std::string energyPlusDirectoryNameStr2 = boost::replace_first_copy(energyPlusDirectoryNameStr,"EnergyPlus-","EnergyPlusV");
-    if (energyPlusDirectoryNameStr != energyPlusDirectoryNameStr2){
-      #ifdef Q_OS_LINUX
-       systemPath = openstudio::toPath("/usr/local") / toPath(energyPlusDirectoryNameStr2) / energyPlusExeName;
-      #elif defined(Q_OS_MAC)
-        systemPath = openstudio::toPath("/Applications") / toPath(energyPlusDirectoryNameStr2) / energyPlusExeName;
-      #else
-        systemPath = openstudio::toPath("C:/") / toPath(energyPlusDirectoryNameStr2) / energyPlusExeName;
-      #endif
-
-      LOG_FREE(Debug, "ApplicationPathHelpers", "Searching for EnergyPlus in '" << systemPath << "'");
-      if( openstudio::filesystem::exists(systemPath) && !openstudio::filesystem::is_directory(systemPath) ) {
-        LOG_FREE(Debug, "ApplicationPathHelpers", "Found EnergyPlus at '" << completeAndNormalize(systemPath) << "'");
-        return completeAndNormalize(systemPath);
-      }
+  // next check environment variable
+  auto envPathString = std::getenv("OS_RAYPATH");
+  if (envPathString) {
+    openstudio::path envPath = openstudio::toPath(envPathString);
+    if (openstudio::filesystem::exists(envPath)) {
+      LOG_FREE(Debug, "ApplicationPathHelpers", "Found Radiance at '" << completeAndNormalize(envPath) << "'");
+      return completeAndNormalize(envPath);
     }
+  }
 
-    // next check user path
-    //QString userPath = QStandardPaths::findExecutable("energyplus");
-    //if (!userPath.isEmpty()){
-    //  openstudio::path path = openstudio::toPath(userPath);
-    //  if( openstudio::filesystem::exists(path) ) {
-    //    return path;
-    //  }
-    //}
+  // next check for installed radiance
+  openstudio::path installPath = getOpenStudioModuleDirectory() / toPath("/../Radiance");
 
-    return openstudio::path();
+  LOG_FREE(Debug, "ApplicationPathHelpers", "Searching for Radiance in '" << installPath << "'");
+  if (openstudio::filesystem::exists(installPath)) {
+    LOG_FREE(Debug, "ApplicationPathHelpers", "Found Radiance at '" << completeAndNormalize(installPath) << "'");
+    return completeAndNormalize(installPath);
   }
 
-  openstudio::path getRadianceDirectory() {
-    // handle developer case first
-    if (moduleIsRunningFromBuildDirectory()){
-      openstudio::path cmakeRadianceDirectory = openstudio::toPath("${RADIANCE_LOCATION}");
-      if( openstudio::filesystem::exists(cmakeRadianceDirectory) ){
-        LOG_FREE(Debug, "ApplicationPathHelpers", "Found Radiance at '" << completeAndNormalize(cmakeRadianceDirectory) << "'");
-        return cmakeRadianceDirectory;
-      }
-    }
+// next check default location
+#ifdef Q_OS_LINUX
+  auto systemPath = openstudio::toPath("/usr/local/radiance");
+#elif defined(Q_OS_MAC)
+  auto systemPath = openstudio::toPath("/usr/local/radiance");
+#else
+  auto systemPath = openstudio::toPath("C:/Program Files/radiance");
+#endif
 
-    // next check environment variable
-    auto envPathString = std::getenv("OS_RAYPATH");
-    if( envPathString ) {
-      openstudio::path envPath = openstudio::toPath(envPathString);
-      if( openstudio::filesystem::exists(envPath) ) {
-        LOG_FREE(Debug, "ApplicationPathHelpers", "Found Radiance at '" << completeAndNormalize(envPath) << "'");
-        return completeAndNormalize(envPath);
-      }
-    }
+  LOG_FREE(Debug, "ApplicationPathHelpers", "Searching for Radiance in '" << systemPath << "'");
+  if (openstudio::filesystem::exists(systemPath)) {
+    LOG_FREE(Debug, "ApplicationPathHelpers", "Found Radiance at '" << completeAndNormalize(systemPath) << "'");
+    return completeAndNormalize(systemPath);
+  }
 
-    // next check for installed radiance
-    openstudio::path installPath = getOpenStudioModuleDirectory() / toPath("/../Radiance");
+  return openstudio::path();
+}
 
-    LOG_FREE(Debug, "ApplicationPathHelpers", "Searching for Radiance in '" << installPath << "'");
-    if( openstudio::filesystem::exists(installPath) ) {
-      LOG_FREE(Debug, "ApplicationPathHelpers", "Found Radiance at '" << completeAndNormalize(installPath) << "'");
-      return completeAndNormalize(installPath);
-    }
+openstudio::path getPerlExecutable() {
+#if defined(Q_OS_LINUX) || defined(Q_OS_MAC)
+  // check environment variable
+  auto envPathString = std::getenv("PERL_EXE_PATH");
+  if (envPathString) {
+    openstudio::path envPath = openstudio::toPath(envPathString);
 
-    // next check default location
-    #ifdef Q_OS_LINUX
-      auto systemPath = openstudio::toPath("/usr/local/radiance");
-    #elif defined(Q_OS_MAC)
-      auto systemPath = openstudio::toPath("/usr/local/radiance");
-    #else
-      auto systemPath = openstudio::toPath("C:/Program Files/radiance");
-    #endif
-
-    LOG_FREE(Debug, "ApplicationPathHelpers", "Searching for Radiance in '" << systemPath << "'");
-    if( openstudio::filesystem::exists(systemPath) ) {
-      LOG_FREE(Debug, "ApplicationPathHelpers", "Found Radiance at '" << completeAndNormalize(systemPath) << "'");
-      return completeAndNormalize(systemPath);
+    LOG_FREE(Debug, "ApplicationPathHelpers", "Searching for Perl in '" << envPath << "'");
+    if (openstudio::filesystem::exists(envPath)) {
+      LOG_FREE(Debug, "ApplicationPathHelpers", "Found Perl at '" << completeAndNormalize(envPath) << "'");
+      return envPath;
     }
-
-    return openstudio::path();
   }
 
-  openstudio::path getPerlExecutable() {
-    #if defined (Q_OS_LINUX) || defined (Q_OS_MAC)
-      // check environment variable
-      auto envPathString = std::getenv("PERL_EXE_PATH");
-      if( envPathString ) {
-        openstudio::path envPath = openstudio::toPath(envPathString);
-
-        LOG_FREE(Debug, "ApplicationPathHelpers", "Searching for Perl in '" << envPath << "'");
-        if( openstudio::filesystem::exists(envPath) ) {
-          LOG_FREE(Debug, "ApplicationPathHelpers", "Found Perl at '" << completeAndNormalize(envPath) << "'");
-          return envPath;
-        }
-      }
-
-      // next check for system perl
-      openstudio::path systemPath = toPath("/usr/bin/perl");
-
-      LOG_FREE(Debug, "ApplicationPathHelpers", "Searching for Perl in '" << systemPath << "'");
-      if( openstudio::filesystem::exists(systemPath) ) {
-        LOG_FREE(Debug, "ApplicationPathHelpers", "Found Perl at '" << completeAndNormalize(systemPath) << "'");
-        return completeAndNormalize(systemPath);
-      }
-    #else
-      // handle developer case first
-      if (moduleIsRunningFromBuildDirectory()){
-        openstudio::path cmakePerlPath = openstudio::toPath("${PERL_EXE}");
-
-        LOG_FREE(Debug, "ApplicationPathHelpers", "Searching for Perl in '" << cmakePerlPath << "'");
-        if( openstudio::filesystem::exists(cmakePerlPath) ){
-          LOG_FREE(Debug, "ApplicationPathHelpers", "Found Perl at '" << completeAndNormalize(cmakePerlPath) << "'");
-          return completeAndNormalize(cmakePerlPath);
-        }
-      }
-
-      // next check environment variable
-      auto envPathString = std::getenv("PERL_EXE_PATH");
-      if( envPathString ) {
-        openstudio::path envPath = openstudio::toPath(envPathString);
-
-        LOG_FREE(Debug, "ApplicationPathHelpers", "Searching for Perl in '" << envPath << "'");
-        if( openstudio::filesystem::exists(envPath) ) {
-          LOG_FREE(Debug, "ApplicationPathHelpers", "Found Perl at '" << completeAndNormalize(envPath) << "'");
-          return completeAndNormalize(envPath);
-        }
-      }
-
-      // next check for installed perl
-      openstudio::path installPath = getOpenStudioModuleDirectory() / toPath("/../Perl/bin/perl.exe");
-
-      LOG_FREE(Debug, "ApplicationPathHelpers", "Searching for Perl in '" << installPath << "'");
-      if( openstudio::filesystem::exists(installPath) ) {
-        LOG_FREE(Debug, "ApplicationPathHelpers", "Found Perl at '" << completeAndNormalize(installPath) << "'");
-        return completeAndNormalize(installPath);
-      }
-
-      // alternate install location (to be removed)
-      installPath = getOpenStudioModuleDirectory() / toPath("/../Perl/perl/bin/perl.exe");
-
-      LOG_FREE(Debug, "ApplicationPathHelpers", "Searching for Perl in '" << installPath << "'");
-      if( openstudio::filesystem::exists(installPath) ) {
-        LOG_FREE(Debug, "ApplicationPathHelpers", "Found Perl at '" << completeAndNormalize(installPath) << "'");
-        return completeAndNormalize(installPath);
-      }
-    #endif
-
-    return openstudio::path();
+  // next check for system perl
+  openstudio::path systemPath = toPath("/usr/bin/perl");
+
+  LOG_FREE(Debug, "ApplicationPathHelpers", "Searching for Perl in '" << systemPath << "'");
+  if (openstudio::filesystem::exists(systemPath)) {
+    LOG_FREE(Debug, "ApplicationPathHelpers", "Found Perl at '" << completeAndNormalize(systemPath) << "'");
+    return completeAndNormalize(systemPath);
+  }
+#else
+  // handle developer case first
+  if (moduleIsRunningFromBuildDirectory()) {
+    openstudio::path cmakePerlPath = openstudio::toPath("${PERL_EXE}");
+
+    LOG_FREE(Debug, "ApplicationPathHelpers", "Searching for Perl in '" << cmakePerlPath << "'");
+    if (openstudio::filesystem::exists(cmakePerlPath)) {
+      LOG_FREE(Debug, "ApplicationPathHelpers", "Found Perl at '" << completeAndNormalize(cmakePerlPath) << "'");
+      return completeAndNormalize(cmakePerlPath);
+    }
   }
 
-  std::string_view getSharedModuleSuffix() {
-    return "${CMAKE_SHARED_MODULE_SUFFIX}";
+  // next check environment variable
+  auto envPathString = std::getenv("PERL_EXE_PATH");
+  if (envPathString) {
+    openstudio::path envPath = openstudio::toPath(envPathString);
+
+    LOG_FREE(Debug, "ApplicationPathHelpers", "Searching for Perl in '" << envPath << "'");
+    if (openstudio::filesystem::exists(envPath)) {
+      LOG_FREE(Debug, "ApplicationPathHelpers", "Found Perl at '" << completeAndNormalize(envPath) << "'");
+      return completeAndNormalize(envPath);
+    }
   }
-  
-  std::string_view getSharedModulePrefix() {
-    return "${CMAKE_SHARED_MODULE_PREFIX}";
+
+  // next check for installed perl
+  openstudio::path installPath = getOpenStudioModuleDirectory() / toPath("/../Perl/bin/perl.exe");
+
+  LOG_FREE(Debug, "ApplicationPathHelpers", "Searching for Perl in '" << installPath << "'");
+  if (openstudio::filesystem::exists(installPath)) {
+    LOG_FREE(Debug, "ApplicationPathHelpers", "Found Perl at '" << completeAndNormalize(installPath) << "'");
+    return completeAndNormalize(installPath);
   }
 
-  std::string getSharedModuleName(std::string_view basename) {
-    return fmt::format("{}{}{}", getSharedModulePrefix(), basename, getSharedModuleSuffix());
+  // alternate install location (to be removed)
+  installPath = getOpenStudioModuleDirectory() / toPath("/../Perl/perl/bin/perl.exe");
+
+  LOG_FREE(Debug, "ApplicationPathHelpers", "Searching for Perl in '" << installPath << "'");
+  if (openstudio::filesystem::exists(installPath)) {
+    LOG_FREE(Debug, "ApplicationPathHelpers", "Found Perl at '" << completeAndNormalize(installPath) << "'");
+    return completeAndNormalize(installPath);
   }
+#endif
+
+  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
+}  // namespace openstudio
diff --git a/src/workflow/OSWorkflow.cpp b/src/workflow/OSWorkflow.cpp
index f57b09cd522..7be426da0ce 100644
--- a/src/workflow/OSWorkflow.cpp
+++ b/src/workflow/OSWorkflow.cpp
@@ -285,7 +285,7 @@ void OSWorkflow::run() {
         // )python",
         //                                      scriptPath_->parent_path().generic_string());
         auto importCmd = fmt::format(R"python(
-import importlib
+import importlib.util
 spec = importlib.util.spec_from_file_location('{}', r'{}')
 module = importlib.util.module_from_spec(spec)
 spec.loader.exec_module(module)


Test patch:
diff --git a/src/model/test/ZoneHVACPackagedTerminalAirConditioner_GTest.cpp b/src/model/test/ZoneHVACPackagedTerminalAirConditioner_GTest.cpp
index 7c7ec66da00..e241ee6221a 100644
--- a/src/model/test/ZoneHVACPackagedTerminalAirConditioner_GTest.cpp
+++ b/src/model/test/ZoneHVACPackagedTerminalAirConditioner_GTest.cpp
@@ -55,6 +55,8 @@
 #include "../ZoneHVACPackagedTerminalAirConditioner.hpp"
 #include "../ZoneHVACPackagedTerminalAirConditioner_Impl.hpp"
 #include "../ScheduleCompact.hpp"
+#include "../FanOnOff.hpp"
+#include "../FanSystemModel.hpp"
 
 using namespace openstudio;
 
@@ -264,3 +266,23 @@ TEST_F(ModelFixture, ZoneHVACPackagedTerminalAirConditioner_CoilSystemCoolingDXH
 
   model::ZoneHVACPackagedTerminalAirConditioner ptac(m, s, fan, heatingCoil, coolingCoil);
 }
+
+TEST_F(ModelFixture, ZoneHVACPackagedTerminalAirConditioner_SupplyAirFanOpSch) {
+  model::Model m;
+  model::CoilHeatingWater heatingCoil(m);
+  model::CoilSystemCoolingDXHeatExchangerAssisted coolingCoil(m);
+  model::FanConstantVolume fanCV(m);
+  model::FanSystemModel fanSystemModel(m);
+  model::FanOnOff fanOnOff(m);
+  auto alwaysOn = m.alwaysOnDiscreteSchedule();
+  auto alwaysOff = m.alwaysOffDiscreteSchedule();
+
+  model::ZoneHVACPackagedTerminalAirConditioner ptacCV(m, alwaysOn, fanCV, heatingCoil, coolingCoil);
+  EXPECT_EQ(alwaysOn, ptacCV.supplyAirFanOperatingModeSchedule());
+
+  model::ZoneHVACPackagedTerminalAirConditioner ptacSystemModel(m, alwaysOn, fanSystemModel, heatingCoil, coolingCoil);
+  EXPECT_EQ(alwaysOff, ptacSystemModel.supplyAirFanOperatingModeSchedule());
+
+  model::ZoneHVACPackagedTerminalAirConditioner ptacOnOff(m, alwaysOn, fanOnOff, heatingCoil, coolingCoil);
+  EXPECT_EQ(alwaysOff, ptacOnOff.supplyAirFanOperatingModeSchedule());
+}
diff --git a/src/model/test/ZoneHVACPackagedTerminalHeatPump_GTest.cpp b/src/model/test/ZoneHVACPackagedTerminalHeatPump_GTest.cpp
index 2ec603faeeb..33614136921 100644
--- a/src/model/test/ZoneHVACPackagedTerminalHeatPump_GTest.cpp
+++ b/src/model/test/ZoneHVACPackagedTerminalHeatPump_GTest.cpp
@@ -55,6 +55,9 @@
 #include "../ZoneHVACPackagedTerminalHeatPump.hpp"
 #include "../ZoneHVACPackagedTerminalHeatPump_Impl.hpp"
 #include "../ScheduleCompact.hpp"
+#include "../FanOnOff.hpp"
+#include "../FanSystemModel.hpp"
+
 #include <utilities/idd/OS_Curve_Biquadratic_FieldEnums.hxx>
 
 using namespace openstudio;
@@ -310,3 +313,24 @@ TEST_F(ModelFixture, ZoneHVACPackagedTerminalHeatPump_CoilSystemCoolingDXHeatExc
 
   model::ZoneHVACPackagedTerminalHeatPump pthp(m, availabilitySchedule, fan, heatingCoil, coolingCoil, supplementalHeatingCoil);
 }
+
+TEST_F(ModelFixture, ZoneHVACPackagedTerminalHeatPump_SupplyAirFanOpSch) {
+  model::Model m;
+  model::CoilHeatingDXVariableSpeed heatingCoil(m);
+  model::CoilCoolingDXVariableSpeed coolingCoil(m);
+  model::CoilHeatingElectric supplementalHeatingCoil(m);
+  model::FanConstantVolume fanCV(m);
+  model::FanSystemModel fanSystemModel(m);
+  model::FanOnOff fanOnOff(m);
+  auto alwaysOn = m.alwaysOnDiscreteSchedule();
+  auto alwaysOff = m.alwaysOffDiscreteSchedule();
+
+  model::ZoneHVACPackagedTerminalHeatPump pthpCV(m, alwaysOn, fanCV, heatingCoil, coolingCoil, supplementalHeatingCoil);
+  EXPECT_EQ(alwaysOn, pthpCV.supplyAirFanOperatingModeSchedule());
+
+  model::ZoneHVACPackagedTerminalHeatPump pthpSystemModel(m, alwaysOn, fanSystemModel, heatingCoil, coolingCoil, supplementalHeatingCoil);
+  EXPECT_EQ(alwaysOff, pthpSystemModel.supplyAirFanOperatingModeSchedule());
+
+  model::ZoneHVACPackagedTerminalHeatPump pthpOnOff(m, alwaysOn, fanOnOff, heatingCoil, coolingCoil, supplementalHeatingCoil);
+  EXPECT_EQ(alwaysOff, pthpOnOff.supplyAirFanOperatingModeSchedule());
+}
diff --git a/src/osversion/test/3_5_0/test_vt_ZoneHVACPackaged.osm b/src/osversion/test/3_5_0/test_vt_ZoneHVACPackaged.osm
new file mode 100644
index 00000000000..4d85ab9ec78
--- /dev/null
+++ b/src/osversion/test/3_5_0/test_vt_ZoneHVACPackaged.osm
@@ -0,0 +1,360 @@
+
+OS:Version,
+  {f2bf6811-825f-4f83-af96-b22b0908f434}, !- Handle
+  3.4.0;                                  !- Version Identifier
+
+OS:Coil:Heating:DX:SingleSpeed,
+  {405cb32f-90c9-4c2c-b22a-ab246f71d243}, !- Handle
+  Coil Heating DX Single Speed 1,         !- Name
+  {3f9d2cbc-47cd-4719-8f61-27cebb66d356}, !- Availability Schedule Name
+  Autosize,                               !- Rated Total Heating Capacity {W}
+  5,                                      !- Rated COP {W/W}
+  Autosize,                               !- Rated Air Flow Rate {m3/s}
+  773.3,                                  !- Rated Supply Fan Power Per Volume Flow Rate {W/(m3/s)}
+  ,                                       !- Air Inlet Node Name
+  ,                                       !- Air Outlet Node Name
+  {9ccb40b4-6d59-4b04-adb4-161de6730946}, !- Total Heating Capacity Function of Temperature Curve Name
+  {f4080dc7-e573-461c-9b26-3f90ba7e1fdb}, !- Total Heating Capacity Function of Flow Fraction Curve Name
+  {cfcc34af-f893-4947-94b8-5ea9624b9256}, !- Energy Input Ratio Function of Temperature Curve Name
+  {9bdf7dad-cd90-400e-b291-4e62bce4cd3f}, !- Energy Input Ratio Function of Flow Fraction Curve Name
+  {60ae2897-36f4-4a0e-b581-fb4a6a941dec}, !- Part Load Fraction Correlation Curve Name
+  ,                                       !- Defrost Energy Input Ratio Function of Temperature Curve Name
+  ,                                       !- Minimum Outdoor Dry-Bulb Temperature for Compressor Operation {C}
+  ,                                       !- Maximum Outdoor Dry-Bulb Temperature for Defrost Operation {C}
+  ,                                       !- Crankcase Heater Capacity {W}
+  ,                                       !- Maximum Outdoor Dry-Bulb Temperature for Crankcase Heater Operation {C}
+  Resistive,                              !- Defrost Strategy
+  Timed,                                  !- Defrost Control
+  0.166667,                               !- Defrost Time Period Fraction
+  2000;                                   !- Resistive Defrost Heater Capacity {W}
+
+OS:Schedule:Constant,
+  {3f9d2cbc-47cd-4719-8f61-27cebb66d356}, !- Handle
+  Always On Discrete,                     !- Name
+  {1a2f843f-ad5d-46a6-a190-83232b9c63f4}, !- Schedule Type Limits Name
+  1;                                      !- Value
+
+OS:ScheduleTypeLimits,
+  {1a2f843f-ad5d-46a6-a190-83232b9c63f4}, !- Handle
+  OnOff,                                  !- Name
+  0,                                      !- Lower Limit Value
+  1,                                      !- Upper Limit Value
+  Discrete,                               !- Numeric Type
+  Availability;                           !- Unit Type
+
+OS:Curve:Cubic,
+  {9ccb40b4-6d59-4b04-adb4-161de6730946}, !- Handle
+  Curve Cubic 1,                          !- Name
+  0.758746,                               !- Coefficient1 Constant
+  0.027626,                               !- Coefficient2 x
+  0.000148716,                            !- Coefficient3 x**2
+  3.4992e-06,                             !- Coefficient4 x**3
+  -20,                                    !- Minimum Value of x
+  20;                                     !- Maximum Value of x
+
+OS:Curve:Cubic,
+  {f4080dc7-e573-461c-9b26-3f90ba7e1fdb}, !- Handle
+  Curve Cubic 2,                          !- Name
+  0.84,                                   !- Coefficient1 Constant
+  0.16,                                   !- Coefficient2 x
+  0,                                      !- Coefficient3 x**2
+  0,                                      !- Coefficient4 x**3
+  0.5,                                    !- Minimum Value of x
+  1.5;                                    !- Maximum Value of x
+
+OS:Curve:Cubic,
+  {cfcc34af-f893-4947-94b8-5ea9624b9256}, !- Handle
+  Curve Cubic 3,                          !- Name
+  1.19248,                                !- Coefficient1 Constant
+  -0.0300438,                             !- Coefficient2 x
+  0.00103745,                             !- Coefficient3 x**2
+  -2.3328e-05,                            !- Coefficient4 x**3
+  -20,                                    !- Minimum Value of x
+  20;                                     !- Maximum Value of x
+
+OS:Curve:Quadratic,
+  {9bdf7dad-cd90-400e-b291-4e62bce4cd3f}, !- Handle
+  Curve Quadratic 1,                      !- Name
+  1.3824,                                 !- Coefficient1 Constant
+  -0.4336,                                !- Coefficient2 x
+  0.0512,                                 !- Coefficient3 x**2
+  0,                                      !- Minimum Value of x
+  1;                                      !- Maximum Value of x
+
+OS:Curve:Quadratic,
+  {60ae2897-36f4-4a0e-b581-fb4a6a941dec}, !- Handle
+  Curve Quadratic 2,                      !- Name
+  0.75,                                   !- Coefficient1 Constant
+  0.25,                                   !- Coefficient2 x
+  0,                                      !- Coefficient3 x**2
+  0,                                      !- Minimum Value of x
+  1;                                      !- Maximum Value of x
+
+OS:Coil:Cooling:DX:SingleSpeed,
+  {c3dc2852-18f2-4199-a93b-2da0c5ed3f64}, !- Handle
+  Coil Cooling DX Single Speed 1,         !- Name
+  {3f9d2cbc-47cd-4719-8f61-27cebb66d356}, !- 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)}
+  ,                                       !- Air Inlet Node Name
+  ,                                       !- Air Outlet Node Name
+  {20cbc7d0-9047-47f4-b3cf-7ebce6dc86ed}, !- Total Cooling Capacity Function of Temperature Curve Name
+  {63462c10-09fc-4c5a-8312-bbe6e8b828bc}, !- Total Cooling Capacity Function of Flow Fraction Curve Name
+  {ef75106e-4029-43dc-a1ca-21dac784d198}, !- Energy Input Ratio Function of Temperature Curve Name
+  {192ff408-13b7-48ed-b324-45aef010bb3a}, !- Energy Input Ratio Function of Flow Fraction Curve Name
+  {2665cb2e-73c1-4ff6-b337-8f13740c0d91}, !- 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:Curve:Biquadratic,
+  {20cbc7d0-9047-47f4-b3cf-7ebce6dc86ed}, !- Handle
+  Curve Biquadratic 1,                    !- Name
+  0.942587793,                            !- Coefficient1 Constant
+  0.009543347,                            !- Coefficient2 x
+  0.00068377,                             !- Coefficient3 x**2
+  -0.011042676,                           !- Coefficient4 y
+  5.249e-06,                              !- Coefficient5 y**2
+  -9.72e-06,                              !- Coefficient6 x*y
+  17,                                     !- Minimum Value of x
+  22,                                     !- Maximum Value of x
+  13,                                     !- Minimum Value of y
+  46;                                     !- Maximum Value of y
+
+OS:Curve:Quadratic,
+  {63462c10-09fc-4c5a-8312-bbe6e8b828bc}, !- Handle
+  Curve Quadratic 3,                      !- Name
+  0.8,                                    !- Coefficient1 Constant
+  0.2,                                    !- Coefficient2 x
+  0,                                      !- Coefficient3 x**2
+  0.5,                                    !- Minimum Value of x
+  1.5;                                    !- Maximum Value of x
+
+OS:Curve:Biquadratic,
+  {ef75106e-4029-43dc-a1ca-21dac784d198}, !- Handle
+  Curve Biquadratic 2,                    !- Name
+  0.342414409,                            !- Coefficient1 Constant
+  0.034885008,                            !- Coefficient2 x
+  -0.0006237,                             !- Coefficient3 x**2
+  0.004977216,                            !- Coefficient4 y
+  0.000437951,                            !- Coefficient5 y**2
+  -0.000728028,                           !- Coefficient6 x*y
+  17,                                     !- Minimum Value of x
+  22,                                     !- Maximum Value of x
+  13,                                     !- Minimum Value of y
+  46;                                     !- Maximum Value of y
+
+OS:Curve:Quadratic,
+  {192ff408-13b7-48ed-b324-45aef010bb3a}, !- Handle
+  Curve Quadratic 4,                      !- Name
+  1.1552,                                 !- Coefficient1 Constant
+  -0.1808,                                !- Coefficient2 x
+  0.0256,                                 !- Coefficient3 x**2
+  0.5,                                    !- Minimum Value of x
+  1.5;                                    !- Maximum Value of x
+
+OS:Curve:Quadratic,
+  {2665cb2e-73c1-4ff6-b337-8f13740c0d91}, !- Handle
+  Curve Quadratic 5,                      !- Name
+  0.85,                                   !- Coefficient1 Constant
+  0.15,                                   !- Coefficient2 x
+  0,                                      !- Coefficient3 x**2
+  0,                                      !- Minimum Value of x
+  1;                                      !- Maximum Value of x
+
+OS:Coil:Heating:Electric,
+  {e90e9d96-bf46-4b1e-9a99-5ea9da8c61f5}, !- Handle
+  Coil Heating Electric 1,                !- Name
+  {3f9d2cbc-47cd-4719-8f61-27cebb66d356}, !- Availability Schedule Name
+  ,                                       !- Efficiency
+  ,                                       !- Nominal Capacity {W}
+  ,                                       !- Air Inlet Node Name
+  ;                                       !- Air Outlet Node Name
+
+OS:Fan:ConstantVolume,
+  {6600a5aa-c5e7-4eb2-8d4c-5fcbf38c5dbb}, !- Handle
+  PTHP Fan,                               !- Name
+  {3f9d2cbc-47cd-4719-8f61-27cebb66d356}, !- Availability Schedule Name
+  ,                                       !- Fan Total Efficiency
+  ,                                       !- Pressure Rise {Pa}
+  AutoSize,                               !- Maximum Flow Rate {m3/s}
+  ,                                       !- Motor Efficiency
+  ,                                       !- Motor In Airstream Fraction
+  ,                                       !- Air Inlet Node Name
+  ,                                       !- Air Outlet Node Name
+  ;                                       !- End-Use Subcategory
+
+OS:ZoneHVAC:PackagedTerminalHeatPump,
+  {bddf0afb-148c-4e76-8d36-479a2e08a2ef}, !- Handle
+  Zone HVAC Packaged Terminal Heat Pump 1, !- Name
+  {3f9d2cbc-47cd-4719-8f61-27cebb66d356}, !- Availability Schedule Name
+  ,                                       !- Air Inlet Node Name
+  ,                                       !- Air Outlet Node Name
+  OutdoorAir:Mixer,                       !- Outdoor Air Mixer Object Type
+  ,                                       !- Outdoor Air Mixer Name
+  Autosize,                               !- Supply Air Flow Rate During Cooling Operation {m3/s}
+  Autosize,                               !- Supply Air Flow Rate During Heating Operation {m3/s}
+  Autosize,                               !- Supply Air Flow Rate When No Cooling or Heating is Needed {m3/s}
+  Autosize,                               !- Outdoor Air Flow Rate During Cooling Operation {m3/s}
+  Autosize,                               !- Outdoor Air Flow Rate During Heating Operation {m3/s}
+  Autosize,                               !- Outdoor Air Flow Rate When No Cooling or Heating is Needed {m3/s}
+  {6600a5aa-c5e7-4eb2-8d4c-5fcbf38c5dbb}, !- Supply Air Fan Name
+  {405cb32f-90c9-4c2c-b22a-ab246f71d243}, !- Heating Coil Name
+  ,                                       !- Heating Convergence Tolerance {dimensionless}
+  ,                                       !- Minimum Outdoor Dry-Bulb Temperature for Compressor Operation {C}
+  {c3dc2852-18f2-4199-a93b-2da0c5ed3f64}, !- Cooling Coil Name
+  ,                                       !- Cooling Convergence Tolerance {dimensionless}
+  {e90e9d96-bf46-4b1e-9a99-5ea9da8c61f5}, !- Supplemental Heating Coil Name
+  Autosize,                               !- Maximum Supply Air Temperature from Supplemental Heater {C}
+  ,                                       !- Maximum Outdoor Dry-Bulb Temperature for Supplemental Heater Operation {C}
+  ,                                       !- Fan Placement
+  ;                                       !- Supply Air Fan Operating Mode Schedule Name
+
+OS:Coil:Heating:Electric,
+  {06692db9-5485-4524-8135-fc0f2ac8c40d}, !- Handle
+  Coil Heating Electric 2,                !- Name
+  {3f9d2cbc-47cd-4719-8f61-27cebb66d356}, !- Availability Schedule Name
+  ,                                       !- Efficiency
+  ,                                       !- Nominal Capacity {W}
+  ,                                       !- Air Inlet Node Name
+  ;                                       !- Air Outlet Node Name
+
+OS:Coil:Cooling:DX:SingleSpeed,
+  {d8b44275-6385-4ddd-8890-47ff167c4652}, !- Handle
+  Coil Cooling DX Single Speed 2,         !- Name
+  {3f9d2cbc-47cd-4719-8f61-27cebb66d356}, !- 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)}
+  ,                                       !- Air Inlet Node Name
+  ,                                       !- Air Outlet Node Name
+  {66fc8437-d62b-4abe-8971-0aca40d45c4a}, !- Total Cooling Capacity Function of Temperature Curve Name
+  {0fe5e1aa-db99-4aa1-947f-377e904875ec}, !- Total Cooling Capacity Function of Flow Fraction Curve Name
+  {c339821c-d155-4f36-865a-a55e1321dae5}, !- Energy Input Ratio Function of Temperature Curve Name
+  {15a0df0a-d826-4d8b-94b7-e2291ee426a5}, !- Energy Input Ratio Function of Flow Fraction Curve Name
+  {fe1f5765-b70e-4436-92a1-145ca6f84746}, !- 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:Curve:Biquadratic,
+  {66fc8437-d62b-4abe-8971-0aca40d45c4a}, !- Handle
+  Curve Biquadratic 3,                    !- Name
+  0.942587793,                            !- Coefficient1 Constant
+  0.009543347,                            !- Coefficient2 x
+  0.00068377,                             !- Coefficient3 x**2
+  -0.011042676,                           !- Coefficient4 y
+  5.249e-06,                              !- Coefficient5 y**2
+  -9.72e-06,                              !- Coefficient6 x*y
+  17,                                     !- Minimum Value of x
+  22,                                     !- Maximum Value of x
+  13,                                     !- Minimum Value of y
+  46;                                     !- Maximum Value of y
+
+OS:Curve:Quadratic,
+  {0fe5e1aa-db99-4aa1-947f-377e904875ec}, !- Handle
+  Curve Quadratic 6,                      !- Name
+  0.8,                                    !- Coefficient1 Constant
+  0.2,                                    !- Coefficient2 x
+  0,                                      !- Coefficient3 x**2
+  0.5,                                    !- Minimum Value of x
+  1.5;                                    !- Maximum Value of x
+
+OS:Curve:Biquadratic,
+  {c339821c-d155-4f36-865a-a55e1321dae5}, !- Handle
+  Curve Biquadratic 4,                    !- Name
+  0.342414409,                            !- Coefficient1 Constant
+  0.034885008,                            !- Coefficient2 x
+  -0.0006237,                             !- Coefficient3 x**2
+  0.004977216,                            !- Coefficient4 y
+  0.000437951,                            !- Coefficient5 y**2
+  -0.000728028,                           !- Coefficient6 x*y
+  17,                                     !- Minimum Value of x
+  22,                                     !- Maximum Value of x
+  13,                                     !- Minimum Value of y
+  46;                                     !- Maximum Value of y
+
+OS:Curve:Quadratic,
+  {15a0df0a-d826-4d8b-94b7-e2291ee426a5}, !- Handle
+  Curve Quadratic 7,                      !- Name
+  1.1552,                                 !- Coefficient1 Constant
+  -0.1808,                                !- Coefficient2 x
+  0.0256,                                 !- Coefficient3 x**2
+  0.5,                                    !- Minimum Value of x
+  1.5;                                    !- Maximum Value of x
+
+OS:Curve:Quadratic,
+  {fe1f5765-b70e-4436-92a1-145ca6f84746}, !- Handle
+  Curve Quadratic 8,                      !- Name
+  0.85,                                   !- Coefficient1 Constant
+  0.15,                                   !- Coefficient2 x
+  0,                                      !- Coefficient3 x**2
+  0,                                      !- Minimum Value of x
+  1;                                      !- Maximum Value of x
+
+OS:Fan:ConstantVolume,
+  {048e8bf0-1f9b-40d4-a0b0-d787991f3d94}, !- Handle
+  PTAC Fan,                               !- Name
+  {3f9d2cbc-47cd-4719-8f61-27cebb66d356}, !- Availability Schedule Name
+  0.75,                                   !- Fan Total Efficiency
+  265,                                    !- Pressure Rise {Pa}
+  0.5,                                    !- Maximum Flow Rate {m3/s}
+  0.89,                                   !- Motor Efficiency
+  0.92,                                   !- Motor In Airstream Fraction
+  ,                                       !- Air Inlet Node Name
+  ,                                       !- Air Outlet Node Name
+  PTAC Fans;                              !- End-Use Subcategory
+
+OS:ZoneHVAC:PackagedTerminalAirConditioner,
+  {f1f8d2af-4f9a-4c72-ae93-dd11d9d6ab49}, !- Handle
+  Zone HVAC Packaged Terminal Air Conditioner 1, !- Name
+  {3f9d2cbc-47cd-4719-8f61-27cebb66d356}, !- Availability Schedule Name
+  ,                                       !- Air Inlet Node Name
+  ,                                       !- Air Outlet Node Name
+  OutdoorAir:Mixer,                       !- Outdoor Air Mixer Object Type
+  ,                                       !- Outdoor Air Mixer Name
+  Autosize,                               !- Supply Air Flow Rate During Cooling Operation {m3/s}
+  Autosize,                               !- Supply Air Flow Rate During Heating Operation {m3/s}
+  Autosize,                               !- Supply Air Flow Rate When No Cooling or Heating is Needed {m3/s}
+  Autosize,                               !- Outdoor Air Flow Rate During Cooling Operation {m3/s}
+  Autosize,                               !- Outdoor Air Flow Rate During Heating Operation {m3/s}
+  Autosize,                               !- Outdoor Air Flow Rate When No Cooling or Heating is Needed {m3/s}
+  {048e8bf0-1f9b-40d4-a0b0-d787991f3d94}, !- Supply Air Fan Name
+  {06692db9-5485-4524-8135-fc0f2ac8c40d}, !- Heating Coil Name
+  {d8b44275-6385-4ddd-8890-47ff167c4652}, !- Cooling Coil Name
+  DrawThrough,                            !- Fan Placement
+  ;                                       !- Supply Air Fan Operating Mode Schedule Name
+
diff --git a/src/osversion/test/3_5_0/test_vt_ZoneHVACPackaged.rb b/src/osversion/test/3_5_0/test_vt_ZoneHVACPackaged.rb
new file mode 100644
index 00000000000..4b86101b614
--- /dev/null
+++ b/src/osversion/test/3_5_0/test_vt_ZoneHVACPackaged.rb
@@ -0,0 +1,31 @@
+#require '/usr/local/openstudio-3.4.0/Ruby/openstudio'
+
+include OpenStudio::Model
+
+m = Model.new
+
+pthp_hc = CoilHeatingDXSingleSpeed.new(m)
+pthp_cc = CoilCoolingDXSingleSpeed.new(m)
+pthp_suppHC = CoilHeatingElectric.new(m)
+pthp_fan = FanConstantVolume.new(m)
+pthp_fan.setName("PTHP Fan")
+pthp = ZoneHVACPackagedTerminalHeatPump.new(m, m.alwaysOnDiscreteSchedule, pthp_fan, pthp_hc, pthp_cc, pthp_suppHC)
+pthp.resetSupplyAirFanOperatingModeSchedule
+
+ptac_hc = CoilHeatingElectric.new(m)
+ptac_cc = CoilCoolingDXSingleSpeed.new(m)
+ptac_fan = FanConstantVolume.new(m)
+ptac_fan.setName("PTAC Fan")
+ptac = ZoneHVACPackagedTerminalAirConditioner.new(m, m.alwaysOnDiscreteSchedule, ptac_fan, ptac_hc, ptac_cc)
+ptac.resetSupplyAirFanOperatingModeSchedule
+
+# Leaving the pthp fan will all defaults, harcoding this one so we test both
+# cases and ensure the resulting Fan:SystemModel is correct
+ptac_fan.setFanTotalEfficiency(0.75)
+ptac_fan.setPressureRise(265)
+ptac_fan.setMaximumFlowRate(0.5)
+ptac_fan.setMotorEfficiency(0.89)
+ptac_fan.setMotorInAirstreamFraction(0.92)
+ptac_fan.setEndUseSubcategory("PTAC Fans")
+
+m.save('test_vt_ZoneHVACPackaged.osm', true)
diff --git a/src/osversion/test/VersionTranslator_GTest.cpp b/src/osversion/test/VersionTranslator_GTest.cpp
index 8d6ce36e644..c8193edeb45 100644
--- a/src/osversion/test/VersionTranslator_GTest.cpp
+++ b/src/osversion/test/VersionTranslator_GTest.cpp
@@ -53,6 +53,7 @@
 #include "../../utilities/idf/IdfExtensibleGroup.hpp"
 #include "../../utilities/idf/WorkspaceExtensibleGroup.hpp"
 
+#include <utilities/idd/OS_Fan_SystemModel_FieldEnums.hxx>
 #include <utilities/idd/OS_Connection_FieldEnums.hxx>
 #include <utilities/idd/OS_Version_FieldEnums.hxx>
 #include <utilities/idd/IddEnums.hxx>
@@ -2228,3 +2229,93 @@ TEST_F(OSVersionFixture, update_3_4_0_to_3_5_0_CoilHeatingGasMultiStage) {
   ASSERT_TRUE(coil.getTarget(2));
   EXPECT_EQ("Always On Discrete", coil.getTarget(2)->nameString());
 }
+
+TEST_F(OSVersionFixture, update_3_4_0_to_3_5_0_ZoneHVACPackaged) {
+  openstudio::path path = resourcesPath() / toPath("osversion/3_5_0/test_vt_ZoneHVACPackaged.osm");
+  osversion::VersionTranslator vt;
+  boost::optional<model::Model> model = vt.loadModel(path);
+  ASSERT_TRUE(model) << "Failed to load " << path;
+
+  openstudio::path outPath = resourcesPath() / toPath("osversion/3_3_0/test_vt_ZoneHVACPackaged_updated.osm");
+  model->save(outPath, true);
+
+  EXPECT_EQ(2, model->getObjectsByType("OS:Fan:SystemModel").size());
+  {
+    std::vector<WorkspaceObject> ptacs = model->getObjectsByType("OS:ZoneHVAC:PackagedTerminalAirConditioner");
+    ASSERT_EQ(1u, ptacs.size());
+    auto& ptac = ptacs.front();
+
+    // Check the Supply Air Fan Operating Mode Schedule
+    ASSERT_TRUE(ptac.getTarget(17));
+    WorkspaceObject fanOpSch = ptac.getTarget(17).get();
+    EXPECT_EQ("Always Off Discrete", fanOpSch.nameString());
+    EXPECT_EQ(0.0, fanOpSch.getDouble(3).get());
+
+    // Check the Fan, converted from Fan:ConstantVolume to Fan:SystemModel
+    ASSERT_TRUE(ptac.getTarget(13));
+    WorkspaceObject fan = ptac.getTarget(13).get();
+
+    EXPECT_EQ(IddObjectType(IddObjectType::OS_Fan_SystemModel), fan.iddObject().type());
+    EXPECT_EQ("PTAC Fan", fan.getString(OS_Fan_SystemModelFields::Name).get());
+    EXPECT_EQ("Always On Discrete", fan.getString(OS_Fan_SystemModelFields::AvailabilityScheduleName).get());
+    EXPECT_TRUE(fan.isEmpty(OS_Fan_SystemModelFields::AirInletNodeName));
+    EXPECT_TRUE(fan.isEmpty(OS_Fan_SystemModelFields::AirOutletNodeName));
+    EXPECT_EQ(0.5, fan.getDouble(OS_Fan_SystemModelFields::DesignMaximumAirFlowRate).get());
+    EXPECT_EQ("Discrete", fan.getString(OS_Fan_SystemModelFields::SpeedControlMethod).get());
+    EXPECT_EQ(0.0, fan.getDouble(OS_Fan_SystemModelFields::ElectricPowerMinimumFlowRateFraction).get());
+    EXPECT_EQ(265.0, fan.getDouble(OS_Fan_SystemModelFields::DesignPressureRise).get());
+    EXPECT_EQ(0.89, fan.getDouble(OS_Fan_SystemModelFields::MotorEfficiency).get());
+    EXPECT_EQ(0.92, fan.getDouble(OS_Fan_SystemModelFields::MotorInAirStreamFraction).get());
+    EXPECT_EQ("Autosize", fan.getString(OS_Fan_SystemModelFields::DesignElectricPowerConsumption).get());
+    EXPECT_EQ("TotalEfficiencyAndPressure", fan.getString(OS_Fan_SystemModelFields::DesignPowerSizingMethod).get());
+    EXPECT_EQ(840.0, fan.getDouble(OS_Fan_SystemModelFields::ElectricPowerPerUnitFlowRate).get());
+    EXPECT_EQ(1.66667, fan.getDouble(OS_Fan_SystemModelFields::ElectricPowerPerUnitFlowRatePerUnitPressure).get());
+    EXPECT_EQ(0.75, fan.getDouble(OS_Fan_SystemModelFields::FanTotalEfficiency).get());
+    EXPECT_TRUE(fan.isEmpty(OS_Fan_SystemModelFields::ElectricPowerFunctionofFlowFractionCurveName));
+    EXPECT_TRUE(fan.isEmpty(OS_Fan_SystemModelFields::NightVentilationModePressureRise));
+    EXPECT_TRUE(fan.isEmpty(OS_Fan_SystemModelFields::NightVentilationModeFlowFraction));
+    EXPECT_TRUE(fan.isEmpty(OS_Fan_SystemModelFields::MotorLossZoneName));
+    EXPECT_EQ(0.0, fan.getDouble(OS_Fan_SystemModelFields::MotorLossRadiativeFraction).get());
+    EXPECT_EQ("PTAC Fans", fan.getString(OS_Fan_SystemModelFields::EndUseSubcategory).get());
+  }
+
+  {
+    std::vector<WorkspaceObject> pthps = model->getObjectsByType("OS:ZoneHVAC:PackagedTerminalHeatPump");
+    ASSERT_EQ(1u, pthps.size());
+    auto& pthp = pthps.front();
+
+    // Check the Supply Air Fan Operating Mode Schedule
+    ASSERT_TRUE(pthp.getTarget(23));
+    WorkspaceObject fanOpSch = pthp.getTarget(23).get();
+    EXPECT_EQ("Always Off Discrete", fanOpSch.nameString());
+    EXPECT_EQ(0.0, fanOpSch.getDouble(3).get());
+
+    // Check the Fan, converted from Fan:ConstantVolume to Fan:SystemModel
+    ASSERT_TRUE(pthp.getTarget(13));
+    WorkspaceObject fan = pthp.getTarget(13).get();
+
+    // This one is all defaulted, ensure we get the SAME values
+    EXPECT_EQ(IddObjectType(IddObjectType::OS_Fan_SystemModel), fan.iddObject().type());
+    EXPECT_EQ("PTHP Fan", fan.getString(OS_Fan_SystemModelFields::Name).get());
+    EXPECT_EQ("Always On Discrete", fan.getString(OS_Fan_SystemModelFields::AvailabilityScheduleName).get());
+    EXPECT_TRUE(fan.isEmpty(OS_Fan_SystemModelFields::AirInletNodeName));
+    EXPECT_TRUE(fan.isEmpty(OS_Fan_SystemModelFields::AirOutletNodeName));
+    EXPECT_EQ("AutoSize", fan.getString(OS_Fan_SystemModelFields::DesignMaximumAirFlowRate).get());
+    EXPECT_EQ("Discrete", fan.getString(OS_Fan_SystemModelFields::SpeedControlMethod).get());
+    EXPECT_EQ(0.0, fan.getDouble(OS_Fan_SystemModelFields::ElectricPowerMinimumFlowRateFraction).get());
+    EXPECT_EQ(250.0, fan.getDouble(OS_Fan_SystemModelFields::DesignPressureRise).get());
+    EXPECT_EQ(0.9, fan.getDouble(OS_Fan_SystemModelFields::MotorEfficiency).get());
+    EXPECT_EQ(1.0, fan.getDouble(OS_Fan_SystemModelFields::MotorInAirStreamFraction).get());
+    EXPECT_EQ("Autosize", fan.getString(OS_Fan_SystemModelFields::DesignElectricPowerConsumption).get());
+    EXPECT_EQ("TotalEfficiencyAndPressure", fan.getString(OS_Fan_SystemModelFields::DesignPowerSizingMethod).get());
+    EXPECT_EQ(840.0, fan.getDouble(OS_Fan_SystemModelFields::ElectricPowerPerUnitFlowRate).get());
+    EXPECT_EQ(1.66667, fan.getDouble(OS_Fan_SystemModelFields::ElectricPowerPerUnitFlowRatePerUnitPressure).get());
+    EXPECT_EQ(0.7, fan.getDouble(OS_Fan_SystemModelFields::FanTotalEfficiency).get());
+    EXPECT_TRUE(fan.isEmpty(OS_Fan_SystemModelFields::ElectricPowerFunctionofFlowFractionCurveName));
+    EXPECT_TRUE(fan.isEmpty(OS_Fan_SystemModelFields::NightVentilationModePressureRise));
+    EXPECT_TRUE(fan.isEmpty(OS_Fan_SystemModelFields::NightVentilationModeFlowFraction));
+    EXPECT_TRUE(fan.isEmpty(OS_Fan_SystemModelFields::MotorLossZoneName));
+    EXPECT_EQ(0.0, fan.getDouble(OS_Fan_SystemModelFields::MotorLossRadiativeFraction).get());
+    EXPECT_EQ("General", fan.getString(OS_Fan_SystemModelFields::EndUseSubcategory).get());
+  }
+}
diff --git a/src/utilities/core/test/ApplicationPathHelpers_GTest.cpp b/src/utilities/core/test/ApplicationPathHelpers_GTest.cpp
index ba23c28d6ef..914a07d8335 100644
--- a/src/utilities/core/test/ApplicationPathHelpers_GTest.cpp
+++ b/src/utilities/core/test/ApplicationPathHelpers_GTest.cpp
@@ -35,6 +35,7 @@
 #include <stdlib.h>
 #include <thread>
 #include <future>
+#include <OpenStudio.hxx>
 
 // TODO: GTEST 1.9 should have a GTEST_SKIP macro we could use
 #define SKIP(TEST_NAME)                                                                                                                              \
@@ -205,7 +206,9 @@ TEST(ApplicationPathHelpers, Simple_test_forThisModule) {
   EXPECT_TRUE(exists(openstudioModulePath));
   // The expected path is the utilities one, but resolved for symlinks (we don't want to hardcode the version eg openstudio_utilities_tests-2.8.0)
 #if defined(_WIN32)
-#  if _DEBUG
+#  if defined(NINJA)
+  openstudio::path expectedOpenstudioModulePath = getApplicationBuildDirectory() / toPath("Products/openstudiolib.dll");
+#  elif _DEBUG
   openstudio::path expectedOpenstudioModulePath = getApplicationBuildDirectory() / toPath("Products/Debug/openstudiolib.dll");
 #  else
   openstudio::path expectedOpenstudioModulePath = getApplicationBuildDirectory() / toPath("Products/Release/openstudiolib.dll");