Skip to content

OpenStudio Model Setup

Eric Bonnema edited this page Aug 9, 2022 · 9 revisions

Introduction

To use OpenStudio models with Alfalfa, it must be within an OpenStudio Workflow (OSW). If a user just has an OpenStudio Model (OSM), this section will show how a simple OSW can be created that utilizes the user's OSM and one OpenStudio measure (described in detail here) for Alfalfa. If a user already has an OSW, they can integrate the measure described in detail here into their existing workflow.

Measure Background

This measure modifies an OpenStudio model by creating readable and writeable points in that OpenStudio Model so that Alfalfa can create its ExternalInterface objects. This DOES NOT define how the appropriate tags should be applied to 'type' something. That is TODO.

Readable Points (Alfalfa Output)

The following steps should be taken in the OpenStudio model in order to expose readable points from simulation to Alfalfa to acquire ExternalInterface objects needed for co-simulation, as well as Project Haystack points with tags associated with the readable points:

  1. Create an Output:Variable object for the simulation point whose value you wish to read
  2. Set the Output:Variable object attributes including name, reporting frequency, and key value as required
  3. Set the exportToBCVTB method to true

A ruby measure method for this is as follows:

def create_output(model, var, key, name, freq)

  # create reporting output variable
  new_var = OpenStudio::Model::OutputVariable.new(
    var, # from variable dictionary (eplusout.rdd)
    model
  )
  new_var.setName(name)
  new_var.setReportingFrequency(freq) # Detailed, Timestep, Hourly, Daily, Monthly, RunPeriod, Annual
  new_var.setKeyValue(key) # from variable dictionary (eplusout.rdd)
  new_var.setExportToBCVTB(true)

end

Writeable Points (Alfalfa Input)

The following steps should be taken in the OpenStudio model in order to expose writeable points in simulation to Alfalfa in order to acquire ExternalInterface objects needed for co-simulation, as well as Project Haystack points and tags associated with the writeable points.

  1. Create two (2) EnergyManagementSystem:GlobalVariable objects for the simulation actuator point you wish to expose
  2. Set the name of the first object (e.g. Fan_Mass_Flow)
  3. Set the name of the second object the same as the first but include _Enable (e.g. Fan_Mass_Flow_Enable)
  4. Set exportToBCVTB method to true for both

A ruby measure method for this is as follows:

def create_input(model, name, freq)

  # create global variable
  global_var = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(
    model,
    name
  )
  global_var.setExportToBCVTB(true)

  # create EMS output variable of global variable
  ems_out_var = OpenStudio::Model::EnergyManagementSystemOutputVariable.new(
    model,
    global_var
  )
  ems_out_var.setName(name + '_EMS_Value')
  ems_out_var.setUpdateFrequency('SystemTimestep')

  # create reporting output variable of EMS output variable of global variable
  global_out_var = OpenStudio::Model::OutputVariable.new(
    ems_out_var.nameString(),
    model
  )
  global_out_var.setName(name + '_Value')
  global_out_var.setReportingFrequency(freq) # Detailed, Timestep, Hourly, Daily, Monthly, RunPeriod, Annual
  global_out_var.setKeyValue('EMS')
  global_out_var.setExportToBCVTB(true)

  # create enable of global variable
  global_var_enable = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(
    model,
    name + "_Enable"
  )
  global_var_enable.setExportToBCVTB(true)

  # create EMS output variable of enable global variable
  ems_out_var_enable = OpenStudio::Model::EnergyManagementSystemOutputVariable.new(
    model,
    global_var_enable
  )
  ems_out_var_enable.setName(name + '_Enable_EMS_Value')
  ems_out_var_enable.setUpdateFrequency('SystemTimestep')

  # create reporting output variable of EMS output variable of enable global variable
  global_out_var_enable = OpenStudio::Model::OutputVariable.new(
    ems_out_var_enable.nameString(),
    model
  )
  global_out_var_enable.setName(name + '_Enable_Value')
  global_out_var_enable.setReportingFrequency(freq) # Detailed, Timestep, Hourly, Daily, Monthly, RunPeriod, Annual
  global_out_var_enable.setKeyValue('EMS')
  global_out_var_enable.setExportToBCVTB(true)

