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