LCOV - code coverage report
Current view: top level - EnergyPlus - PluginManager.cc (source / functions) Coverage Total Hit
Test: lcov.output.filtered Lines: 54.8 % 794 435
Test Date: 2025-06-02 07:23:51 Functions: 67.5 % 40 27

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

Generated by: LCOV version 2.0-1