How to Integrate Python based Electric Vehicle Models with OpenStudio Workflows - NatLabRockies/alfalfa GitHub Wiki
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 electric vehicle class
- The electric vehicle supply equipment class
- The EnergyPlus python script above
- The electric vehicle parameter file above, in a comma-separated format
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"
}
}