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
|