Skip to content

How to Integrate Python based Electric Vehicle Models with OpenStudio Workflows

Maggie Sullivan edited this page Jun 29, 2023 · 1 revision

Introduction

Electric vehicles (EVs) can be modeled within an OpenStudio Workflow (OSW) in Alfalfa. Using OSWs with Alfalfa is described in detail here. Since using an OpenStudio model already requires a workflow, adding electric vehicle modeling means inserting a measure into the workflow that encapsulates the electric vehicle model.

Python Electric Vehicle Model

The electric vehicle model repository includes two Python classes:

  • One that models the charging and discharging behavior of an electric vehicle (EV)
  • One that emulates an electric vehicle supply equipment (EVSE) or the charging port

These classes can be instantiated with required parameter values to generate multiple EV and EVSE agents that can interact with each other through built-in methods.

EnergyPlus Python Script Example

To interact with the Python EV model above, an EnergyPlus-specific python script is needed. An example is shown below.

from pyenergyplus.plugin import EnergyPlusPlugin
from ElectricVehicles import ElectricVehicles
from evse_class import EVSE_class
import pandas as pd
import os

class EVSE(EnergyPlusPlugin):

    def __init__(self):

        super().__init__()
        self.need_to_get_handles = True
        self.ev_sch_handle = None

        # define list for EVs
        self.ev_list = []
        self.ev_pmax = []
        self.ev_inst = []

        # read CSV
        py_path = os.path.dirname(os.path.abspath(__file__))
        csv_path = os.path.join(py_path, 'in.csv')
        ev_sch = pd.read_csv(csv_path)

        # iterate over CSV rows
        for row in ev_sch.iterrows():
            ev = ElectricVehicles(
                departure_time = row[1]['departure_time'] * 60,
                vehicle_type = row[1]['vehicle_type'],
                arrival_time = row[1]['arrival_time'] * 60,
                initial_soc = row[1]['initial_soc'],
                target_soc = 0.9,
                batterycapacity_kWh = row[1]['batterycapacity_kWh']
            )
            self.ev_list.append(ev)
            self.ev_pmax.append(0)

        # loop over EV list and create EVSE
        for ev in self.ev_list:
            evse = EVSE_class(
                efficiency = 0.99,
                Prated_kW = 6.6,
                evse_id = self.ev_list.index(ev)
            )
            evse.server_setpoint = 10
            self.ev_inst.append(evse)

        # assign EVSE
        for ev in self.ev_list:
            ev.assign_evse(self.ev_inst[self.ev_list.index(ev)].evse_id)

    def get_handles(self, state):

        self.ev_sch_handle = self.api.exchange.get_actuator_handle(
            state,
            'Schedule:Constant',
            'Schedule Value',
            'EV Sch'
        )

    def on_begin_timestep_before_predictor(self, state) -> int:

        if not self.api.exchange.api_data_fully_ready(state):
            return 0

        if self.need_to_get_handles:
            self.get_handles(state)
            self.need_to_get_handles = False

        # timestep
        zt = self.api.exchange.zone_time_step(state)
        dt = zt * 60 * 60 # sec
        dw = self.api.exchange.day_of_week(state)
        ct = self.api.exchange.current_time(state)

        # calc time of week (min)
        if dw == 1:
            tw = ((dw + 5) * 24 + ct) * 60
        else:
            tw = ((dw - 2) * 24 + ct) * 60

        # charge vehicle
        for ev in self.ev_list:
            ev.chargevehicle(
                tw * 60,
                dt = dt,
                evsePower_kW = self.ev_pmax[self.ev_list.index(ev)]
            )

            # reset SOC to initial SOC at departure time
            pt = ev.departuretime / 60
            if ((tw == pt) or ((tw + 60*24*7) == pt)):
                ev.soc = ev.initialsoc

        # EV -> EVSE
        for evse in self.ev_inst:
            evse.receive_from_ev(
                self.ev_list[self.ev_inst.index(evse)].packvoltage,
                self.ev_list[self.ev_inst.index(evse)].packpower,
                self.ev_list[self.ev_inst.index(evse)].soc,
                self.ev_list[self.ev_inst.index(evse)].pluggedin,
                self.ev_list[self.ev_inst.index(evse)].readytocharge
            )

        # EVSE -> EV
        for i in range(len(self.ev_pmax)):
            if shed:
                self.ev_pmax[i] = 0
            else:
                self.ev_pmax[i] = self.ev_inst[i].send_to_ev()

        # calc charge power
        tot_ev_pwr = 0
        for evse in self.ev_inst:
            tot_ev_pwr = tot_ev_pwr + (evse.ev_power / evse.efficiency)

        # set total power actuator
        self.api.exchange.set_actuator_value(
            state,
            self.ev_sch_handle,
            tot_ev_pwr
        )

        return 0

EV Parameter File Example

The python script above reads a CSV file that defines each charging event. An example CSV file would be:

arrival_day_of_week arrival_time departure_time initial_soc batterycapacity_kWh vehicle_type
Monday 1200 1987 0.41 45 BEV
Tuesday 2455 3404 0.26 45 BEV
Wednesday 4165 4837 0.36 45 BEV
Thursday 5100 6229 0.48 45 BEV
Friday 6925 7851 0.15 45 BEV
Saturday 8360 8870 0.53 45 BEV
Sunday 9595 10153 0.60 45 BEV

The arrival and departure times are in minutes of the week.

OpenStudio Measure Example

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:

The OpenStudio (EnergyPlus) measure to would be:

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

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

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

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

  # define the arguments that the user will input
  def arguments(workspace)
    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., in.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

    # change timestep to 1 min
    ws.getObjectsByType('Timestep'.to_IddObjectType).each do |o|
      o.setInt(0, 60)
    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, 'EVSE Program')
    n.setString(1, 'No')
    n.setString(2, py_name.sub('.py', ''))
    n.setString(3, 'EVSE')
    ws.addObject(n)

    # add EV schedule type limits
    n = OpenStudio::IdfObject.new('ScheduleTypeLimits'.to_IddObjectType)
    n.setString(0, 'EV Sch Type Limits')
    ws.addObject(n)

    # add EV schedule to actuate
    n = OpenStudio::IdfObject.new('Schedule_Constant'.to_IddObjectType)
    n.setString(0, 'EV Sch')
    n.setString(1, 'EV Sch Type Limits')
    n.setInt(2, 0)
    ws.addObject(n)

    # add EVSE
    n = OpenStudio::IdfObject.new('Exterior_FuelEquipment'.to_IddObjectType)
    n.setString(0, 'EVSE')
    n.setString(1, 'Electricity')
    n.setString(2, 'EV Sch')
    n.setInt(3, 1)
    n.setString(4, '')
    ws.addObject(n)

  end

end

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

Modifying Your OpenStudio Workflow

Finally, insert this into your OSW:

{
  "measure_dir_name" : "python_ev",
  "name" : "Python EV",
  "description" : "Add python EV to IDF",
  "modeler_description" : "Add python EV pieces to IDF",
  "arguments" : {
    "py_name" : "in.py"
  }
}

Model Configuration

Openstudio

Tutorials

Guides

Reference

Modelica

Guides

Alfalfa Interaction

Tutorials

Guides

Reference

Explanation

Alfalfa Development

Guides

General

Reference

Explanation

Clone this wiki locally