end

Writing EnergyPlus EMS Programs

The EMS feature in EnergyPlus provides a way to develop custom control and modeling routines for EnergyPlus models. Typically, a user will want to write EnergyPlus EMS Programs that change the operation of a model based on the value of the writeable point (Alfalfa input) via EMS actuators. EMS provides high-level, supervisory control to override selected aspects of EnergyPlus modeling. A small programming language called EnergyPlus Runtime Language (ERL) is used to describe the control algorithms. EnergyPlus interprets and executes the ERL program as the model is being run.

The following step should be taken in the OpenStudio model EnergyPlus EMS Programs and Subroutines that reference this writeable point:

  • EnergyPlus EMS Programs and Subroutines should reference the <NAME>_Enable variable by String name whose Boolean value (True or False) dictates whether or not EnergyPlus should write the value from Alfalfa to the associated EnergyPlus EMS actuator. For example:
IF Fan_Mass_Flow_enable == 1
  SET EMS_fanmassflow_actuator = Fan_Mass_Flow
ELSE,
  SET EMS_fanmassflow_actuator = NULL
ENDIF,

Where 1 is True and 0 is False.

Initializing Variables for Local Testing

Often, a user will want to test their EnergyPlus EMS Programs locally (e.g., run the OSW stand-alone). This can be helpful not only to make sure the workflow completes successfully but also to verify that the EMS program is operating as desired. However, the writeable points (Alfalfa inputs) described above will not have an initialized value if the OSW is run outside of Alfalfa. To alleviate this issue, a user can expand the create_input method above to write small EMS programs that initialize the variables and allow the OSW to be run stand-alone without crashing.

def create_input(model, name, freq, init_val)

  # [...]
  # same as create_input method above
  # [...]

  # add EMS initialization program
  init_prgm = OpenStudio::Model::EnergyManagementSystemProgram.new(model)
  init_prgm.setName('Init_' + name)
  init_prgm.addLine("SET #{name} = #{init_val}")
  init_prgm.addLine("SET #{name}_Enable = 1")

  # add EMS initialization program calling manager
  init_prgm_mngr = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model)
  init_prgm_mngr.setName('Init ' + name)
  init_prgm_mngr.setCallingPoint('BeginNewEnvironment')
  init_prgm_mngr.addProgram(init_prgm)

end

These initializations will NOT extend to Alfalfa; instead, Alfalfa will initialize the values to nil and the <NAME>_Enable to 0. If the user sets the value of a writeable point (Alfalfa input) the <NAME>_Enable will automatically be set to 1.

Using Python Measures with Alfalfa

If your model includes python measures which have package dependencies you need to tell alfalfa what the dependencies are so that it can install them for your project. To do this you just need to create a python requirements file called requirements.txt

Contents of an example requirements.txt (most basic is just the names of each of your packages).

sklearn
pysindy
CoolProp

Note: you may be running a different version of python from alfalfa so specifying exact versions of packages may not work properly.

Developing a Python Measure

For EnergyPlus, a Python Plugin is a user-defined Python class that overrides a premade EnergyPlusPlugin base class. Thus, a user plugin should always include:

from pyenergyplus.plugin import EnergyPlusPlugin

Including this means that when EnergyPlus processes the Python file, it will always find this imported base class. Then create a derived class that inherits this base class, and write custom functions. This will be illustrated with a minimal example. A minimal plugin does not actually have to interact with EnergyPlus. It is typical for a plugin to read data from output variables and write data with actuators, but it is not necessary. Consider this:

from pyenergyplus.plugin import EnergyPlusPlugin

class MinimalPlugin(EnergyPlusPlugin):

    def on_begin_timestep_before_predictor(self, state):
        return 0

OpenStudio Measure

The user will want to create a directory named resources at the same level as their measure.rb file. Inside that directory, the user will want to add their EnergyPlus Python file (could be minimal example script above, but more typically with reading data from output variables and writing data with actuators).

