LCOV - code coverage report
Current view: top level - EnergyPlus - PluginManager.cc (source / functions) Hit Total Coverage
Test: lcov.output.filtered Lines: 427 772 55.3 %
Date: 2024-08-24 18:31:18 Functions: 25 38 65.8 %

          Line data    Source code
       1             : // EnergyPlus, Copyright (c) 1996-2024, The Board of Trustees of the University of Illinois,
       2             : // The Regents of the University of California, through Lawrence Berkeley National Laboratory
       3             : // (subject to receipt of any required approvals from the U.S. Dept. of Energy), Oak Ridge
       4             : // National Laboratory, managed by UT-Battelle, Alliance for Sustainable Energy, LLC, and other
       5             : // contributors. All rights reserved.
       6             : //
       7             : // NOTICE: This Software was developed under funding from the U.S. Department of Energy and the
       8             : // U.S. Government consequently retains certain rights. As such, the U.S. Government has been
       9             : // granted for itself and others acting on its behalf a paid-up, nonexclusive, irrevocable,
      10             : // worldwide license in the Software to reproduce, distribute copies to the public, prepare
      11             : // derivative works, and perform publicly and display publicly, and to permit others to do so.
      12             : //
      13             : // Redistribution and use in source and binary forms, with or without modification, are permitted
      14             : // provided that the following conditions are met:
      15             : //
      16             : // (1) Redistributions of source code must retain the above copyright notice, this list of
      17             : //     conditions and the following disclaimer.
      18             : //
      19             : // (2) Redistributions in binary form must reproduce the above copyright notice, this list of
      20             : //     conditions and the following disclaimer in the documentation and/or other materials
      21             : //     provided with the distribution.
      22             : //
      23             : // (3) Neither the name of the University of California, Lawrence Berkeley National Laboratory,
      24             : //     the University of Illinois, U.S. Dept. of Energy nor the names of its contributors may be
      25             : //     used to endorse or promote products derived from this software without specific prior
      26             : //     written permission.
      27             : //
      28             : // (4) Use of EnergyPlus(TM) Name. If Licensee (i) distributes the software in stand-alone form
      29             : //     without changes from the version obtained under this License, or (ii) Licensee makes a
      30             : //     reference solely to the software portion of its product, Licensee must refer to the
      31             : //     software as "EnergyPlus version X" software, where "X" is the version number Licensee
      32             : //     obtained under this License and may not use a different name for the software. Except as
      33             : //     specifically required in this Section (4), Licensee shall not use in a company name, a
      34             : //     product name, in advertising, publicity, or other promotional activities any name, trade
      35             : //     name, trademark, logo, or other designation of "EnergyPlus", "E+", "e+" or confusingly
      36             : //     similar designation, without the U.S. Department of Energy's prior written consent.
      37             : //
      38             : // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
      39             : // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
      40             : // AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
      41             : // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
      42             : // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
      43             : // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
      44             : // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
      45             : // OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
      46             : // POSSIBILITY OF SUCH DAMAGE.
      47             : 
      48             : #include <EnergyPlus/Data/EnergyPlusData.hh>
      49             : #include <EnergyPlus/DataGlobalConstants.hh>
      50             : #include <EnergyPlus/DataStringGlobals.hh>
      51             : #include <EnergyPlus/FileSystem.hh>
      52             : #include <EnergyPlus/InputProcessing/InputProcessor.hh>
      53             : #include <EnergyPlus/OutputProcessor.hh>
      54             : #include <EnergyPlus/PluginManager.hh>
      55             : #include <EnergyPlus/UtilityRoutines.hh>
      56             : 
      57             : #include <algorithm>
      58             : #include <nlohmann/json.hpp>
      59             : 
      60             : #if LINK_WITH_PYTHON
      61             : #include <fmt/format.h>
      62             : template <> struct fmt::formatter<PyStatus>
      63             : {
      64             :     // parse is inherited from formatter<string_view>.
      65           0 :     static constexpr auto parse(const format_parse_context &ctx) -> format_parse_context::iterator
      66             :     {
      67           0 :         return ctx.begin();
      68             :     }
      69             : 
      70           0 :     static auto format(const PyStatus &status, format_context &ctx) -> format_context::iterator
      71             :     {
      72           0 :         if (!PyStatus_Exception(status)) {
      73           0 :             return ctx.out();
      74             :         }
      75           0 :         if (PyStatus_IsExit(status)) {
      76           0 :             return format_to(ctx.out(), "Exited with code {}", status.exitcode);
      77             :         }
      78           0 :         if (PyStatus_IsError(status)) {
      79           0 :             auto it = ctx.out();
      80           0 :             it = format_to(it, "Fatal Python error: ");
      81           0 :             if (status.func) {
      82           0 :                 it = format_to(it, "{}: ", status.func);
      83             :             }
      84           0 :             it = format_to(it, "{}", status.err_msg);
      85           0 :             return it;
      86             :         }
      87           0 :         return ctx.out();
      88             :     }
      89             : }; // namespace fmt
      90             : #endif
      91             : 
      92             : namespace EnergyPlus::PluginManagement {
      93             : 
      94           4 : PluginTrendVariable::PluginTrendVariable(const EnergyPlusData &state, std::string _name, int const _numValues, int const _indexOfPluginVariable)
      95           4 :     : name(std::move(_name)), numValues(_numValues), indexOfPluginVariable(_indexOfPluginVariable)
      96             : {
      97             :     // initialize the deque, so it can be queried immediately, even with just zeroes
      98        1214 :     for (int i = 1; i <= this->numValues; i++) {
      99        1210 :         this->values.push_back(0);
     100        1210 :         this->times.push_back(-i * state.dataGlobal->TimeStepZone);
     101             :     }
     102           4 : }
     103             : 
     104           0 : void registerNewCallback(const EnergyPlusData &state, EMSManager::EMSCallFrom const iCalledFrom, const std::function<void(void *)> &f)
     105             : {
     106           0 :     state.dataPluginManager->callbacks[iCalledFrom].push_back(f);
     107           0 : }
     108             : 
     109           0 : void registerUserDefinedCallback(const EnergyPlusData &state, const std::function<void(void *)> &f, const std::string &programNameInInputFile)
     110             : {
     111             :     // internally, E+ will UPPER the program name; we should upper the passed in registration name so it matches
     112           0 :     state.dataPluginManager->userDefinedCallbackNames.push_back(Util::makeUPPER(programNameInInputFile));
     113           0 :     state.dataPluginManager->userDefinedCallbacks.push_back(f);
     114           0 : }
     115             : 
     116         142 : void onBeginEnvironment(const EnergyPlusData &state)
     117             : {
     118             :     // reset vars and trends -- sensors and actuators are reset by EMS
     119         200 :     for (auto &v : state.dataPluginManager->globalVariableValues) {
     120          58 :         v = 0;
     121         142 :     }
     122             :     // reinitialize trend variables so old data are purged
     123         148 :     for (auto &tr : state.dataPluginManager->trends) {
     124           6 :         tr.reset();
     125         142 :     }
     126         142 : }
     127             : 
     128        2600 : int PluginManager::numActiveCallbacks(const EnergyPlusData &state)
     129             : {
     130        2600 :     return static_cast<int>(state.dataPluginManager->callbacks.size() + state.dataPluginManager->userDefinedCallbacks.size());
     131             : }
     132             : 
     133     5341042 : void runAnyRegisteredCallbacks(EnergyPlusData &state, EMSManager::EMSCallFrom const iCalledFrom, bool &anyRan)
     134             : {
     135     5341042 :     if (state.dataGlobal->KickOffSimulation) return;
     136     5330961 :     for (auto const &cb : state.dataPluginManager->callbacks[iCalledFrom]) {
     137           0 :         if (iCalledFrom == EMSManager::EMSCallFrom::UserDefinedComponentModel) {
     138           0 :             continue; // these are called -intentionally- using the runSingleUserDefinedCallback method
     139             :         }
     140           0 :         cb(&state);
     141           0 :         anyRan = true;
     142     5330961 :     }
     143             : #if LINK_WITH_PYTHON
     144     8378002 :     for (auto &plugin : state.dataPluginManager->plugins) {
     145     3047041 :         if (plugin.runDuringWarmup || !state.dataGlobal->WarmupFlag) {
     146     2775425 :             if (plugin.run(state, iCalledFrom)) anyRan = true;
     147             :         }
     148     5330961 :     }
     149             : #endif
     150             : }
     151             : 
     152             : #if LINK_WITH_PYTHON
     153         796 : std::string pythonStringForUsage(const EnergyPlusData &state)
     154             : {
     155         796 :     if (state.dataGlobal->errorCallback) {
     156           0 :         return "Python Version not accessible during API calls";
     157             :     }
     158         796 :     std::string sVersion = Py_GetVersion();
     159             :     // 3.8.3 (default, Jun  2 2020, 15:25:16) \n[GCC 7.5.0]
     160             :     // Remove the '\n'
     161         796 :     sVersion.erase(std::remove(sVersion.begin(), sVersion.end(), '\n'), sVersion.end());
     162        1592 :     return "Linked to Python Version: \"" + sVersion + "\"";
     163         796 : }
     164             : #else
     165             : std::string pythonStringForUsage([[maybe_unused]] const EnergyPlusData &state)
     166             : {
     167             :     return "This version of EnergyPlus not linked to Python library.";
     168             : }
     169             : #endif
     170             : 
     171         795 : void PluginManager::setupOutputVariables(EnergyPlusData &state)
     172             : {
     173             : #if LINK_WITH_PYTHON
     174             :     // with the PythonPlugin:Variables all set in memory, we can now set them up as outputs as needed
     175         795 :     std::string const sOutputVariable = "PythonPlugin:OutputVariable";
     176         795 :     int const outputVarInstances = state.dataInputProcessing->inputProcessor->getNumObjectsFound(state, sOutputVariable);
     177         795 :     if (outputVarInstances > 0) {
     178          11 :         auto const instances = state.dataInputProcessing->inputProcessor->epJSON.find(sOutputVariable);
     179          11 :         if (instances == state.dataInputProcessing->inputProcessor->epJSON.end()) {
     180             :             ShowSevereError(state, format("{}: Somehow getNumObjectsFound was > 0 but epJSON.find found 0", sOutputVariable)); // LCOV_EXCL_LINE
     181             :         }
     182          11 :         auto &instancesValue = instances.value();
     183          28 :         for (auto instance = instancesValue.begin(); instance != instancesValue.end(); ++instance) {
     184          17 :             auto const &fields = instance.value();
     185          17 :             std::string const &thisObjectName = instance.key();
     186          17 :             std::string const objNameUC = Util::makeUPPER(thisObjectName);
     187             :             // no need to validate name, the JSON will validate that.
     188          17 :             state.dataInputProcessing->inputProcessor->markObjectAsUsed(sOutputVariable, thisObjectName);
     189          34 :             std::string varName = fields.at("python_plugin_variable_name").get<std::string>();
     190          34 :             std::string avgOrSum = Util::makeUPPER(fields.at("type_of_data_in_variable").get<std::string>());
     191          34 :             std::string updateFreq = Util::makeUPPER(fields.at("update_frequency").get<std::string>());
     192          17 :             std::string units;
     193          17 :             if (fields.find("units") != fields.end()) {
     194          13 :                 units = fields.at("units").get<std::string>();
     195             :             }
     196             :             // get the index of the global variable, fatal if it doesn't mach one
     197             :             // validate type of data, update frequency, and look up units enum value
     198             :             // call setup output variable - variable TYPE is "PythonPlugin:OutputVariable"
     199          17 :             int variableHandle = EnergyPlus::PluginManagement::PluginManager::getGlobalVariableHandle(state, varName);
     200          17 :             if (variableHandle == -1) {
     201           0 :                 ShowSevereError(state, "Failed to match Python Plugin Output Variable");
     202           0 :                 ShowContinueError(state, format("Trying to create output instance for variable name \"{}\"", varName));
     203           0 :                 ShowContinueError(state, "No match found, make sure variable is listed in PythonPlugin:Variables object");
     204           0 :                 ShowFatalError(state, "Python Plugin Output Variable problem causes program termination");
     205             :             }
     206          17 :             bool isMetered = false;
     207          17 :             OutputProcessor::StoreType sAvgOrSum = OutputProcessor::StoreType::Average;
     208          17 :             if (avgOrSum == "SUMMED") {
     209           0 :                 sAvgOrSum = OutputProcessor::StoreType::Sum;
     210          17 :             } else if (avgOrSum == "METERED") {
     211           2 :                 sAvgOrSum = OutputProcessor::StoreType::Sum;
     212           2 :                 isMetered = true;
     213             :             }
     214          17 :             OutputProcessor::TimeStepType sUpdateFreq = OutputProcessor::TimeStepType::Zone;
     215          17 :             if (updateFreq == "SYSTEMTIMESTEP") {
     216           9 :                 sUpdateFreq = OutputProcessor::TimeStepType::System;
     217             :             }
     218          17 :             Constant::Units thisUnit = Constant::Units::None;
     219          17 :             if (!units.empty()) {
     220          13 :                 thisUnit = static_cast<Constant::Units>(getEnumValue(Constant::unitNamesUC, Util::makeUPPER(units)));
     221          13 :                 if (thisUnit == Constant::Units::Invalid) {
     222           4 :                     thisUnit = Constant::Units::customEMS;
     223             :                 }
     224             :             }
     225          17 :             if (!isMetered) {
     226             :                 // regular output variable, ignore the meter/resource stuff and register the variable
     227          15 :                 if (thisUnit != Constant::Units::customEMS) {
     228          22 :                     SetupOutputVariable(state,
     229             :                                         sOutputVariable,
     230             :                                         thisUnit,
     231          11 :                                         state.dataPluginManager->globalVariableValues[variableHandle],
     232             :                                         sUpdateFreq,
     233             :                                         sAvgOrSum,
     234             :                                         thisObjectName);
     235             :                 } else {
     236           8 :                     SetupOutputVariable(state,
     237             :                                         sOutputVariable,
     238             :                                         thisUnit,
     239           4 :                                         state.dataPluginManager->globalVariableValues[variableHandle],
     240             :                                         sUpdateFreq,
     241             :                                         sAvgOrSum,
     242             :                                         thisObjectName,
     243             :                                         Constant::eResource::Invalid,
     244             :                                         OutputProcessor::Group::Invalid,
     245             :                                         OutputProcessor::EndUseCat::Invalid,
     246             :                                         "", // EndUseSub
     247             :                                         "", // Zone
     248             :                                         1,
     249             :                                         1,
     250             :                                         "", // SpaceType
     251             :                                         -999,
     252             :                                         units);
     253             :                 }
     254             :             } else {
     255             :                 // We are doing a metered type, we need to get the extra stuff
     256             :                 // Resource Type
     257           2 :                 if (fields.find("resource_type") == fields.end()) {
     258           0 :                     ShowSevereError(state, format("Input error on PythonPlugin:OutputVariable = {}", thisObjectName));
     259           0 :                     ShowContinueError(state, "The variable was marked as metered, but did not define a resource type");
     260           0 :                     ShowContinueError(state, "For metered variables, the resource type, group type, and end use category must be defined");
     261           0 :                     ShowFatalError(state, "Input error on PythonPlugin:OutputVariable causes program termination");
     262             :                 }
     263           4 :                 std::string const resourceType = EnergyPlus::Util::makeUPPER(fields.at("resource_type").get<std::string>());
     264             :                 Constant::eResource resource;
     265           2 :                 if (resourceType == "WATERUSE") {
     266           0 :                     resource = Constant::eResource::Water;
     267           2 :                 } else if (resourceType == "ONSITEWATERPRODUCED") {
     268           0 :                     resource = Constant::eResource::OnSiteWater;
     269           2 :                 } else if (resourceType == "MAINSWATERSUPPLY") {
     270           0 :                     resource = Constant::eResource::MainsWater;
     271           2 :                 } else if (resourceType == "RAINWATERCOLLECTED") {
     272           0 :                     resource = Constant::eResource::RainWater;
     273           2 :                 } else if (resourceType == "WELLWATERDRAWN") {
     274           0 :                     resource = Constant::eResource::WellWater;
     275           2 :                 } else if (resourceType == "CONDENSATEWATERCOLLECTED") {
     276           0 :                     resource = Constant::eResource::Condensate;
     277           2 :                 } else if (resourceType == "ELECTRICITYPRODUCEDONSITE") {
     278           0 :                     resource = Constant::eResource::ElectricityProduced;
     279           2 :                 } else if (resourceType == "SOLARWATERHEATING") {
     280           0 :                     resource = Constant::eResource::SolarWater;
     281           2 :                 } else if (resourceType == "SOLARAIRHEATING") {
     282           0 :                     resource = Constant::eResource::SolarAir;
     283           2 :                 } else if ((resource = static_cast<Constant::eResource>(getEnumValue(Constant::eResourceNamesUC, resourceType))) ==
     284             :                            Constant::eResource::Invalid) {
     285           0 :                     ShowSevereError(state, format("Invalid input for PythonPlugin:OutputVariable, unexpected Resource Type = {}", resourceType));
     286           0 :                     ShowFatalError(state, "Python plugin output variable input problem causes program termination");
     287             :                 }
     288             : 
     289             :                 // Group Type
     290           2 :                 if (fields.find("group_type") == fields.end()) {
     291           0 :                     ShowSevereError(state, format("Input error on PythonPlugin:OutputVariable = {}", thisObjectName));
     292           0 :                     ShowContinueError(state, "The variable was marked as metered, but did not define a group type");
     293           0 :                     ShowContinueError(state, "For metered variables, the resource type, group type, and end use category must be defined");
     294           0 :                     ShowFatalError(state, "Input error on PythonPlugin:OutputVariable causes program termination");
     295             :                 }
     296           4 :                 std::string const groupType = EnergyPlus::Util::makeUPPER(fields.at("group_type").get<std::string>());
     297           2 :                 auto group = static_cast<OutputProcessor::Group>(getEnumValue(OutputProcessor::groupNamesUC, groupType));
     298           2 :                 if (group == OutputProcessor::Group::Invalid) {
     299           0 :                     ShowSevereError(state, format("Invalid input for PythonPlugin:OutputVariable, unexpected Group Type = {}", groupType));
     300           0 :                     ShowFatalError(state, "Python plugin output variable input problem causes program termination");
     301             :                 }
     302             : 
     303             :                 // End Use Type
     304           2 :                 if (fields.find("end_use_category") == fields.end()) {
     305           0 :                     ShowSevereError(state, format("Input error on PythonPlugin:OutputVariable = {}", thisObjectName));
     306           0 :                     ShowContinueError(state, "The variable was marked as metered, but did not define an end-use category");
     307           0 :                     ShowContinueError(state, "For metered variables, the resource type, group type, and end use category must be defined");
     308           0 :                     ShowFatalError(state, "Input error on PythonPlugin:OutputVariable causes program termination");
     309             :                 }
     310           4 :                 std::string const endUse = EnergyPlus::Util::makeUPPER(fields.at("end_use_category").get<std::string>());
     311           2 :                 auto endUseCat = static_cast<OutputProcessor::EndUseCat>(getEnumValue(OutputProcessor::endUseCatNamesUC, endUse));
     312             : 
     313           2 :                 if (endUseCat == OutputProcessor::EndUseCat::Invalid) {
     314           0 :                     ShowSevereError(state, format("Invalid input for PythonPlugin:OutputVariable, unexpected End-use Subcategory = {}", endUse));
     315           0 :                     ShowFatalError(state, "Python plugin output variable input problem causes program termination");
     316             :                 }
     317             : 
     318             :                 // Additional End Use Types Only Used for EnergyTransfer
     319           2 :                 if ((resource != Constant::eResource::EnergyTransfer) &&
     320           2 :                     (endUseCat == OutputProcessor::EndUseCat::HeatingCoils || endUseCat == OutputProcessor::EndUseCat::CoolingCoils ||
     321           2 :                      endUseCat == OutputProcessor::EndUseCat::Chillers || endUseCat == OutputProcessor::EndUseCat::Boilers ||
     322           2 :                      endUseCat == OutputProcessor::EndUseCat::Baseboard || endUseCat == OutputProcessor::EndUseCat::HeatRecoveryForCooling ||
     323             :                      endUseCat == OutputProcessor::EndUseCat::HeatRecoveryForHeating)) {
     324           0 :                     ShowWarningError(state, format("Inconsistent resource type input for PythonPlugin:OutputVariable = {}", thisObjectName));
     325           0 :                     ShowContinueError(state, format("For end use subcategory = {}, resource type must be EnergyTransfer", endUse));
     326           0 :                     ShowContinueError(state, "Resource type is being reset to EnergyTransfer and the simulation continues...");
     327           0 :                     resource = Constant::eResource::EnergyTransfer;
     328             :                 }
     329             : 
     330           2 :                 std::string sEndUseSubcategory;
     331           2 :                 if (fields.find("end_use_subcategory") != fields.end()) {
     332           2 :                     sEndUseSubcategory = fields.at("end_use_subcategory").get<std::string>();
     333             :                 }
     334             : 
     335           2 :                 if (sEndUseSubcategory.empty()) { // no subcategory
     336           0 :                     SetupOutputVariable(state,
     337             :                                         sOutputVariable,
     338             :                                         thisUnit,
     339           0 :                                         state.dataPluginManager->globalVariableValues[variableHandle],
     340             :                                         sUpdateFreq,
     341             :                                         sAvgOrSum,
     342             :                                         thisObjectName,
     343             :                                         resource,
     344             :                                         group,
     345             :                                         endUseCat);
     346             :                 } else { // has subcategory
     347           4 :                     SetupOutputVariable(state,
     348             :                                         sOutputVariable,
     349             :                                         thisUnit,
     350           2 :                                         state.dataPluginManager->globalVariableValues[variableHandle],
     351             :                                         sUpdateFreq,
     352             :                                         sAvgOrSum,
     353             :                                         thisObjectName,
     354             :                                         resource,
     355             :                                         group,
     356             :                                         endUseCat,
     357             :                                         sEndUseSubcategory);
     358             :                 }
     359           2 :             }
     360          28 :         } // for (instance)
     361          11 :     }     // if (OutputVarInstances > 0)
     362             : #endif
     363         795 : } // setupOutputVariables()
     364             : 
     365             : #if LINK_WITH_PYTHON
     366          22 : void initPython(EnergyPlusData &state, fs::path const &pathToPythonPackages)
     367             : {
     368             :     PyStatus status;
     369             : 
     370             :     // first pre-config Python so that it can speak UTF-8
     371             :     PyPreConfig preConfig;
     372             :     // This is the other related line that caused Decent CI to start having trouble.  I'm putting it back to
     373             :     // PyPreConfig_InitPythonConfig, even though I think it should be isolated.  Will deal with this after IO freeze.
     374          22 :     PyPreConfig_InitPythonConfig(&preConfig);
     375             :     // PyPreConfig_InitIsolatedConfig(&preConfig);
     376          22 :     preConfig.utf8_mode = 1;
     377          22 :     status = Py_PreInitialize(&preConfig);
     378          22 :     if (PyStatus_Exception(status)) {
     379           0 :         ShowFatalError(state, fmt::format("Could not pre-initialize Python to speak UTF-8... {}", status));
     380             :     }
     381             : 
     382             :     PyConfig config;
     383          22 :     PyConfig_InitIsolatedConfig(&config);
     384          22 :     config.isolated = 1;
     385             : 
     386          22 :     status = PyConfig_SetBytesString(&config, &config.program_name, PluginManagement::programName);
     387          22 :     if (PyStatus_Exception(status)) {
     388           0 :         ShowFatalError(state, fmt::format("Could not initialize program_name on PyConfig... {}", status));
     389             :     }
     390             : 
     391          22 :     status = PyConfig_Read(&config);
     392          22 :     if (PyStatus_Exception(status)) {
     393           0 :         ShowFatalError(state, fmt::format("Could not read back the PyConfig... {}", status));
     394             :     }
     395             : 
     396             :     // ReSharper disable once CppRedundantTypenameKeyword
     397             :     if constexpr (std::is_same_v<typename fs::path::value_type, wchar_t>) {
     398             :         // PyConfig_SetString copies the wide character string str into *config_str.
     399             :         // ReSharper disable once CppDFAUnreachableCode
     400             :         std::wstring const ws = pathToPythonPackages.generic_wstring();
     401             :         const wchar_t *wcharPath = ws.c_str();
     402             : 
     403             :         status = PyConfig_SetString(&config, &config.home, wcharPath);
     404             :         if (PyStatus_Exception(status)) {
     405             :             ShowFatalError(state, fmt::format("Could not set home to {:g} on PyConfig... {}", pathToPythonPackages, status));
     406             :         }
     407             :         status = PyConfig_SetString(&config, &config.base_prefix, wcharPath);
     408             :         if (PyStatus_Exception(status)) {
     409             :             ShowFatalError(state, fmt::format("Could not set base_prefix to {:g} on PyConfig... {}", pathToPythonPackages, status));
     410             :         }
     411             :         config.module_search_paths_set = 1;
     412             :         status = PyWideStringList_Append(&config.module_search_paths, wcharPath);
     413             :         if (PyStatus_Exception(status)) {
     414             :             ShowFatalError(state, fmt::format("Could not add {:g} to module_search_paths on PyConfig... {}", pathToPythonPackages, status));
     415             :         }
     416             : 
     417             :     } else {
     418             :         // PyConfig_SetBytesString takes a `const char * str` and decodes str using Py_DecodeLocale() and set the result into *config_str
     419             :         // But we want to avoid doing it three times, so we PyDecodeLocale manually
     420             :         // Py_DecodeLocale can be called because Python has been PreInitialized.
     421          22 :         wchar_t *wcharPath = Py_DecodeLocale(pathToPythonPackages.generic_string().c_str(), nullptr); // This allocates!
     422             : 
     423          22 :         status = PyConfig_SetString(&config, &config.home, wcharPath);
     424          22 :         if (PyStatus_Exception(status)) {
     425           0 :             ShowFatalError(state, fmt::format("Could not set home to {:g} on PyConfig... {}", pathToPythonPackages, status));
     426             :         }
     427          22 :         status = PyConfig_SetString(&config, &config.base_prefix, wcharPath);
     428          22 :         if (PyStatus_Exception(status)) {
     429           0 :             ShowFatalError(state, fmt::format("Could not set base_prefix to {:g} on PyConfig... {}", pathToPythonPackages, status));
     430             :         }
     431          22 :         config.module_search_paths_set = 1;
     432          22 :         status = PyWideStringList_Append(&config.module_search_paths, wcharPath);
     433          22 :         if (PyStatus_Exception(status)) {
     434           0 :             ShowFatalError(state, fmt::format("Could not add {:g} to module_search_paths on PyConfig... {}", pathToPythonPackages, status));
     435             :         }
     436             : 
     437          22 :         PyMem_RawFree(wcharPath);
     438             :     }
     439             : 
     440             :     // This was Py_InitializeFromConfig(&config), but was giving a seg fault when running inside
     441             :     // another Python instance, for example as part of an API run.  Per the example here:
     442             :     // https://docs.python.org/3/c-api/init_config.html#preinitialize-python-with-pypreconfig
     443             :     // It looks like we don't need to initialize from config again, it should be all set up with
     444             :     // the init calls above, so just initialize and move on.
     445             :     // UPDATE: This worked happily for me on Linux, and also when I build locally on Windows, but not on Decent CI
     446             :     // I suspect a difference in behavior for Python versions.  I'm going to temporarily revert this back to initialize
     447             :     // with config and get IO freeze going, then get back to solving it.
     448             :     // Py_Initialize();
     449          22 :     Py_InitializeFromConfig(&config);
     450          22 : }
     451             : #endif // LINK_WITH_PYTHON
     452             : 
     453         796 : PluginManager::PluginManager(EnergyPlusData &state) : eplusRunningViaPythonAPI(state.dataPluginManager->eplusRunningViaPythonAPI)
     454             : {
     455             :     // Now read all the actual plugins and interpret them
     456             :     // IMPORTANT -- DO NOT CALL setup() UNTIL ALL INSTANCES ARE DONE
     457         796 :     std::string const sPlugins = "PythonPlugin:Instance";
     458         796 :     if (state.dataInputProcessing->inputProcessor->getNumObjectsFound(state, sPlugins) == 0) {
     459         774 :         return;
     460             :     }
     461             : 
     462             : #if LINK_WITH_PYTHON
     463             :     // we'll need the program directory for a few things so get it once here at the top and sanitize it
     464          22 :     fs::path programDir;
     465          22 :     if (state.dataGlobal->installRootOverride) {
     466           0 :         programDir = state.dataStrGlobals->exeDirectoryPath;
     467             :     } else {
     468          22 :         programDir = FileSystem::getParentDirectoryPath(FileSystem::getAbsolutePath(FileSystem::getProgramPath()));
     469             :     }
     470          22 :     fs::path const pathToPythonPackages = programDir / "python_standard_lib";
     471             : 
     472          22 :     initPython(state, pathToPythonPackages);
     473             : 
     474             :     // Take control of the global interpreter lock while we are here, make sure to release it...
     475          22 :     PyGILState_STATE gil = PyGILState_Ensure();
     476             : 
     477             :     // call this once to allow us to add to, and report, sys.path later as needed
     478          22 :     PyRun_SimpleString("import sys"); // allows us to report sys.path later
     479             : 
     480             :     // we also need to set an extra import path to find some dynamic library loading stuff, again make it relative to the binary
     481          22 :     addToPythonPath(state, programDir / "python_standard_lib/lib-dynload", false);
     482             : 
     483             :     // now for additional paths:
     484             :     // we'll always want to add the program executable directory to PATH so that Python can find the installed pyenergyplus package
     485             :     // we will then optionally add the current working directory to allow Python to find scripts in the current directory
     486             :     // we will then optionally add the directory of the running IDF to allow Python to find scripts kept next to the IDF
     487             :     // we will then optionally add any additional paths the user specifies on the search paths object
     488             : 
     489             :     // so add the executable directory here
     490          22 :     addToPythonPath(state, programDir, false);
     491             : 
     492             :     // Read all the additional search paths next
     493          22 :     std::string const sPaths = "PythonPlugin:SearchPaths";
     494          22 :     int searchPaths = state.dataInputProcessing->inputProcessor->getNumObjectsFound(state, sPaths);
     495          22 :     if (searchPaths > 0) {
     496           0 :         auto const instances = state.dataInputProcessing->inputProcessor->epJSON.find(sPaths);
     497           0 :         if (instances == state.dataInputProcessing->inputProcessor->epJSON.end()) {
     498             :             ShowSevereError(state,                                                                                   // LCOV_EXCL_LINE
     499             :                             "PythonPlugin:SearchPaths: Somehow getNumObjectsFound was > 0 but epJSON.find found 0"); // LCOV_EXCL_LINE
     500             :         }
     501           0 :         auto &instancesValue = instances.value();
     502           0 :         for (auto instance = instancesValue.begin(); instance != instancesValue.end(); ++instance) {
     503             :             // This is a unique object, so we should have one, but this is fine
     504           0 :             auto const &fields = instance.value();
     505           0 :             std::string const &thisObjectName = instance.key();
     506           0 :             state.dataInputProcessing->inputProcessor->markObjectAsUsed(sPaths, thisObjectName);
     507           0 :             std::string workingDirFlagUC = "YES";
     508             :             try {
     509           0 :                 workingDirFlagUC = EnergyPlus::Util::makeUPPER(fields.at("add_current_working_directory_to_search_path").get<std::string>());
     510           0 :             } catch ([[maybe_unused]] nlohmann::json::out_of_range &e) {
     511             :                 // defaulted to YES
     512           0 :             }
     513           0 :             if (workingDirFlagUC == "YES") {
     514           0 :                 addToPythonPath(state, ".", false);
     515             :             }
     516           0 :             std::string inputFileDirFlagUC = "YES";
     517             :             try {
     518           0 :                 inputFileDirFlagUC = EnergyPlus::Util::makeUPPER(fields.at("add_input_file_directory_to_search_path").get<std::string>());
     519           0 :             } catch ([[maybe_unused]] nlohmann::json::out_of_range &e) {
     520             :                 // defaulted to YES
     521           0 :             }
     522           0 :             if (inputFileDirFlagUC == "YES") {
     523           0 :                 addToPythonPath(state, state.dataStrGlobals->inputDirPath, false);
     524             :             }
     525             : 
     526           0 :             std::string epInDirFlagUC = "YES";
     527             :             try {
     528           0 :                 epInDirFlagUC = EnergyPlus::Util::makeUPPER(fields.at("add_epin_environment_variable_to_search_path").get<std::string>());
     529           0 :             } catch ([[maybe_unused]] nlohmann::json::out_of_range &e) {
     530             :                 // defaulted to YES
     531           0 :             }
     532           0 :             if (epInDirFlagUC == "YES") {
     533           0 :                 std::string epin_path; // NOLINT(misc-const-correctness)
     534           0 :                 get_environment_variable("epin", epin_path);
     535           0 :                 fs::path const epinPathObject = fs::path(epin_path);
     536           0 :                 if (epinPathObject.empty()) {
     537           0 :                     ShowWarningMessage(
     538             :                         state,
     539             :                         "PluginManager: Search path inputs requested adding epin variable to Python path, but epin variable was empty, skipping.");
     540             :                 } else {
     541           0 :                     fs::path const epinRootDir = FileSystem::getParentDirectoryPath(fs::path(epinPathObject));
     542           0 :                     if (FileSystem::pathExists(epinRootDir)) {
     543           0 :                         addToPythonPath(state, epinRootDir, true);
     544             :                     } else {
     545           0 :                         ShowWarningMessage(state,
     546             :                                            "PluginManager: Search path inputs requested adding epin variable to Python path, but epin "
     547             :                                            "variable value is not a valid existent path, skipping.");
     548             :                     }
     549           0 :                 }
     550           0 :             }
     551             : 
     552             :             try {
     553           0 :                 auto const &vars = fields.at("py_search_paths");
     554           0 :                 for (const auto &var : vars) {
     555             :                     try {
     556           0 :                         addToPythonPath(state, fs::path(var.at("search_path").get<std::string>()), true);
     557           0 :                     } catch ([[maybe_unused]] nlohmann::json::out_of_range &e) {
     558             :                         // empty entry
     559           0 :                     }
     560           0 :                 }
     561           0 :             } catch ([[maybe_unused]] nlohmann::json::out_of_range &e) {
     562             :                 // catch when no paths are passed
     563             :                 // nothing to do here
     564           0 :             }
     565           0 :         }
     566           0 :     } else {
     567             :         // no search path objects in the IDF, just do the default behavior: add the current working dir and the input file dir, + epin env var
     568          22 :         addToPythonPath(state, ".", false);
     569          22 :         addToPythonPath(state, state.dataStrGlobals->inputDirPath, false);
     570             : 
     571          22 :         std::string epin_path; // NOLINT(misc-const-correctness)
     572          22 :         get_environment_variable("epin", epin_path);
     573          22 :         fs::path const epinPathObject = fs::path(epin_path);
     574          22 :         if (!epinPathObject.empty()) {
     575           0 :             fs::path const epinRootDir = FileSystem::getParentDirectoryPath(fs::path(epinPathObject));
     576           0 :             if (FileSystem::pathExists(epinRootDir)) {
     577           0 :                 addToPythonPath(state, epinRootDir, true);
     578             :             }
     579           0 :         }
     580          22 :     }
     581             : 
     582             :     // Now read all the actual plugins and interpret them
     583             :     // IMPORTANT -- DO NOT CALL setup() UNTIL ALL INSTANCES ARE DONE
     584             :     {
     585          22 :         auto const instances = state.dataInputProcessing->inputProcessor->epJSON.find(sPlugins);
     586          22 :         if (instances == state.dataInputProcessing->inputProcessor->epJSON.end()) {
     587             :             ShowSevereError(state,                                                                                // LCOV_EXCL_LINE
     588             :                             "PythonPlugin:Instance: Somehow getNumObjectsFound was > 0 but epJSON.find found 0"); // LCOV_EXCL_LINE
     589             :         }
     590          22 :         auto &instancesValue = instances.value();
     591          67 :         for (auto instance = instancesValue.begin(); instance != instancesValue.end(); ++instance) {
     592          45 :             auto const &fields = instance.value();
     593          45 :             std::string const &thisObjectName = instance.key();
     594          45 :             state.dataInputProcessing->inputProcessor->markObjectAsUsed(sPlugins, thisObjectName);
     595          90 :             fs::path modulePath(fields.at("python_module_name").get<std::string>());
     596          90 :             std::string className = fields.at("plugin_class_name").get<std::string>();
     597          90 :             std::string const sWarmup = EnergyPlus::Util::makeUPPER(fields.at("run_during_warmup_days").get<std::string>());
     598          45 :             bool const warmup = (sWarmup == "YES");
     599          45 :             state.dataPluginManager->plugins.emplace_back(modulePath, className, thisObjectName, warmup);
     600          67 :         }
     601          22 :     }
     602             : 
     603             :     // IMPORTANT - CALL setup() HERE ONCE ALL INSTANCES ARE CONSTRUCTED TO AVOID DESTRUCTOR/MEMORY ISSUES DURING VECTOR RESIZING
     604          67 :     for (auto &plugin : state.dataPluginManager->plugins) {
     605          45 :         plugin.setup(state);
     606          22 :     }
     607             : 
     608          22 :     std::string const sGlobals = "PythonPlugin:Variables";
     609          22 :     int const globalVarInstances = state.dataInputProcessing->inputProcessor->getNumObjectsFound(state, sGlobals);
     610          22 :     if (globalVarInstances > 0) {
     611          15 :         auto const instances = state.dataInputProcessing->inputProcessor->epJSON.find(sGlobals);
     612          15 :         if (instances == state.dataInputProcessing->inputProcessor->epJSON.end()) {
     613             :             ShowSevereError(state, format("{}: Somehow getNumObjectsFound was > 0 but epJSON.find found 0", sGlobals)); // LCOV_EXCL_LINE
     614             :         }
     615          15 :         std::set<std::string> uniqueNames;
     616          15 :         auto &instancesValue = instances.value();
     617          31 :         for (auto instance = instancesValue.begin(); instance != instancesValue.end(); ++instance) {
     618          16 :             auto const &fields = instance.value();
     619          16 :             std::string const &thisObjectName = instance.key();
     620          16 :             state.dataInputProcessing->inputProcessor->markObjectAsUsed(sGlobals, thisObjectName);
     621          16 :             auto const &vars = fields.at("global_py_vars");
     622          47 :             for (const auto &var : vars) {
     623          62 :                 std::string const varNameToAdd = var.at("variable_name").get<std::string>();
     624          31 :                 if (uniqueNames.find(varNameToAdd) == uniqueNames.end()) {
     625          31 :                     this->addGlobalVariable(state, varNameToAdd);
     626          31 :                     uniqueNames.insert(varNameToAdd);
     627             :                 } else {
     628           0 :                     ShowWarningMessage(state,
     629           0 :                                        format("Found duplicate variable name in PythonPLugin:Variables objects, ignoring: \"{}\"", varNameToAdd));
     630             :                 }
     631          47 :             }
     632          15 :         }
     633          15 :     }
     634             : 
     635             :     // PythonPlugin:TrendVariable,
     636             :     //       \memo This object sets up a Python plugin trend variable from an Python plugin variable
     637             :     //       \memo A trend variable logs values across timesteps
     638             :     //       \min-fields 3
     639             :     //  A1 , \field Name
     640             :     //       \required-field
     641             :     //       \type alpha
     642             :     //  A2 , \field Name of a Python Plugin Variable
     643             :     //       \required-field
     644             :     //       \type alpha
     645             :     //  N1 ; \field Number of Timesteps to be Logged
     646             :     //       \required-field
     647             :     //       \type integer
     648             :     //       \minimum 1
     649          22 :     std::string const sTrends = "PythonPlugin:TrendVariable";
     650          22 :     int const trendInstances = state.dataInputProcessing->inputProcessor->getNumObjectsFound(state, sTrends);
     651          22 :     if (trendInstances > 0) {
     652           3 :         auto const instances = state.dataInputProcessing->inputProcessor->epJSON.find(sTrends);
     653           3 :         if (instances == state.dataInputProcessing->inputProcessor->epJSON.end()) {
     654             :             ShowSevereError(state, format("{}: Somehow getNumObjectsFound was > 0 but epJSON.find found 0", sTrends)); // LCOV_EXCL_LINE
     655             :         }
     656           3 :         auto &instancesValue = instances.value();
     657           7 :         for (auto instance = instancesValue.begin(); instance != instancesValue.end(); ++instance) {
     658           4 :             auto const &fields = instance.value();
     659           4 :             std::string const &thisObjectName = EnergyPlus::Util::makeUPPER(instance.key());
     660           4 :             state.dataInputProcessing->inputProcessor->markObjectAsUsed(sGlobals, thisObjectName);
     661           8 :             std::string variableName = fields.at("name_of_a_python_plugin_variable").get<std::string>();
     662           4 :             int variableIndex = EnergyPlus::PluginManagement::PluginManager::getGlobalVariableHandle(state, variableName);
     663           4 :             int numValues = fields.at("number_of_timesteps_to_be_logged").get<int>();
     664           4 :             state.dataPluginManager->trends.emplace_back(state, thisObjectName, numValues, variableIndex);
     665           4 :             this->maxTrendVariableIndex++;
     666           7 :         }
     667           3 :     }
     668             : 
     669             :     // Release the global interpreter lock
     670          22 :     PyGILState_Release(gil);
     671             :     // setting up output variables deferred until later in the simulation setup process
     672             : #else
     673             :     // need to alert only if a plugin instance is found
     674             :     EnergyPlus::ShowFatalError(state, "Python Plugin instance found, but this build of EnergyPlus is not compiled with Python.");
     675             : #endif
     676         796 : }
     677             : 
     678         796 : PluginManager::~PluginManager()
     679             : {
     680             : #if LINK_WITH_PYTHON
     681         796 :     if (!this->eplusRunningViaPythonAPI) {
     682         796 :         if (Py_IsInitialized() != 0) {
     683          22 :             if (Py_FinalizeEx() < 0) {
     684           0 :                 exit(120);
     685             :             }
     686             :         }
     687             :     }
     688             : #endif // LINK_WITH_PYTHON
     689         796 : }
     690             : 
     691          45 : PluginInstance::PluginInstance(const fs::path &_modulePath, const std::string &_className, std::string emsName, bool runPluginDuringWarmup)
     692          45 :     : modulePath(_modulePath), className(_className), emsAlias(std::move(emsName)), runDuringWarmup(runPluginDuringWarmup),
     693          45 :       stringIdentifier(FileSystem::toString(_modulePath) + "." + _className)
     694             : {
     695          45 : }
     696             : 
     697           0 : void PluginInstance::reportPythonError([[maybe_unused]] EnergyPlusData &state)
     698             : {
     699             : #if LINK_WITH_PYTHON
     700           0 :     PyObject *exc_type = nullptr;
     701           0 :     PyObject *exc_value = nullptr;
     702           0 :     PyObject *exc_tb = nullptr;
     703           0 :     PyErr_Fetch(&exc_type, &exc_value, &exc_tb);
     704             :     // Normalizing the exception is needed. Without it, our custom EnergyPlusException go through just fine
     705             :     // but any ctypes built-in exception for eg will have wrong types
     706           0 :     PyErr_NormalizeException(&exc_type, &exc_value, &exc_tb);
     707           0 :     PyObject *str_exc_value = PyObject_Repr(exc_value); // Now a unicode object
     708           0 :     PyObject *pyStr2 = PyUnicode_AsEncodedString(str_exc_value, "utf-8", "Error ~");
     709             :     Py_DECREF(str_exc_value);
     710           0 :     char *strExcValue = PyBytes_AsString(pyStr2); // NOLINT(hicpp-signed-bitwise)
     711             :     Py_DECREF(pyStr2);
     712           0 :     ShowContinueError(state, "Python error description follows: ");
     713           0 :     ShowContinueError(state, strExcValue);
     714             : 
     715             :     // See if we can get a full traceback.
     716             :     // Calls into python, and does the same as capturing the exception in `e`
     717             :     // then `print(traceback.format_exception(e.type, e.value, e.tb))`
     718           0 :     PyObject *pModuleName = PyUnicode_DecodeFSDefault("traceback");
     719           0 :     PyObject *pyth_module = PyImport_Import(pModuleName);
     720             :     Py_DECREF(pModuleName);
     721             : 
     722           0 :     if (pyth_module == nullptr) {
     723           0 :         ShowContinueError(state, "Cannot find 'traceback' module in reportPythonError(), this is weird");
     724           0 :         return;
     725             :     }
     726             : 
     727           0 :     PyObject *pyth_func = PyObject_GetAttrString(pyth_module, "format_exception");
     728             :     Py_DECREF(pyth_module); // PyImport_Import returns a new reference, decrement it
     729             : 
     730           0 :     if (pyth_func || PyCallable_Check(pyth_func)) {
     731             : 
     732           0 :         PyObject *pyth_val = PyObject_CallFunction(pyth_func, "OOO", exc_type, exc_value, exc_tb);
     733             : 
     734             :         // traceback.format_exception returns a list, so iterate on that
     735           0 :         if (!pyth_val || !PyList_Check(pyth_val)) { // NOLINT(hicpp-signed-bitwise)
     736           0 :             ShowContinueError(state, "In reportPythonError(), traceback.format_exception did not return a list.");
     737           0 :             return;
     738             :         }
     739             : 
     740           0 :         Py_ssize_t numVals = PyList_Size(pyth_val);
     741           0 :         if (numVals == 0) {
     742           0 :             ShowContinueError(state, "No traceback available");
     743           0 :             return;
     744             :         }
     745             : 
     746           0 :         ShowContinueError(state, "Python traceback follows: ");
     747           0 :         ShowContinueError(state, "```");
     748             : 
     749           0 :         for (Py_ssize_t itemNum = 0; itemNum < numVals; itemNum++) {
     750           0 :             PyObject *item = PyList_GetItem(pyth_val, itemNum);
     751           0 :             if (PyUnicode_Check(item)) { // NOLINT(hicpp-signed-bitwise) -- something inside Python code causes warning
     752           0 :                 std::string traceback_line = PyUnicode_AsUTF8(item);
     753           0 :                 if (!traceback_line.empty() && traceback_line[traceback_line.length() - 1] == '\n') {
     754           0 :                     traceback_line.erase(traceback_line.length() - 1);
     755             :                 }
     756           0 :                 ShowContinueError(state, format(" >>> {}", traceback_line));
     757           0 :             }
     758             :             // PyList_GetItem returns a borrowed reference, do not decrement
     759             :         }
     760             : 
     761           0 :         ShowContinueError(state, "```");
     762             : 
     763             :         // PyList_Size returns a borrowed reference, do not decrement
     764             :         Py_DECREF(pyth_val); // PyObject_CallFunction returns new reference, decrement
     765             :     }
     766             :     Py_DECREF(pyth_func); // PyObject_GetAttrString returns a new reference, decrement it
     767             : #endif
     768             : }
     769             : 
     770          45 : void PluginInstance::setup([[maybe_unused]] EnergyPlusData &state)
     771             : {
     772             : #if LINK_WITH_PYTHON
     773             :     // this first section is really all about just ultimately getting a full Python class instance
     774             :     // this answer helped with a few things: https://ru.stackoverflow.com/a/785927
     775             : 
     776             :     PyObject *pModuleName;
     777             :     // ReSharper disable once CppRedundantTypenameKeyword
     778             :     if constexpr (std::is_same_v<typename fs::path::value_type, wchar_t>) {
     779             :         // ReSharper disable once CppDFAUnreachableCode
     780             :         const std::wstring ws = this->modulePath.generic_wstring();
     781             :         pModuleName = PyUnicode_FromWideChar(ws.c_str(), static_cast<Py_ssize_t>(ws.size())); // New reference
     782             :     } else {
     783          45 :         const std::string s = this->modulePath.generic_string();
     784          45 :         pModuleName = PyUnicode_FromString(s.c_str()); // New reference
     785          45 :     }
     786          45 :     if (pModuleName == nullptr) {
     787           0 :         ShowFatalError(state, format("Failed to convert the Module Path \"{:g}\" for import", this->modulePath));
     788             :     }
     789          45 :     this->pModule = PyImport_Import(pModuleName);
     790             :     Py_DECREF(pModuleName);
     791             : 
     792          45 :     if (!this->pModule) {
     793           0 :         ShowSevereError(state, format("Failed to import module \"{:g}\"", this->modulePath));
     794           0 :         ShowContinueError(state, format("Current sys.path={}", PluginManager::currentPythonPath()));
     795             :         // ONLY call PyErr_Print if PyErr has occurred, otherwise it will cause other problems
     796           0 :         if (PyErr_Occurred()) {
     797           0 :             reportPythonError(state);
     798             :         } else {
     799           0 :             ShowContinueError(state, "It could be that the module could not be found, or that there was an error in importing");
     800             :         }
     801           0 :         ShowFatalError(state, "Python import error causes program termination");
     802             :     }
     803          45 :     PyObject *pModuleDict = PyModule_GetDict(this->pModule);
     804          45 :     if (!pModuleDict) {
     805           0 :         ShowSevereError(state, format("Failed to read module dictionary from module \"{:g}\"", this->modulePath));
     806           0 :         if (PyErr_Occurred()) {
     807           0 :             reportPythonError(state);
     808             :         } else {
     809           0 :             ShowContinueError(state, "It could be that the module was empty");
     810             :         }
     811           0 :         ShowFatalError(state, "Python module error causes program termination");
     812             :     }
     813          45 :     std::string fileVarName = "__file__";
     814          45 :     PyObject *pFullPath = PyDict_GetItemString(pModuleDict, fileVarName.c_str());
     815          45 :     if (!pFullPath) {
     816             :         // something went really wrong, this should only happen if you do some *weird* python stuff like
     817             :         // import from database or something
     818           0 :         ShowFatalError(state, "Could not get full path");
     819             :     } else {
     820          45 :         const char *zStr = PyUnicode_AsUTF8(pFullPath);
     821          45 :         std::string sHere(zStr);
     822          45 :         ShowMessage(state, format("PythonPlugin: Class {} imported from: {}", className, sHere));
     823          45 :     }
     824          45 :     PyObject *pClass = PyDict_GetItemString(pModuleDict, className.c_str());
     825             :     // Py_DECREF(pModuleDict);  // PyModule_GetDict returns a borrowed reference, DO NOT decrement
     826          45 :     if (!pClass) {
     827           0 :         ShowSevereError(state, format(R"(Failed to get class type "{}" from module "{:g}")", className, modulePath));
     828           0 :         if (PyErr_Occurred()) {
     829           0 :             reportPythonError(state);
     830             :         } else {
     831           0 :             ShowContinueError(state, "It could be the class name is misspelled or missing.");
     832             :         }
     833           0 :         ShowFatalError(state, "Python class import error causes program termination");
     834             :     }
     835          45 :     if (!PyCallable_Check(pClass)) {
     836           0 :         ShowSevereError(state, format("Got class type \"{}\", but it cannot be called/instantiated", className));
     837           0 :         if (PyErr_Occurred()) {
     838           0 :             reportPythonError(state);
     839             :         } else {
     840           0 :             ShowContinueError(state, "Is it possible the class name is actually just a variable?");
     841             :         }
     842           0 :         ShowFatalError(state, "Python class check error causes program termination");
     843             :     }
     844          45 :     this->pClassInstance = PyObject_CallObject(pClass, nullptr);
     845             :     // Py_DECREF(pClass);  // PyDict_GetItemString returns a borrowed reference, DO NOT decrement
     846          45 :     if (!this->pClassInstance) {
     847           0 :         ShowSevereError(state, format("Something went awry calling class constructor for class \"{}\"", className));
     848           0 :         if (PyErr_Occurred()) {
     849           0 :             reportPythonError(state);
     850             :         } else {
     851           0 :             ShowContinueError(state, "It is possible the plugin class constructor takes extra arguments - it shouldn't.");
     852             :         }
     853           0 :         ShowFatalError(state, "Python class constructor error causes program termination");
     854             :     }
     855             :     // PyObject_CallObject returns a new reference, that we need to manage
     856             :     // I think we need to keep it around in memory though so the class methods can be called later on,
     857             :     // so I don't intend on decrementing it, at least not until the manager destructor
     858             :     // In any case, it will be an **extremely** tiny memory use if we hold onto it a bit too long
     859             : 
     860             :     // check which methods are overridden in the derived class
     861          45 :     std::string const detectOverriddenFunctionName = "_detect_overridden";
     862          45 :     PyObject *detectFunction = PyObject_GetAttrString(this->pClassInstance, detectOverriddenFunctionName.c_str());
     863          45 :     if (!detectFunction || !PyCallable_Check(detectFunction)) {
     864           0 :         ShowSevereError(
     865             :             state,
     866           0 :             format(R"(Could not find or call function "{}" on class "{:g}.{}")", detectOverriddenFunctionName, this->modulePath, this->className));
     867           0 :         if (PyErr_Occurred()) {
     868           0 :             reportPythonError(state);
     869             :         } else {
     870           0 :             ShowContinueError(state, "This function should be available on the base class, so this is strange.");
     871             :         }
     872           0 :         ShowFatalError(state, "Python _detect_overridden() function error causes program termination");
     873             :     }
     874          45 :     PyObject *pFunctionResponse = PyObject_CallFunction(detectFunction, nullptr);
     875             :     Py_DECREF(detectFunction); // PyObject_GetAttrString returns a new reference, decrement it
     876          45 :     if (!pFunctionResponse) {
     877           0 :         ShowSevereError(state, format("Call to _detect_overridden() on {} failed!", this->stringIdentifier));
     878           0 :         if (PyErr_Occurred()) {
     879           0 :             reportPythonError(state);
     880             :         } else {
     881           0 :             ShowContinueError(state, "This is available on the base class and should not be overridden...strange.");
     882             :         }
     883           0 :         ShowFatalError(state, format("Program terminates after call to _detect_overridden() on {} failed!", this->stringIdentifier));
     884             :     }
     885          45 :     if (!PyList_Check(pFunctionResponse)) { // NOLINT(hicpp-signed-bitwise)
     886           0 :         ShowFatalError(state, format("Invalid return from _detect_overridden() on class \"{}\", this is weird", this->stringIdentifier));
     887             :     }
     888          45 :     Py_ssize_t numVals = PyList_Size(pFunctionResponse);
     889             :     // at this point we know which base class methods are being overridden by the derived class
     890             :     // we can loop over them and based on the name check the appropriate flag and assign the function pointer
     891          45 :     if (numVals == 0) {
     892           0 :         ShowFatalError(state,
     893           0 :                        format("Python plugin \"{}\" did not override any base class methods; must override at least one", this->stringIdentifier));
     894             :     }
     895          91 :     for (Py_ssize_t itemNum = 0; itemNum < numVals; itemNum++) {
     896          46 :         PyObject *item = PyList_GetItem(pFunctionResponse, itemNum);
     897          46 :         if (PyUnicode_Check(item)) { // NOLINT(hicpp-signed-bitwise) -- something inside Python code causes warning
     898          46 :             std::string functionName = PyUnicode_AsUTF8(item);
     899          46 :             if (functionName == this->sHookBeginNewEnvironment) {
     900           1 :                 this->bHasBeginNewEnvironment = true;
     901           1 :                 this->pBeginNewEnvironment = PyUnicode_FromString(functionName.c_str());
     902          45 :             } else if (functionName == this->sHookBeginZoneTimestepBeforeSetCurrentWeather) {
     903           1 :                 this->bHasBeginZoneTimestepBeforeSetCurrentWeather = true;
     904           1 :                 this->pBeginZoneTimestepBeforeSetCurrentWeather = PyUnicode_FromString(functionName.c_str());
     905          44 :             } else if (functionName == this->sHookAfterNewEnvironmentWarmUpIsComplete) {
     906           0 :                 this->bHasAfterNewEnvironmentWarmUpIsComplete = true;
     907           0 :                 this->pAfterNewEnvironmentWarmUpIsComplete = PyUnicode_FromString(functionName.c_str());
     908          44 :             } else if (functionName == this->sHookBeginZoneTimestepBeforeInitHeatBalance) {
     909           1 :                 this->bHasBeginZoneTimestepBeforeInitHeatBalance = true;
     910           1 :                 this->pBeginZoneTimestepBeforeInitHeatBalance = PyUnicode_FromString(functionName.c_str());
     911          43 :             } else if (functionName == this->sHookBeginZoneTimestepAfterInitHeatBalance) {
     912           0 :                 this->bHasBeginZoneTimestepAfterInitHeatBalance = true;
     913           0 :                 this->pBeginZoneTimestepAfterInitHeatBalance = PyUnicode_FromString(functionName.c_str());
     914          43 :             } else if (functionName == this->sHookBeginTimestepBeforePredictor) {
     915          10 :                 this->bHasBeginTimestepBeforePredictor = true;
     916          10 :                 this->pBeginTimestepBeforePredictor = PyUnicode_FromString(functionName.c_str());
     917          33 :             } else if (functionName == this->sHookAfterPredictorBeforeHVACManagers) {
     918           0 :                 this->bHasAfterPredictorBeforeHVACManagers = true;
     919           0 :                 this->pAfterPredictorBeforeHVACManagers = PyUnicode_FromString(functionName.c_str());
     920          33 :             } else if (functionName == this->sHookAfterPredictorAfterHVACManagers) {
     921          17 :                 this->bHasAfterPredictorAfterHVACManagers = true;
     922          17 :                 this->pAfterPredictorAfterHVACManagers = PyUnicode_FromString(functionName.c_str());
     923          16 :             } else if (functionName == this->sHookInsideHVACSystemIterationLoop) {
     924           4 :                 this->bHasInsideHVACSystemIterationLoop = true;
     925           4 :                 this->pInsideHVACSystemIterationLoop = PyUnicode_FromString(functionName.c_str());
     926          12 :             } else if (functionName == this->sHookEndOfZoneTimestepBeforeZoneReporting) {
     927           5 :                 this->bHasEndOfZoneTimestepBeforeZoneReporting = true;
     928           5 :                 this->pEndOfZoneTimestepBeforeZoneReporting = PyUnicode_FromString(functionName.c_str());
     929           7 :             } else if (functionName == this->sHookEndOfZoneTimestepAfterZoneReporting) {
     930           0 :                 this->bHasEndOfZoneTimestepAfterZoneReporting = true;
     931           0 :                 this->pEndOfZoneTimestepAfterZoneReporting = PyUnicode_FromString(functionName.c_str());
     932           7 :             } else if (functionName == this->sHookEndOfSystemTimestepBeforeHVACReporting) {
     933           0 :                 this->bHasEndOfSystemTimestepBeforeHVACReporting = true;
     934           0 :                 this->pEndOfSystemTimestepBeforeHVACReporting = PyUnicode_FromString(functionName.c_str());
     935           7 :             } else if (functionName == this->sHookEndOfSystemTimestepAfterHVACReporting) {
     936           0 :                 this->bHasEndOfSystemTimestepAfterHVACReporting = true;
     937           0 :                 this->pEndOfSystemTimestepAfterHVACReporting = PyUnicode_FromString(functionName.c_str());
     938           7 :             } else if (functionName == this->sHookEndOfZoneSizing) {
     939           0 :                 this->bHasEndOfZoneSizing = true;
     940           0 :                 this->pEndOfZoneSizing = PyUnicode_FromString(functionName.c_str());
     941           7 :             } else if (functionName == this->sHookEndOfSystemSizing) {
     942           1 :                 this->bHasEndOfSystemSizing = true;
     943           1 :                 this->pEndOfSystemSizing = PyUnicode_FromString(functionName.c_str());
     944           6 :             } else if (functionName == this->sHookAfterComponentInputReadIn) {
     945           0 :                 this->bHasAfterComponentInputReadIn = true;
     946           0 :                 this->pAfterComponentInputReadIn = PyUnicode_FromString(functionName.c_str());
     947           6 :             } else if (functionName == this->sHookUserDefinedComponentModel) {
     948           6 :                 this->bHasUserDefinedComponentModel = true;
     949           6 :                 this->pUserDefinedComponentModel = PyUnicode_FromString(functionName.c_str());
     950           0 :             } else if (functionName == this->sHookUnitarySystemSizing) {
     951           0 :                 this->bHasUnitarySystemSizing = true;
     952           0 :                 this->pUnitarySystemSizing = PyUnicode_FromString(functionName.c_str());
     953             :             } else {
     954             :                 // the Python _detect_function worker is supposed to ignore any other functions so they don't show up at this point
     955             :                 // I don't think it's appropriate to warn here, so just ignore and move on
     956             :             }
     957          46 :         }
     958             :         // PyList_GetItem returns a borrowed reference, do not decrement
     959             :     }
     960             :     // PyList_Size returns a borrowed reference, do not decrement
     961             :     Py_DECREF(pFunctionResponse); // PyObject_CallFunction returns new reference, decrement
     962             : #endif
     963          45 : }
     964             : 
     965           0 : void PluginInstance::shutdown() const
     966             : {
     967             : #if LINK_WITH_PYTHON
     968           0 :     Py_DECREF(this->pClassInstance);
     969           0 :     Py_DECREF(this->pModule); // PyImport_Import returns a new reference, decrement it
     970           0 :     if (this->bHasBeginNewEnvironment) Py_DECREF(this->pBeginNewEnvironment);
     971           0 :     if (this->bHasAfterNewEnvironmentWarmUpIsComplete) Py_DECREF(this->pAfterNewEnvironmentWarmUpIsComplete);
     972           0 :     if (this->bHasBeginZoneTimestepBeforeInitHeatBalance) Py_DECREF(this->pBeginZoneTimestepBeforeInitHeatBalance);
     973           0 :     if (this->bHasBeginZoneTimestepAfterInitHeatBalance) Py_DECREF(this->pBeginZoneTimestepAfterInitHeatBalance);
     974           0 :     if (this->bHasBeginTimestepBeforePredictor) Py_DECREF(this->pBeginTimestepBeforePredictor);
     975           0 :     if (this->bHasAfterPredictorBeforeHVACManagers) Py_DECREF(this->pAfterPredictorBeforeHVACManagers);
     976           0 :     if (this->bHasAfterPredictorAfterHVACManagers) Py_DECREF(this->pAfterPredictorAfterHVACManagers);
     977           0 :     if (this->bHasInsideHVACSystemIterationLoop) Py_DECREF(this->pInsideHVACSystemIterationLoop);
     978           0 :     if (this->bHasEndOfZoneTimestepBeforeZoneReporting) Py_DECREF(this->pEndOfZoneTimestepBeforeZoneReporting);
     979           0 :     if (this->bHasEndOfZoneTimestepAfterZoneReporting) Py_DECREF(this->pEndOfZoneTimestepAfterZoneReporting);
     980           0 :     if (this->bHasEndOfSystemTimestepBeforeHVACReporting) Py_DECREF(this->pEndOfSystemTimestepBeforeHVACReporting);
     981           0 :     if (this->bHasEndOfSystemTimestepAfterHVACReporting) Py_DECREF(this->pEndOfSystemTimestepAfterHVACReporting);
     982           0 :     if (this->bHasEndOfZoneSizing) Py_DECREF(this->pEndOfZoneSizing);
     983           0 :     if (this->bHasEndOfSystemSizing) Py_DECREF(this->pEndOfSystemSizing);
     984           0 :     if (this->bHasAfterComponentInputReadIn) Py_DECREF(this->pAfterComponentInputReadIn);
     985           0 :     if (this->bHasUserDefinedComponentModel) Py_DECREF(this->pUserDefinedComponentModel);
     986           0 :     if (this->bHasUnitarySystemSizing) Py_DECREF(this->pUnitarySystemSizing);
     987             : #endif
     988           0 : }
     989             : 
     990             : #if LINK_WITH_PYTHON
     991     2988897 : bool PluginInstance::run(EnergyPlusData &state, EMSManager::EMSCallFrom iCalledFrom) const
     992             : {
     993             :     // returns true if a plugin actually ran
     994     2988897 :     PyObject *pFunctionName = nullptr;
     995     2988897 :     const char *functionName = nullptr;
     996     2988897 :     if (iCalledFrom == EMSManager::EMSCallFrom::BeginNewEnvironment) {
     997          86 :         if (this->bHasBeginNewEnvironment) {
     998           2 :             pFunctionName = this->pBeginNewEnvironment;
     999           2 :             functionName = this->sHookBeginNewEnvironment;
    1000             :         }
    1001     2988811 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::BeginZoneTimestepBeforeSetCurrentWeather) {
    1002      160944 :         if (this->bHasBeginZoneTimestepBeforeSetCurrentWeather) {
    1003        2160 :             pFunctionName = this->pBeginZoneTimestepBeforeSetCurrentWeather;
    1004        2160 :             functionName = this->sHookBeginZoneTimestepBeforeSetCurrentWeather;
    1005             :         }
    1006     2827867 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::ZoneSizing) {
    1007          37 :         if (this->bHasEndOfZoneSizing) {
    1008           0 :             pFunctionName = this->pEndOfZoneSizing;
    1009           0 :             functionName = this->sHookEndOfZoneSizing;
    1010             :         }
    1011     2827830 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::SystemSizing) {
    1012          34 :         if (this->bHasEndOfSystemSizing) {
    1013           1 :             pFunctionName = this->pEndOfSystemSizing;
    1014           1 :             functionName = this->sHookEndOfSystemSizing;
    1015             :         }
    1016     2827796 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::BeginNewEnvironmentAfterWarmUp) {
    1017         162 :         if (this->bHasAfterNewEnvironmentWarmUpIsComplete) {
    1018           0 :             pFunctionName = this->pAfterNewEnvironmentWarmUpIsComplete;
    1019           0 :             functionName = this->sHookAfterNewEnvironmentWarmUpIsComplete;
    1020             :         }
    1021     2827634 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::BeginTimestepBeforePredictor) {
    1022      219240 :         if (this->bHasBeginTimestepBeforePredictor) {
    1023       70302 :             pFunctionName = this->pBeginTimestepBeforePredictor;
    1024       70302 :             functionName = this->sHookBeginTimestepBeforePredictor;
    1025             :         }
    1026     2608394 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::BeforeHVACManagers) {
    1027      226809 :         if (this->bHasAfterPredictorBeforeHVACManagers) {
    1028           0 :             pFunctionName = this->pAfterPredictorBeforeHVACManagers;
    1029           0 :             functionName = this->sHookAfterPredictorBeforeHVACManagers;
    1030             :         }
    1031     2381585 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::AfterHVACManagers) {
    1032      226809 :         if (this->bHasAfterPredictorAfterHVACManagers) {
    1033       58843 :             pFunctionName = this->pAfterPredictorAfterHVACManagers;
    1034       58843 :             functionName = this->sHookAfterPredictorAfterHVACManagers;
    1035             :         }
    1036     2154776 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::HVACIterationLoop) {
    1037      503009 :         if (this->bHasInsideHVACSystemIterationLoop) {
    1038       97224 :             pFunctionName = this->pInsideHVACSystemIterationLoop;
    1039       97224 :             functionName = this->sHookInsideHVACSystemIterationLoop;
    1040             :         }
    1041     1651767 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::EndSystemTimestepBeforeHVACReporting) {
    1042      280624 :         if (this->bHasEndOfSystemTimestepBeforeHVACReporting) {
    1043           0 :             pFunctionName = this->pEndOfSystemTimestepBeforeHVACReporting;
    1044           0 :             functionName = this->sHookEndOfSystemTimestepBeforeHVACReporting;
    1045             :         }
    1046     1371143 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::EndSystemTimestepAfterHVACReporting) {
    1047      280624 :         if (this->bHasEndOfSystemTimestepAfterHVACReporting) {
    1048           0 :             pFunctionName = this->pEndOfSystemTimestepAfterHVACReporting;
    1049           0 :             functionName = this->sHookEndOfSystemTimestepAfterHVACReporting;
    1050             :         }
    1051     1090519 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::EndZoneTimestepBeforeZoneReporting) {
    1052      219240 :         if (this->bHasEndOfZoneTimestepBeforeZoneReporting) {
    1053       15528 :             pFunctionName = this->pEndOfZoneTimestepBeforeZoneReporting;
    1054       15528 :             functionName = this->sHookEndOfZoneTimestepBeforeZoneReporting;
    1055             :         }
    1056      871279 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::EndZoneTimestepAfterZoneReporting) {
    1057      219240 :         if (this->bHasEndOfZoneTimestepAfterZoneReporting) {
    1058           0 :             pFunctionName = this->pEndOfZoneTimestepAfterZoneReporting;
    1059           0 :             functionName = this->sHookEndOfZoneTimestepAfterZoneReporting;
    1060             :         }
    1061      652039 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::ComponentGetInput) {
    1062          43 :         if (this->bHasAfterComponentInputReadIn) {
    1063           0 :             pFunctionName = this->pAfterComponentInputReadIn;
    1064           0 :             functionName = this->sHookAfterComponentInputReadIn;
    1065             :         }
    1066      651996 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::UserDefinedComponentModel) {
    1067      213472 :         if (this->bHasUserDefinedComponentModel) {
    1068      213472 :             pFunctionName = this->pUserDefinedComponentModel;
    1069      213472 :             functionName = this->sHookUserDefinedComponentModel;
    1070             :         }
    1071      438524 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::UnitarySystemSizing) {
    1072           0 :         if (this->bHasUnitarySystemSizing) {
    1073           0 :             pFunctionName = this->pUnitarySystemSizing;
    1074           0 :             functionName = this->sHookUnitarySystemSizing;
    1075             :         }
    1076      438524 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::BeginZoneTimestepBeforeInitHeatBalance) {
    1077      219240 :         if (this->bHasBeginZoneTimestepBeforeInitHeatBalance) {
    1078        4038 :             pFunctionName = this->pBeginZoneTimestepBeforeInitHeatBalance;
    1079        4038 :             functionName = this->sHookBeginZoneTimestepBeforeInitHeatBalance;
    1080             :         }
    1081      219284 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::BeginZoneTimestepAfterInitHeatBalance) {
    1082      219240 :         if (this->bHasBeginZoneTimestepAfterInitHeatBalance) {
    1083           0 :             pFunctionName = this->pBeginZoneTimestepAfterInitHeatBalance;
    1084           0 :             functionName = this->sHookBeginZoneTimestepAfterInitHeatBalance;
    1085             :         }
    1086             :     }
    1087             : 
    1088             :     // leave if we didn't find a match
    1089     2988897 :     if (!pFunctionName) {
    1090     2527327 :         return false;
    1091             :     }
    1092             : 
    1093             :     // Get control of the global interpreter lock
    1094      461570 :     PyGILState_STATE gil = PyGILState_Ensure();
    1095             : 
    1096             :     // then call the main function
    1097             :     // static const PyObject oneArgObjFormat = Py_BuildValue)("O");
    1098      461570 :     PyObject *pStateInstance = PyLong_FromVoidPtr(&state);
    1099      461570 :     PyObject *pFunctionResponse = PyObject_CallMethodObjArgs(this->pClassInstance, pFunctionName, pStateInstance, nullptr);
    1100             :     Py_DECREF(pStateInstance);
    1101      461570 :     if (!pFunctionResponse) {
    1102           0 :         std::string const functionNameAsString(functionName); // only convert to string if an error occurs
    1103           0 :         ShowSevereError(state, format("Call to {}() on {} failed!", functionNameAsString, this->stringIdentifier));
    1104           0 :         if (PyErr_Occurred()) {
    1105           0 :             reportPythonError(state);
    1106             :         } else {
    1107           0 :             ShowContinueError(state, "This could happen for any number of reasons, check the plugin code.");
    1108             :         }
    1109           0 :         PyGILState_Release(gil);
    1110           0 :         ShowFatalError(state, format("Program terminates after call to {}() on {} failed!", functionNameAsString, this->stringIdentifier));
    1111           0 :     }
    1112      461570 :     if (PyLong_Check(pFunctionResponse)) { // NOLINT(hicpp-signed-bitwise)
    1113      461570 :         long exitCode = PyLong_AsLong(pFunctionResponse);
    1114      461570 :         if (exitCode == 0) {
    1115             :             // success
    1116           0 :         } else if (exitCode == 1) {
    1117           0 :             PyGILState_Release(gil);
    1118           0 :             ShowFatalError(state, format("Python Plugin \"{}\" returned 1 to indicate EnergyPlus should abort", this->stringIdentifier));
    1119             :         }
    1120             :     } else {
    1121           0 :         std::string const functionNameAsString(functionName); // only convert to string if an error occurs
    1122           0 :         PyGILState_Release(gil);
    1123           0 :         ShowFatalError(
    1124             :             state,
    1125           0 :             format("Invalid return from {}() on class \"{}, make sure it returns an integer exit code, either zero (success) or one (failure)",
    1126             :                    functionNameAsString,
    1127           0 :                    this->stringIdentifier));
    1128           0 :     }
    1129             :     Py_DECREF(pFunctionResponse); // PyObject_CallFunction returns new reference, decrement
    1130      461570 :     if (state.dataPluginManager->apiErrorFlag) {
    1131           0 :         PyGILState_Release(gil);
    1132           0 :         ShowFatalError(state, "API problems encountered while running plugin cause program termination.");
    1133             :     }
    1134      461570 :     PyGILState_Release(gil);
    1135      461570 :     return true;
    1136             : }
    1137             : #else
    1138             : bool PluginInstance::run([[maybe_unused]] EnergyPlusData &state, [[maybe_unused]] EMSManager::EMSCallFrom iCalledFrom) const
    1139             : {
    1140             :     return false;
    1141             : }
    1142             : #endif
    1143             : 
    1144             : #if LINK_WITH_PYTHON
    1145           0 : std::vector<std::string> PluginManager::currentPythonPath()
    1146             : {
    1147           0 :     PyObject *sysPath = PySys_GetObject("path"); // Borrowed reference
    1148           0 :     Py_ssize_t const n = PyList_Size(sysPath);   // Py_ssize_t
    1149           0 :     std::vector<std::string> pathLibs(n);
    1150           0 :     for (Py_ssize_t i = 0; i < n; ++i) {
    1151           0 :         PyObject *element = PyList_GetItem(sysPath, i); // Borrowed reference
    1152           0 :         pathLibs[i] = std::string{PyUnicode_AsUTF8(element)};
    1153             :     }
    1154           0 :     return pathLibs;
    1155           0 : }
    1156             : 
    1157          88 : void PluginManager::addToPythonPath(EnergyPlusData &state, const fs::path &includePath, bool userDefinedPath)
    1158             : {
    1159          88 :     if (includePath.empty()) {
    1160           0 :         return;
    1161             :     }
    1162             : 
    1163             :     // We use generic_string / generic_wstring here, which will always use a forward slash as directory separator even on windows
    1164             :     // This doesn't handle the (very strange, IMHO) case were on unix you have backlashes (which are VALID filenames on Unix!)
    1165             :     // Could use FileSystem::makeNativePath first to convert the backslashes to forward slashes on Unix
    1166             :     PyObject *unicodeIncludePath;
    1167             :     // ReSharper disable once CppRedundantTypenameKeyword
    1168             :     if constexpr (std::is_same_v<typename fs::path::value_type, wchar_t>) {
    1169             :         // ReSharper disable once CppDFAUnreachableCode
    1170             :         const std::wstring ws = includePath.generic_wstring();
    1171             :         unicodeIncludePath = PyUnicode_FromWideChar(ws.c_str(), static_cast<Py_ssize_t>(ws.size())); // New reference
    1172             :     } else {
    1173          88 :         const std::string s = includePath.generic_string();
    1174          88 :         unicodeIncludePath = PyUnicode_FromString(s.c_str()); // New reference
    1175          88 :     }
    1176          88 :     if (unicodeIncludePath == nullptr) {
    1177           0 :         ShowFatalError(state, format("ERROR converting the path \"{:g}\" for addition to the sys.path in Python", includePath));
    1178             :     }
    1179             : 
    1180          88 :     PyObject *sysPath = PySys_GetObject("path"); // Borrowed reference
    1181          88 :     int const ret = PyList_Insert(sysPath, 0, unicodeIncludePath);
    1182             :     Py_DECREF(unicodeIncludePath);
    1183             : 
    1184          88 :     if (ret != 0) {
    1185           0 :         if (PyErr_Occurred()) {
    1186           0 :             PluginInstance::reportPythonError(state);
    1187             :         }
    1188           0 :         ShowFatalError(state, format("ERROR adding \"{:g}\" to the sys.path in Python", includePath));
    1189             :     }
    1190             : 
    1191          88 :     if (userDefinedPath) {
    1192           0 :         ShowMessage(state, format("Successfully added path \"{:g}\" to the sys.path in Python", includePath));
    1193             :     }
    1194             : 
    1195             :     // PyRun_SimpleString)("print(' EPS : ' + str(sys.path))");
    1196             : }
    1197             : #else
    1198             : std::vector<std::string> PluginManager::currentPythonPath()
    1199             : {
    1200             :     return {};
    1201             : }
    1202             : void PluginManager::addToPythonPath([[maybe_unused]] EnergyPlusData &state,
    1203             :                                     [[maybe_unused]] const fs::path &path,
    1204             :                                     [[maybe_unused]] bool userDefinedPath)
    1205             : {
    1206             : }
    1207             : #endif
    1208             : 
    1209             : #if LINK_WITH_PYTHON
    1210          31 : void PluginManager::addGlobalVariable(const EnergyPlusData &state, const std::string &name)
    1211             : {
    1212          31 :     std::string const varNameUC = EnergyPlus::Util::makeUPPER(name);
    1213          31 :     state.dataPluginManager->globalVariableNames.push_back(varNameUC);
    1214          31 :     state.dataPluginManager->globalVariableValues.push_back(Real64());
    1215          31 :     this->maxGlobalVariableIndex++;
    1216          31 : }
    1217             : #else
    1218             : void PluginManager::addGlobalVariable([[maybe_unused]] const EnergyPlusData &state, [[maybe_unused]] const std::string &name)
    1219             : {
    1220             : }
    1221             : #endif
    1222             : 
    1223             : #if LINK_WITH_PYTHON
    1224          51 : int PluginManager::getGlobalVariableHandle(EnergyPlusData &state, const std::string &name, bool const suppress_warning)
    1225             : { // note zero is a valid handle
    1226          51 :     std::string const varNameUC = EnergyPlus::Util::makeUPPER(name);
    1227          51 :     auto const &gVarNames = state.dataPluginManager->globalVariableNames;
    1228         102 :     auto const it = std::find(gVarNames.begin(), gVarNames.end(), varNameUC);
    1229          51 :     if (it != gVarNames.end()) {
    1230          51 :         return static_cast<int>(std::distance(gVarNames.begin(), it));
    1231             :     }
    1232           0 :     if (suppress_warning) {
    1233           0 :         return -1;
    1234             :     }
    1235           0 :     ShowSevereError(state, "Tried to retrieve handle for a nonexistent plugin global variable");
    1236           0 :     ShowContinueError(state, format("Name looked up: \"{}\", available names: ", varNameUC));
    1237           0 :     for (auto const &gvName : gVarNames) {
    1238           0 :         ShowContinueError(state, format("    \"{}\"", gvName));
    1239           0 :     }
    1240           0 :     ShowFatalError(state, "Plugin global variable problem causes program termination");
    1241           0 :     return -1; // hush the compiler warning
    1242          51 : }
    1243             : #else
    1244             : int PluginManager::getGlobalVariableHandle([[maybe_unused]] EnergyPlusData &state,
    1245             :                                            [[maybe_unused]] const std::string &name,
    1246             :                                            [[maybe_unused]] bool const suppress_warning)
    1247             : {
    1248             :     return -1;
    1249             : }
    1250             : #endif
    1251             : 
    1252             : #if LINK_WITH_PYTHON
    1253           3 : int PluginManager::getTrendVariableHandle(const EnergyPlusData &state, const std::string &name)
    1254             : {
    1255           3 :     std::string const varNameUC = Util::makeUPPER(name);
    1256           4 :     for (size_t i = 0; i < state.dataPluginManager->trends.size(); i++) {
    1257           4 :         auto &thisTrend = state.dataPluginManager->trends[i];
    1258           4 :         if (thisTrend.name == varNameUC) {
    1259           3 :             return static_cast<int>(i);
    1260             :         }
    1261             :     }
    1262           0 :     return -1;
    1263           3 : }
    1264             : #else
    1265             : int PluginManager::getTrendVariableHandle([[maybe_unused]] const EnergyPlusData &state, [[maybe_unused]] const std::string &name)
    1266             : {
    1267             :     return -1;
    1268             : }
    1269             : #endif
    1270             : 
    1271             : #if LINK_WITH_PYTHON
    1272        6346 : Real64 PluginManager::getTrendVariableValue(const EnergyPlusData &state, int handle, int timeIndex)
    1273             : {
    1274        6346 :     return state.dataPluginManager->trends[handle].values[timeIndex];
    1275             : }
    1276             : #else
    1277             : Real64 PluginManager::getTrendVariableValue([[maybe_unused]] const EnergyPlusData &state, [[maybe_unused]] int handle, [[maybe_unused]] int timeIndex)
    1278             : {
    1279             :     return 0.0;
    1280             : }
    1281             : #endif
    1282             : 
    1283             : #if LINK_WITH_PYTHON
    1284        2016 : Real64 PluginManager::getTrendVariableAverage(const EnergyPlusData &state, int handle, int count)
    1285             : {
    1286        2016 :     Real64 sum = 0;
    1287     2034144 :     for (int i = 0; i < count; i++) {
    1288     2032128 :         sum += state.dataPluginManager->trends[handle].values[i];
    1289             :     }
    1290        2016 :     return sum / count;
    1291             : }
    1292             : #else
    1293             : Real64 PluginManager::getTrendVariableAverage([[maybe_unused]] const EnergyPlusData &state, [[maybe_unused]] int handle, [[maybe_unused]] int count)
    1294             : {
    1295             :     return 0.0;
    1296             : }
    1297             : #endif
    1298             : 
    1299             : #if LINK_WITH_PYTHON
    1300           0 : Real64 PluginManager::getTrendVariableMin(const EnergyPlusData &state, int handle, int count)
    1301             : {
    1302           0 :     Real64 minimumValue = 9999999999999;
    1303           0 :     for (int i = 0; i < count; i++) {
    1304           0 :         if (state.dataPluginManager->trends[handle].values[i] < minimumValue) {
    1305           0 :             minimumValue = state.dataPluginManager->trends[handle].values[i];
    1306             :         }
    1307             :     }
    1308           0 :     return minimumValue;
    1309             : }
    1310             : #else
    1311             : Real64 PluginManager::getTrendVariableMin([[maybe_unused]] const EnergyPlusData &state, [[maybe_unused]] int handle, [[maybe_unused]] int count)
    1312             : {
    1313             :     return 0.0;
    1314             : }
    1315             : #endif
    1316             : 
    1317             : #if LINK_WITH_PYTHON
    1318           0 : Real64 PluginManager::getTrendVariableMax(const EnergyPlusData &state, int handle, int count)
    1319             : {
    1320           0 :     Real64 maximumValue = -9999999999999;
    1321           0 :     for (int i = 0; i < count; i++) {
    1322           0 :         if (state.dataPluginManager->trends[handle].values[i] > maximumValue) {
    1323           0 :             maximumValue = state.dataPluginManager->trends[handle].values[i];
    1324             :         }
    1325             :     }
    1326           0 :     return maximumValue;
    1327             : }
    1328             : #else
    1329             : Real64 PluginManager::getTrendVariableMax([[maybe_unused]] const EnergyPlusData &state, [[maybe_unused]] int handle, [[maybe_unused]] int count)
    1330             : {
    1331             :     return 0.0;
    1332             : }
    1333             : #endif
    1334             : 
    1335             : #if LINK_WITH_PYTHON
    1336           0 : Real64 PluginManager::getTrendVariableSum(const EnergyPlusData &state, int handle, int count)
    1337             : {
    1338           0 :     Real64 sum = 0.0;
    1339           0 :     for (int i = 0; i < count; i++) {
    1340           0 :         sum += state.dataPluginManager->trends[handle].values[i];
    1341             :     }
    1342           0 :     return sum;
    1343             : }
    1344             : #else
    1345             : Real64 PluginManager::getTrendVariableSum([[maybe_unused]] const EnergyPlusData &state, [[maybe_unused]] int handle, [[maybe_unused]] int count)
    1346             : {
    1347             :     return 0.0;
    1348             : }
    1349             : #endif
    1350             : 
    1351             : #if LINK_WITH_PYTHON
    1352        3173 : Real64 PluginManager::getTrendVariableDirection(const EnergyPlusData &state, int handle, int count)
    1353             : {
    1354        3173 :     auto &trend = state.dataPluginManager->trends[handle];
    1355        3173 :     Real64 timeSum = 0.0;
    1356        3173 :     Real64 valueSum = 0.0;
    1357        3173 :     Real64 crossSum = 0.0;
    1358        3173 :     Real64 powSum = 0.0;
    1359       15865 :     for (int i = 0; i < count; i++) {
    1360       12692 :         timeSum += trend.times[i];
    1361       12692 :         valueSum += trend.values[i];
    1362       12692 :         crossSum += trend.times[i] * trend.values[i];
    1363       12692 :         powSum += pow2(trend.times[i]);
    1364             :     }
    1365        3173 :     Real64 numerator = timeSum * valueSum - count * crossSum;
    1366        3173 :     Real64 denominator = pow_2(timeSum) - count * powSum;
    1367        3173 :     return numerator / denominator;
    1368             : }
    1369             : #else
    1370             : Real64 PluginManager::getTrendVariableDirection([[maybe_unused]] const EnergyPlusData &state, [[maybe_unused]] int handle, [[maybe_unused]] int count)
    1371             : {
    1372             :     return 0.0;
    1373             : }
    1374             : #endif
    1375             : 
    1376             : #if LINK_WITH_PYTHON
    1377       11535 : size_t PluginManager::getTrendVariableHistorySize(const EnergyPlusData &state, int handle)
    1378             : {
    1379       11535 :     return state.dataPluginManager->trends[handle].values.size();
    1380             : }
    1381             : #else
    1382             : size_t PluginManager::getTrendVariableHistorySize([[maybe_unused]] const EnergyPlusData &state, [[maybe_unused]] int handle)
    1383             : {
    1384             :     return 0;
    1385             : }
    1386             : #endif
    1387             : 
    1388     2804481 : void PluginManager::updatePluginValues([[maybe_unused]] EnergyPlusData &state)
    1389             : {
    1390             : #if LINK_WITH_PYTHON
    1391     2816916 :     for (auto &trend : state.dataPluginManager->trends) {
    1392       12435 :         Real64 newVarValue = getGlobalVariableValue(state, trend.indexOfPluginVariable);
    1393       12435 :         trend.values.push_front(newVarValue);
    1394       12435 :         trend.values.pop_back();
    1395     2804481 :     }
    1396             : #endif
    1397     2804481 : }
    1398             : 
    1399             : #if LINK_WITH_PYTHON
    1400       68620 : Real64 PluginManager::getGlobalVariableValue(EnergyPlusData &state, int handle)
    1401             : {
    1402       68620 :     if (state.dataPluginManager->globalVariableValues.empty()) {
    1403           0 :         ShowFatalError(
    1404             :             state,
    1405             :             "Tried to access plugin global variable but it looks like there aren't any; use the PythonPlugin:Variables object to declare them.");
    1406             :     }
    1407             :     try {
    1408       68620 :         return state.dataPluginManager->globalVariableValues[handle]; // TODO: This won't be caught as an exception I think
    1409             :     } catch (...) {
    1410             :         ShowSevereError(state, format("Tried to access plugin global variable value at index {}", handle));
    1411             :         ShowContinueError(state, format("Available handles range from 0 to {}", state.dataPluginManager->globalVariableValues.size() - 1));
    1412             :         ShowFatalError(state, "Plugin global variable problem causes program termination");
    1413             :     }
    1414             :     return 0.0;
    1415             : }
    1416             : #else
    1417             : Real64 PluginManager::getGlobalVariableValue([[maybe_unused]] EnergyPlusData &state, [[maybe_unused]] int handle)
    1418             : {
    1419             :     return 0.0;
    1420             : }
    1421             : #endif
    1422             : 
    1423             : #if LINK_WITH_PYTHON
    1424      411381 : void PluginManager::setGlobalVariableValue(EnergyPlusData &state, int handle, Real64 value)
    1425             : {
    1426      411381 :     if (state.dataPluginManager->globalVariableValues.empty()) {
    1427           0 :         ShowFatalError(state,
    1428             :                        "Tried to set plugin global variable but it looks like there aren't any; use the PythonPlugin:GlobalVariables "
    1429             :                        "object to declare them.");
    1430             :     }
    1431             :     try {
    1432      411381 :         state.dataPluginManager->globalVariableValues[handle] = value; // TODO: This won't be caught as an exception I think
    1433             :     } catch (...) {
    1434             :         ShowSevereError(state, format("Tried to set plugin global variable value at index {}", handle));
    1435             :         ShowContinueError(state, format("Available handles range from 0 to {}", state.dataPluginManager->globalVariableValues.size() - 1));
    1436             :         ShowFatalError(state, "Plugin global variable problem causes program termination");
    1437             :     }
    1438      411381 : }
    1439             : #else
    1440             : void PluginManager::setGlobalVariableValue([[maybe_unused]] EnergyPlusData &state, [[maybe_unused]] int handle, [[maybe_unused]] Real64 value)
    1441             : {
    1442             : }
    1443             : #endif
    1444             : 
    1445             : #if LINK_WITH_PYTHON
    1446           6 : int PluginManager::getLocationOfUserDefinedPlugin(const EnergyPlusData &state, std::string const &_programName)
    1447             : {
    1448          10 :     for (size_t handle = 0; handle < state.dataPluginManager->plugins.size(); handle++) {
    1449          10 :         auto const &thisPlugin = state.dataPluginManager->plugins[handle];
    1450          10 :         if (Util::makeUPPER(thisPlugin.emsAlias) == Util::makeUPPER(_programName)) {
    1451           6 :             return static_cast<int>(handle);
    1452             :         }
    1453             :     }
    1454           0 :     return -1;
    1455             : }
    1456             : #else
    1457             : int PluginManager::getLocationOfUserDefinedPlugin([[maybe_unused]] const EnergyPlusData &state, [[maybe_unused]] std::string const &_programName)
    1458             : {
    1459             :     return -1;
    1460             : }
    1461             : #endif
    1462             : 
    1463             : #if LINK_WITH_PYTHON
    1464      213472 : void PluginManager::runSingleUserDefinedPlugin(EnergyPlusData &state, int index)
    1465             : {
    1466      213472 :     state.dataPluginManager->plugins[index].run(state, EMSManager::EMSCallFrom::UserDefinedComponentModel);
    1467      213472 : }
    1468             : #else
    1469             : void PluginManager::runSingleUserDefinedPlugin([[maybe_unused]] EnergyPlusData &state, [[maybe_unused]] int index)
    1470             : {
    1471             : }
    1472             : #endif
    1473             : 
    1474           0 : int PluginManager::getUserDefinedCallbackIndex(const EnergyPlusData &state, const std::string &callbackProgramName)
    1475             : {
    1476           0 :     for (int i = 0; i < static_cast<int>(state.dataPluginManager->userDefinedCallbackNames.size()); i++) {
    1477           0 :         if (state.dataPluginManager->userDefinedCallbackNames[i] == callbackProgramName) {
    1478           0 :             return i;
    1479             :         }
    1480             :     }
    1481           0 :     return -1;
    1482             : }
    1483             : 
    1484           0 : void PluginManager::runSingleUserDefinedCallback(EnergyPlusData &state, int index)
    1485             : {
    1486           0 :     if (state.dataGlobal->KickOffSimulation) return;              // Maybe?
    1487           0 :     state.dataPluginManager->userDefinedCallbacks[index](&state); // Check Index first
    1488             : }
    1489             : 
    1490             : #if LINK_WITH_PYTHON
    1491           0 : bool PluginManager::anyUnexpectedPluginObjects(EnergyPlusData &state)
    1492             : {
    1493           0 :     int numTotalThings = 0;
    1494           0 :     for (std::string const &objToFind : state.dataPluginManager->objectsToFind) {
    1495           0 :         int instances = state.dataInputProcessing->inputProcessor->getNumObjectsFound(state, objToFind);
    1496           0 :         numTotalThings += instances;
    1497           0 :         if (numTotalThings == 1) {
    1498           0 :             ShowSevereMessage(state, "Found PythonPlugin objects in an IDF that is running in an API/Library workflow...this is invalid");
    1499             :         }
    1500           0 :         if (instances > 0) {
    1501           0 :             ShowContinueError(state, format("Invalid PythonPlugin object type: {}", objToFind));
    1502             :         }
    1503           0 :     }
    1504           0 :     return numTotalThings > 0;
    1505             : }
    1506             : #else
    1507             : bool PluginManager::anyUnexpectedPluginObjects([[maybe_unused]] EnergyPlusData &state)
    1508             : {
    1509             :     return false;
    1510             : }
    1511             : #endif
    1512             : 
    1513             : } // namespace EnergyPlus::PluginManagement

Generated by: LCOV version 1.14