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.

These two operation modes cannot be mixed. In plugin mode, EnergyPlus creates a new Python instance internally to allow processing the user-defined scripts. When EnergyPlus is run in an API mode, it cannot create a new Python session internally, as it may be in an existing Python session. Thus, if a plugin object is found in input during an API call, the program will abort. In the future, this limitation may be lifted, but for most use cases, if you are calling EnergyPlus as a library (API), you can simply register API callbacks to execute user-defined code at runtime, and don’t need to worry about also writing Python Plugins.

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.

The 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.

1. The base class plugin is properly imported.

2. A user-defined class is created which inherits the EnergyPlusPlugin base class.

3. 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.

4. 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:

• on_begin_new_environment

• on_after_new_environment_warmup_is_complete

• on_begin_zone_timestep_before_init_heat_balance

• on_begin_zone_timestep_after_init_heat_balance

• on_begin_timestep_before_predictor

• on_after_predictor_before_hvac_managers

• on_after_predictor_after_hvac_managers

• on_inside_hvac_system_iteration_loop

• on_end_of_zone_timestep_before_zone_reporting

• on_end_of_zone_timestep_after_zone_reporting

• on_end_of_system_timestep_before_hvac_reporting

• on_end_of_system_timestep_after_hvac_reporting

• on_end_of_zone_sizing

• on_end_of_system_sizing

• on_end_of_component_input_read_in

• on_user_defined_component_model

• on_unitary_system_sizing

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.

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.

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.

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 on_user_defined_component_model function.

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.

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.

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.

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_value function, 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_average function, 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_min function, 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_max function, 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_sum function, 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_direction function, 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.