The OpenStudio (EnergyPlus) measure to would be:

# start the measure
class PythonEMS < OpenStudio::Ruleset::WorkspaceUserScript

  # human readable name
  def name
    return 'Python EMS'
  end

  # human readable description
  def description
    return 'Add python EMS to IDF'
  end

  # human readable description of modeling approach
  def modeler_description
    return 'Add python EMS to IDF'
  end

  # define the arguments that the user will input
  def arguments(ws)
    args = OpenStudio::Ruleset::OSArgumentVector.new

    # argument for python script name
    py_name = OpenStudio::Ruleset::OSArgument.makeStringArgument(
      'py_name',
      true
    )
    py_name.setDisplayName('Python Script Name')
    py_name.setDescription('Name of script with extension (e.g., myPlugin.py)')
    args << py_name

    return args
  end

  # define what happens when the measure is run
  def run(ws, runner, usr_args)

    # call the parent class method
    super(ws, runner, usr_args)

    # use the built-in error checking
    return false unless runner.validateUserArguments(
      arguments(ws),
      usr_args
    )

    # assign the user inputs to variables
    py_name = runner.getStringArgumentValue(
      'py_name',
      usr_args
    )

    # define python script dir
    py_dir = "#{__dir__}/resources"

    # make sure python script exists
    unless File.exist?("#{py_dir}/#{py_name}")
      runner.registerError("Could not find file at #{py_dir}/#{py_name}.")
      return false
    end

    # add python plugin search paths
    n = OpenStudio::IdfObject.new('PythonPlugin_SearchPaths'.to_IddObjectType)
    n.setString(0, 'Python Plugin Search Paths')
    n.setString(1, 'Yes')
    n.setString(2, 'Yes')
    # set site packages location depending on operating system
    if (RUBY_PLATFORM =~ /linux/) != nil
      n.setString(3, '/usr/local/lib/python3.7/dist-packages')
    elsif (RUBY_PLATFORM =~ /darwin/) != nil
      n.setString(3, '/usr/local/lib/python3.7/site-packages')
    elsif (RUBY_PLATFORM =~ /cygwin|mswin|mingw|bccwin|wince|emx/) != nil
      h = ENV['USERPROFILE'].gsub('\\', '/')
      n.setString(3, "#{h}/AppData/Local/Programs/Python/Python37/Lib/site-packages")
    end
    # add python dir
    n.setString(4, py_dir)
    ws.addObject(n)

    # add python plugin instance
    n = OpenStudio::IdfObject.new('PythonPlugin_Instance'.to_IddObjectType)
    n.setString(0, 'Python Plugin Instance Name')
    n.setString(1, 'No')
    n.setString(2, py_name.sub('.py', ''))
    n.setString(3, 'PluginClassName')
    ws.addObject(n)

  end

end

# register the measure to be used by the application
PythonEMS.new.registerWithApplication

Putting It All Together

The run method of the measure would then be something like:

def run(model, runner, user_args)

  # call the parent class method
  super(model, runner, user_args)

	# alfalfa outputs
  create_output(
    model,
    "Facility Total Purchased Electricity Energy", # from variable dictionary (eplusout.rdd)
    "Whole Building", # from report data dictionary (eplusout.rdd)
    "MyElectricMeter",
    "Timestep" # Detailed, Timestep, Hourly, Daily, Monthly, RunPeriod, Annual
  )

  # alfalfa inputs
  create_input(
    model,
    "MyInput",
    "Timestep" # Detailed, Timestep, Hourly, Daily, Monthly, RunPeriod, Annual
  )

  return true

end

Setting up an OSW

Alfalfa's OpenStudio support is exclusively through an OpenStudio Workflow (OSW). An OSW can be designed using the OpenStudio application or by correctly defining a directory structure with the necessary components. For those familiar with creating workflows, this is the standard directory structure. It generally takes the form:

  • path/to/my_model.osm
  • path/to/my_model/workflow.osw
  • path/to/my_model/measures/…[insert measure directories]
  • path/to/my_model/files/desired_weather_file.epw

