Group – Python Plugins / Python EMS[LINK]
The Energy Management System in EnergyPlus was a major breakthrough in provided user-defined capabilities. By allowing the user to read simulation data, perform custom calculations, and ultimately manipulate control settings, the door was opened to any number of new applications, including evaluation of custom control strategies or custom physical component models. However, users were limited to writing scripts in the built-in, custom, EnergyPlus Runtime Language, and debugging was possible, but difficult.
Users have pushed EMS in many ways well beyond a traditional Energy “Management” System. At this point, a lot of EMS scripts are what would be called “plugins” in most software environments. As of version 9.3 of EnergyPlus, a new way to write these plugins, in the Python programming language was made available.
Note: This functionality relies on some aspects of the EnergyPlus API for making calls into EnergyPlus when these plugins are running. A majority of the API is described in section [sec:api-usage], and it is highly recommended that that section be read first, as this section will only focus on the differences, and additions. One point should be made perfectly clear. There are two Python-related ways of interacting with EnergyPlus. We can call these “modes”, with each clarified here:
- Library Mode
In library mode, an external script (C or Python) is written which leverages the EnergyPlus API to register callback functions and initiate a simulation from a function call.
- Plugin Mode
In plugin mode, a (typically) small Python script is written, then by specifying parameters in an input file, the client runs EnergyPlus(.exe) in a traditional fashion, and that EnergyPlus run will call out to the Python script as needed.
The initial versions of the Plugin and API systems could not work together, however as of EnergyPlus version 9.6, they can now be combined! During plugin execution, EnergyPlus creates a new Python instance internally to allow processing the user-defined scripts. When EnergyPlus is run in an API mode, it can actively call back to your client script, as well as do all the normal things EnergyPlus does. One caveat is that if you run multiple EnergyPlus runs in parallel threads in the same process, and they all utilize Python Plugins as well, you will notice a small slowdown based on how many you run in parallel. This is due to having to access Python’s global interpreter lock during the Python portion of the simulation, and will be investigated to avoid this in a later version, however it is a small effect unless the number of runs grows substantially. Also, this problem is only realized if you are running multiple EnergyPlus threads in the same API process space.
The API section has information about how to access output variable data, meters, internal variable data, simulation parameters, and actuators, so these do not need additional information here. However, plugin objects have additional ways to interact with the simulation, and the plugins themselves need description, which is here.
For EnergyPlus, a Python Plugin is a user-defined Python class that overrides a premade
EnergyPlusPlugin base class. This base class creates overrideable functions that, when overridden, determine when the user-defined code will be called by EnergyPlus. The functions are structured in this way to allow a single class to defined multiple plugins. In this way, data can be shared between multiple calling points on the same class instance by using regular Python class members.
EnergyPlusPlugin base class is defined in a ‘plugin.py’ file inside the ‘pyenergyplus’ folder, which is located in the root EnergyPlus install directory. When EnergyPlus starts up, if any plugins are to be run, it starts up a Python instance and adds the executable directory to the current Python search path. In this way, a user plugin should always
from pyenergyplus.plugin import EnergyPlusPlugin. This will ensure that when EnergyPlus processes this Python file, it always finds this imported base class. The user will then create a derived class that inherits this base class, and write custom functions.
A minimal plugin does not actually have to interact with EnergyPlus. It is expected that essentially every plugin will at least read data from sensors or meters, or write data using actuators, but it is not necessary. Just like when calling EnergyPlus as a library, when operating in plugin mode, this data is accessed through the same API methods. To learn how to interact with the simulation API, see the API usage section: [sec:api-usage]. Consider this minimal plugin:
from pyenergyplus.plugin import EnergyPlusPlugin class DummyPlugin(EnergyPlusPlugin): def on_end_of_zone_timestep_before_zone_reporting(self, state): return 0
This skeleton captures the use of a plugin to do nothing, but still shows some important features.
The base class plugin is properly imported.
A user-defined class is created which inherits the EnergyPlusPlugin base class.
A method is overridden, which is named appropriately to describe when EnergyPlus will be calling this function, in this case, at the end of each zone time step but before zone-level data is reported.
The custom method returns zero to indicate success.
NOTE! The functional form of the plugin methods changed due to an API change between versions 9.3 and 9.4. Any plugin files created using 9.3 will need to add a second argument to the methods. This same argument will need to be passed into any EnergyPlus API methods called during the plugin. Much more information can be found in the API usage section: [sec:api-usage].
Several notes can be taken from this list. The name of the function matters, as it determines when the function will be called. The full list of possible names are:
The meaning of these names can be found in the EMSApplicationGuide which comes with the EnergyPlus install.
It is possible future versions of the program will expose additional calling points. Note: a single class can override multiple functions, so that the same class instance is called at multiple points in a simulation.
Once this plugin is created, it can be placed in a number of locations on the filesystem, as described in the PythonPlugin:SearchPaths section 1.2. The plugin instance, along with any other plugin related obejcts are declared in the input file, and then EnergyPlus is executed. More data is provided in each of the specific object sections below.
Once EnergyPlus initiates a Python process, it will need to know where to search for user-defined plugin script files A Python search path is managed that contains a list of possible folders where Python will search when encountering an import statement. The folder containing the currently running EnergyPlus process will be added no matter what, as this directory contains the plugin and API code, but additional folders can be specified to meet specific needs. These options are described in the inputs of the object.
An identifier given to this object. The object is required to be unique, so this field is only used for reporting purposes.
Field: Add Current Working Directory to Search Path[LINK]
Based on different workflows, EnergyPlus may be executed in directories far away from where the actual binary is stored. In these cases, it may be useful to put plugin files in the current working directory, even if the simulation input file is not located there. Turning this field to Yes will add the current working directory to the list of search paths.
Field: Add Input File Directory to Search Path[LINK]
In a lot of cases, it is expected that the plugin will be tied closely to a specific input file. In these cases, it may be useful to keep plugin files right next to the input file, regardless of the current working directory. Turning this field to Yes will add the directory where the input file is located to the list of search paths.
Field: Search Path N[LINK]
There are also some cases, where a user may keep a specific library of plugin scripts that work with a variety of input files. In this case, they may want to just point EnergyPlus to a custom directory away from any specific input file or working directory. Declaring a path in these fields will add those paths to the list of search paths. Note: This does not check the filesystem for path existence.
This object captures a single plugin instance for EnergyPlus. As previously described, a single plugin may be called at multiple calling points if there are multiple functions overridden. A single instance is analogous to a single user-defined class that inherits the EnergyPlusPlugin base class.
This field represents an identifier for this plugin within EnergyPlus. In most cases, this field is just used for reporting, but for user-defined-components, this name is used as the name of the EMS program manager. Note that if this instance is used for that application, the plugin must override the
Field: Run During Warmup Days[LINK]
This field specifies whether EnergyPlus should execute this plugin during warmup days. In most applications, this should not be enabled, both for runtime savings, and because warmups are just used to initialize the simulation state.
Field: Python Module Name[LINK]
This is the name of the Python plugin file name – except – no file extension. For
pluginA.py, just use
pluginA. This file should be found in one of the folders in the plugin search path, which the user can customize using the PythonPlugin:SearchPaths object.
Field: Plugin Class Name[LINK]
This is the name of the user-defined class inside the plugin file. This class must inherit the EnergyPlusPlugin base class, or the simulation will issue an error and abort.
Global and Trend Variable API Methods[LINK]
Plugins have two areas of data storage/bookkeeping that are not available on the regular EnergyPlus API: Global variables and Trend variables
- Global Variables
Global variables, in this context, reflect variables which are given space in EnergyPlus, and can be accessed in read/write fashion by all running plugin instances at any time. Global variables can then be further used as the basis for creating custom output variables and trend variables. Global variables are declared in the input file, then accessed by name in the plugin script. An example usage is provided below in the global variable section.
- Trend Variables
Trend variables are used to track the history of a previously declared plugin global variable. By first declaring a plugin global variable, and then assigning that value in plugins, this trend mechanism will keep history. The actual number of history terms is declared in the input file, and the trend variables are accessed by name in the plugin script. Example usage is provided below in the trend variable section.
This object defines the “global” variables described elsewhere in this section. The term global is carried along as it is the convention used in traditional EMS applications. There are a few reasons why users may need to use global variables:
If a variable value is to be read/write between multiple plugins in different files.
If a client wants to create a custom output variable, these are based on, and reference, existing global variables.
If a client wants to create a trend variable, these are based on, and reference, existing global variables.
To use a global variable, an input object must be defined in the current input file, which specifies the variable name. Then, in a plugin, a handle can be looked up for this global variable using the
get_global_handle function, which accepts the current state and the variable name as arguments. With the handle, the current value of the global variable can be looked up using the
get_global_value function, which takes the current state and the handle for the variable as arguments. Finally, to update the value of the global variable, the
set_global_value function is used, which takes the current state, the variable handle and a new value.
Assuming a global variable is defined on input called “OutdoorTemperature”, an example of this inside a plugin function follows:
global_var_handle = self.api.exchange.get_global_handle(state, 'OutdoorTemperature') var_value = self.api.exchange.get_global_value(state, global_var_handle) self.api.exchange.set_global_value(state, global_var_handle, 3.141)
The inputs required to declare a global variable consist of the unique PythonPlugin:GlobalVariable object, along with a series of names to identify each variable.
This field is the name of this variables object, however since this is required to be a unique object, it does not do much. It is purely used for reporting of errors.
Field Variable Name N[LINK]
This field is used to declare and identify a plugin variable inside EnergyPlus. This name is used as the identifier when accessing this value to read/write in user-defined plugins.
Trend variables allow a predeclared global variable to be tracked over time steps in the simulation. Once these are declared, a number of useful worker functions can be employed to process and read data from the trend. Note that trend variables are initially declared with values of zero at each history place, so the first few calls into the simulation could result in unexpected values of the trend.
To use a trend variable, an input object must be defined int he current input file, which specifies the attributes of the trend, including a name and a number of history terms to keep. Then, in a plugin, a handle can be looked up for this trend variable using the
get_trend_handle function, which accepts the trend variable name as the only argument. With the handle, a number of aspects can be processed/looked up:
The trend value at a specific point in history can be retrieved using the
get_trend_valuefunction, which accepts the current state, a trend handle and an index specifying how far back in history to evaluate the lookup.
The average trend value over a specific range can be retrieved using the
get_trend_averagefunction, which accepts the current state, a trend handle and a count of how far back in history to evaluate.
The minimum trend value over a specific range can be retrieved using the
get_trend_minfunction, which accepts the current state, a trend handle and a count of how far back in history to evaluate.
The maximum trend value over a specific range can be retrieved using the
get_trend_maxfunction, which accepts the current state, a trend handle and a count of how far back in history to evaluate.
The sum of a trend value over a specific range can be retrieved using the
get_trend_sumfunction, which accepts the current state, a trend handle and a count of how far back in history to evaluate.
The trajectory of a trend (slope of a linear regression line) over a specific range can be calculated using the
get_trend_directionfunction, which accepts the current state, a trend handle and a count of how far back in history to evaluate.
Assuming a global variable is defined on input called “CustomTemperature”, and a trend variable is declared called “TrendVar” with at least 5 history terms saved, an example usage of this inside a plugin function follows:
# update the variable throughout the simulation global_var_handle = self.api.exchange.get_global_handle(state, 'CustomTemperature') self.api.exchange.set_global_value(state, global_var_handle, 3.141) # get trend values as needed trend_var_handle = self.api.exchange.get_trend_handle(state, 'TrendVar') trend_avg = self.api.exchange.get_trend_average(state, trend_var_handle, 5) trend_min = self.api.exchange.get_trend_min(state, trend_var_handle, 5) trend_max = self.api.exchange.get_trend_max(state, trend_var_handle, 5) trend_sum = self.api.exchange.get_trend_sum(state, trend_var_handle, 5) trend_dir = self.api.exchange.get_trend_direction(state, trend_var_handle, 5)
This field is the name of the trend object, and is used both for reporting purposes, as well as to identify this trend inside of plugin code.
Field: Name of a Python Plugin Variable[LINK]
This field should include the name of a plugin variable that is declared in the PythonPlugin:Variables object. A trend variable is simply a wrapper around an existing variable object, which must be read/written by plugins, so first declare the variable, then add a trend to it as needed by referencing the name here.
Field: Number of Timesteps to be Logged[LINK]
This field specified the number of history terms to keep for this trend. Trend variables are stored for this number of zone time steps. If a plugin attempts to access a trend parameter at greater than this number, an error will occur.
Implementing these plugins required structural additions to the package and to the build process. From a user perspective, the only thing that will be noticed likely is the additional size of the installer. At the very onset of this project, it was decided that we would package Python into the installer, and not require a user to install an external version. This has been successful, however, at the cost of a larger install footprint. The EnergyPlus install now includes a Python binary shared library, any dependent libraries, along with the core Python standard library of code. This allows users to create user-defined plugins that access all of the standard Python library capabilities.
The main question received about this is about installing non-standard Python packages. While we have not polished up the instructions on the process, some advice can be given. However, note that if you break your Python install, either system or EnergyPlus version, in the process, you could end up with a very, very, very broken system. So do not do this if you do not understand the consequences and have a solid understanding of Python package management. Also, once you start down this process of using anything non-standard, the development team will no longer be able to help you debug any plugin problems.
Some possibilities for using external libraries include:
If the external package you are wanting is pure Python code, then you can download and extract it right into a local directory, add it to the SearchPaths object, and the Python process set up by EnergyPlus should be able to find it.
For native code libraries, if you are able to install a Python version on your system with an identical version to the one that is packaged up with your install of EnergyPlus, you may be able to leverage that separate module directory. Use Pip or whatever package management tool you choose to install the package. This will install it in that version. Then you can try to point EnergyPlus to that directory, or copy those files from one package into the EnergyPlus package.
It is hoped that future versions of this will include a package management tool with EnergyPlus itself, so that installing native packages is straightforward.