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