An example of the OSW is also provided below, with the steps being the measure described above:

{
  "seed_file" : "myModel.osm",
  "weather_file": "USA_CO_Denver.Intl.AP.725650_TMY3.epw",
  "measure_paths": [
    "./measures/"
  ],
  "file_paths": [
    "./weather/"
  ],
  "run_directory": "./run/",
  "steps" : [
    {
      "measure_dir_name": "alfalfa_measure",
      "name": "Alfalfa Measure",
      "description": "Add variable(s) for Alfalfa",
      "modeler_description": "Add EMS global variable(s) required for Alfalfa"
    }
  ]
}

For existing workflows, simply add the measure described above to your measures directory and the step to your OSW (preferably as one of the first measures in the workflow).

Modifying Your OSW for a Python Measure

To incorporate the Python measure example above, insert this into your OSW:

{
  "measure_dir_name" : "python_ems",
  "name" : "Python EMS",
  "description" : "Add python EMS to IDF",
  "modeler_description" : "Add python EMS to IDF",
  "arguments" : {
    "py_name" : "myPlugin.py"
  }
}

Testing Locally

Before uploading to Alfalfa, it is a good idea to test the OSW locally. Typically, an EnergyPlus EMS Program that is meant to interface with Alfalfa will not 'do' anything when being tested locally, as the actuators in the program are set up to respond to changes in writable point (Alfalfa input) values. To navigate around this, placeholder EMS programs can be written that are only for local testing. This can be accomplished by adding temporary code to the run method above that will change the value of the writable point (Alfalfa input) values based on some arbitrary input (such as time of day or outdoor temperature).

def run(model, runner, user_args)

  # [...]
  # same as run method above
  # [...]

  # add EMS test program
  test_prgm = OpenStudio::Model::EnergyManagementSystemProgram.new(model)
  test_prgm.setName('Test_Pgm')
  test_prgm.addLine('SET hr = HOUR') # HOUR is built-in EMS variable with a value of 0–23 (whole hours only)
  test_prgm.addLine('IF ((hr >= 12) && (hr < 13))') # between noon and one
  test_prgm.addLine('SET <NAME> = <VALUE1>') # writable point (Alfalfa input)
  test_prgm.addLine('ELSE')
  test_prgm.addLine('SET <NAME> = <VALUE2>') # writable point (Alfalfa input)
  test_prgm.addLine('ENDIF')

  # add EMS test program calling manager
  test_prgm_mngr = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model)
  test_prgm_mngr.setName('Test_Mgr')
  test_prgm_mngr.setCallingPoint('BeginTimestepBeforePredictor')
  test_prgm_mngr.addProgram(test_prgm)

  return true

end

Set <VALUE1> and <VALUE2> as desired and run the OSW locally, typically with a command like:

/path/to/openstudio_binary run -w /path/to/my.osw

Then verify that the model behaved as desired, typically by confirming with a set of timeseries EnergyPlus Output:Variable objects from the EnergyPlus variable dictionary (eplusout.rdd). Once local testing is complete, make sure to remove this temporary code from the run method otherwise collisions with Alfalfa may occur.

Uploading to Alfalfa

Once the workflow has been setup, the model and its associated files simply need to be zipped together. To do this, select the following:

  1. The measure directory containing the measure(s)
    • the measure described above if the user only has an OpenStudio model
    • all the measures (including the measure described above) if the user already has an OpenStudio workflow
  2. The weather directory containing the weather file
  3. The OpenStudio model (.osm)
  4. The OpenStudio workflow (.osw)

Then (typically) right click, and the operating system will provide options for compressing. This zipped (.zip) folder can then be directly uploaded to Alfalfa.


Model Configuration

Openstudio

Tutorials

Guides

Reference

Modelica

Guides

Alfalfa Interaction

Tutorials

Guides

Reference

Explanation

Alfalfa Development

Guides

General

Reference

Explanation

Clone this wiki locally