Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Model EV Batteries #1533

Draft
wants to merge 33 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
7b651b0
new arguments and method to write EV battery to xml
aspeake Oct 27, 2023
36492fb
custom schedule columns for EV batteries
aspeake Oct 27, 2023
08c8c5a
fix ev battery schedule generation, generalize for ev and non-ev batt…
aspeake Oct 30, 2023
3f94374
allow one EV and one non-EV battery in schema
aspeake Oct 30, 2023
744ad66
stub out defaults for EV batteries
aspeake Oct 30, 2023
4f1a37e
update how battery ids are assigned in xml
aspeake Oct 30, 2023
210c463
make ev_battery_preset argument optional
aspeake Oct 31, 2023
5c36fcb
Merge branch 'master' of https://github.com/NREL/OpenStudio-HPXML int…
shorowit Nov 2, 2023
01437d0
always require schedule when modeling ev batteries
aspeake Nov 9, 2023
8f33e3d
initial tests for ev batteries
aspeake Nov 9, 2023
52bc3ab
Merge remote-tracking branch 'origin/master' into ev_batteries
aspeake Nov 9, 2023
9db3aeb
add missing ev battery schedule for tests
aspeake Nov 9, 2023
da8dc17
update log message tests for batteries
aspeake Nov 16, 2023
357ba18
Merge remote-tracking branch 'origin/master' into ev_batteries
aspeake Nov 16, 2023
374aae8
fix test for battery log message
aspeake Nov 16, 2023
2a9b7bb
Latest results.
Nov 16, 2023
dd4d5cf
Merge remote-tracking branch 'origin/master' into ev_batteries
aspeake Jan 11, 2024
61d39b6
Seperate EVs from batteries and leverage existing EV charger fields
aspeake Jan 23, 2024
22f00b2
Merge remote-tracking branch 'origin/master' into ev_batteries
aspeake Jan 23, 2024
0056e2c
Merge branch 'ev_batteries' of https://github.com/NREL/OpenStudio-HPX…
aspeake Jan 23, 2024
f6b88a3
Refine inputs for EV battery and charger, update tests
aspeake Feb 5, 2024
1effbc8
Merge remote-tracking branch 'origin/master' into ev_batteries
aspeake Feb 5, 2024
6fbd4f5
update unit tests for EVs and chargers; apply rubocop
aspeake Feb 28, 2024
bd3fbfe
Merge remote-tracking branch 'origin/master' into ev_batteries
aspeake Feb 28, 2024
35e8be4
fix failing tests for ev batteries
aspeake Feb 29, 2024
cefa4e2
Merge remote-tracking branch 'origin/master' into ev_batteries
aspeake Feb 29, 2024
e279ea4
Latest results.
Feb 29, 2024
b86a506
Merge remote-tracking branch 'origin/master' into ev_batteries
aspeake Jun 6, 2024
3228c7a
Merge branch 'ev_batteries' of https://github.com/NREL/OpenStudio-HPX…
aspeake Jun 6, 2024
5d21e32
Update EV arguments for latest approach to argument handling
aspeake Jun 7, 2024
b1ef026
Generate an ELCD object for each EV battery
aspeake Jun 7, 2024
a413534
Update reporting measure to output EV energy
aspeake Jun 7, 2024
2432bfb
EMS program to adjust effective EV discharge power and offset that di…
aspeake Jun 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
73 changes: 73 additions & 0 deletions BuildResidentialHPXML/measure.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2578,6 +2578,51 @@ def arguments(model) # rubocop:disable Lint/UnusedMethodArgument
arg.setUnits('Frac')
args << arg

arg = OpenStudio::Measure::OSArgument::makeBoolArgument('ev_present', false)
arg.setDisplayName('Electric Vehicle: Present')
arg.setDescription('Whether there is an electric vehicle battery present.')
arg.setDefaultValue(false)
args << arg

arg = OpenStudio::Measure::OSArgument::makeDoubleArgument('ev_battery_discharge_power', false)
arg.setDisplayName('Electric Vehicle: Rated Battery Power Output')
arg.setDescription('The rated power output of the EV battery. If not provided, the OS-HPXML default is used.')
arg.setUnits('W')
args << arg

arg = OpenStudio::Measure::OSArgument::makeDoubleArgument('ev_battery_capacity', false)
arg.setDisplayName('Electric Vehicle: Nominal Battery Capacity')
arg.setDescription('The nominal capacity of the EV battery. If not provided, the OS-HPXML default is used.')
arg.setUnits('kWh')
args << arg

arg = OpenStudio::Measure::OSArgument::makeDoubleArgument('ev_battery_usable_capacity', false)
arg.setDisplayName('Electric Vehicle: Usable Battery Capacity')
arg.setDescription('The usable capacity of the EV battery. If not provided, the OS-HPXML default is used.')
arg.setUnits('kWh')
args << arg

arg = OpenStudio::Measure::OSArgument::makeBoolArgument('ev_charger_present', false)
arg.setDisplayName('Electric Vehicle Charger: Present')
arg.setDescription('Whether there is an electric vehicle charger present.')
arg.setDefaultValue(false)
args << arg

arg = OpenStudio::Measure::OSArgument::makeDoubleArgument('ev_charger_power', false)
arg.setDisplayName('Electric Vehicle Charger: Rated Charger Power Output')
arg.setDescription('The rated power output of the EV charger. If not provided, the OS-HPXML default is used.')
arg.setUnits('W')
args << arg

ev_charger_location_choices = OpenStudio::StringVector.new
ev_charger_location_choices << HPXML::LocationGarage
ev_charger_location_choices << HPXML::LocationOutside

arg = OpenStudio::Measure::OSArgument::makeChoiceArgument('ev_charger_location', ev_charger_location_choices, false)
arg.setDisplayName('Electric Vehicle Charger: Location')
arg.setDescription('The space type for the EV charger. If not provided, the OS-HPXML default is used.')
args << arg

arg = OpenStudio::Measure::OSArgument::makeIntegerArgument('battery_num_bedrooms_served', false)
arg.setDisplayName('Battery: Number of Bedrooms Served')
arg.setDescription("Number of bedrooms served by the lithium ion battery. Only needed if #{HPXML::ResidentialTypeSFA} or #{HPXML::ResidentialTypeApartment} and it is a shared battery serving multiple dwelling units. Used to apportion battery charging/discharging to the unit of a SFA/MF building.")
Expand Down Expand Up @@ -3815,6 +3860,7 @@ def self.create(runner, model, args, epw_path, hpxml_path, existing_hpxml_path)
set_solar_thermal(hpxml_bldg, args, epw_file)
set_pv_systems(hpxml_bldg, args, epw_file)
set_battery(hpxml_bldg, args)
set_electric_vehicle(hpxml_bldg, args)
set_lighting(hpxml_bldg, args)
set_dehumidifier(hpxml_bldg, args)
set_clothes_washer(hpxml_bldg, args)
Expand Down Expand Up @@ -6614,6 +6660,33 @@ def self.set_battery(hpxml_bldg, args)
number_of_bedrooms_served: number_of_bedrooms_served)
end

def self.set_electric_vehicle(hpxml_bldg, args)
if args[:ev_present] != true
return
end

charger_id = nil
if args[:ev_charger_present]
if args[:ev_charger_location].nil?
args[:ev_charger_location] = 'outside'
end
location = get_location(args[:ev_charger_location], hpxml_bldg.foundations[-1].foundation_type, hpxml_bldg.attics[-1].attic_type)

charger_id = "EVCharger#{hpxml_bldg.ev_chargers.size + 1}"
hpxml_bldg.ev_chargers.add(id: charger_id,
location: location,
charging_power: args[:ev_charger_power])
end

ev_ct = hpxml_bldg.vehicles.count { |vehicle| vehicle.id.include?('ElectricVehicle') }
hpxml_bldg.vehicles.add(id: "ElectricVehicle#{ev_ct + 1}",
type: HPXML::BatteryTypeLithiumIon,
rated_power_output: args[:ev_battery_discharge_power],
nominal_capacity_kwh: args[:ev_battery_capacity],
usable_capacity_kwh: args[:ev_battery_usable_capacity],
ev_charger_idref: charger_id)
end

# Set the lighting properties, including:
# - interior/exterior/garage fraction of lamps that are LFL/CFL/LED
# - interior/exterior/garage usage multipliers
Expand Down
12 changes: 11 additions & 1 deletion BuildResidentialHPXML/tests/test_build_residential_hpxml.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
class BuildResidentialHPXMLTest < Minitest::Test
def setup
@output_path = File.join(File.dirname(__FILE__), 'extra_files')
@model_save = false # true helpful for debugging, i.e., can render osm in 3D
@model_save = true # true helpful for debugging, i.e., can render osm in 3D
end

def teardown
Expand Down Expand Up @@ -68,6 +68,8 @@ def test_workflows
'extra-water-heater-attic.xml' => 'base-sfd.xml',
'extra-battery-crawlspace.xml' => 'base-sfd.xml',
'extra-battery-attic.xml' => 'base-sfd.xml',
'extra-ev-battery.xml' => 'extra-enclosure-garage-partially-protruded.xml',
'extra-two-batteries.xml' => 'base-sfd.xml',
'extra-detailed-performance-autosize.xml' => 'base-sfd.xml',
'extra-power-outage-periods.xml' => 'base-sfd.xml',

Expand Down Expand Up @@ -932,6 +934,14 @@ def _set_measure_argument_values(hpxml_file, args)
elsif ['extra-battery-attic.xml'].include? hpxml_file
args['battery_present'] = true
args['battery_location'] = HPXML::LocationAttic
elsif ['extra-ev-battery.xml'].include? hpxml_file
args['ev_present'] = true
args['ev_charger_present'] = true
args['ev_charger_location'] = HPXML::LocationGarage
elsif ['extra-two-batteries.xml'].include? hpxml_file
args['ev_present'] = true
args['battery_present'] = true
args['battery_location'] = HPXML::LocationAttic
elsif ['extra-detailed-performance-autosize.xml'].include? hpxml_file
args['heating_system_type'] = 'none'
args['cooling_system_type'] = 'none'
Expand Down
25 changes: 25 additions & 0 deletions HPXMLtoOpenStudio/measure.rb
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,7 @@ def create_unit_model(hpxml, hpxml_bldg, runner, model, epw_path, epw_file, weat
add_photovoltaics(model)
add_generators(model)
add_batteries(runner, model, spaces)
add_electric_vehicles(runner, model, spaces)
add_building_unit(model, unit_num)
end

Expand Down Expand Up @@ -2354,6 +2355,30 @@ def add_batteries(runner, model, spaces)
end
end

def add_electric_vehicles(runner, model, spaces)
@hpxml_bldg.vehicles.each do |vehicle|
next unless vehicle.id.include?('ElectricVehicle')

ev_charger = nil
if not vehicle.ev_charger_idref.nil?
@hpxml_bldg.ev_chargers.each do |charger|
next unless vehicle.ev_charger_idref == charger.id

ev_charger = charger
end
end

# Assign charging and vehicle space
if ev_charger
ev_charger.additional_properties.space = get_space_from_location(ev_charger.location, spaces)
vehicle.location = ev_charger.location
vehicle.additional_properties.space = get_space_from_location(vehicle.location, spaces)
end

ElectricVehicle.apply(runner, model, vehicle, ev_charger, @schedules_file, @hpxml_bldg.building_construction.number_of_units)
end
end

# TODO
#
# @param model [OpenStudio::Model::Model] OpenStudio Model object
Expand Down
58 changes: 44 additions & 14 deletions HPXMLtoOpenStudio/resources/battery.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,28 @@ class Battery
# @param schedules_file [SchedulesFile] SchedulesFile wrapper class instance of detailed schedule files
# @param unit_multiplier [Integer] Number of similar dwelling units
# @return [TODO] TODO
def self.apply(runner, model, nbeds, pv_systems, battery, schedules_file, unit_multiplier)
def self.apply(runner, model, nbeds, pv_systems, battery, schedules_file, unit_multiplier, is_ev: false, ev_charger: nil)
charging_schedule = nil
discharging_schedule = nil
if is_ev
charging_col = SchedulesFile::Columns[:EVBatteryCharging].name
discharging_col = SchedulesFile::Columns[:EVBatteryDischarging].name
else
charging_col = SchedulesFile::Columns[:BatteryCharging].name
discharging_col = SchedulesFile::Columns[:BatteryDischarging].name
end

if not schedules_file.nil?
charging_schedule = schedules_file.create_schedule_file(model, col_name: SchedulesFile::Columns[:BatteryCharging].name)
discharging_schedule = schedules_file.create_schedule_file(model, col_name: SchedulesFile::Columns[:BatteryDischarging].name)
charging_schedule = schedules_file.create_schedule_file(model, col_name: charging_col)
discharging_schedule = schedules_file.create_schedule_file(model, col_name: discharging_col)
end

if pv_systems.empty? && charging_schedule.nil? && discharging_schedule.nil?
if !is_ev && pv_systems.empty? && charging_schedule.nil? && discharging_schedule.nil?
runner.registerWarning('Battery without PV specified, and no charging/discharging schedule provided; battery is assumed to operate as backup and will not be modeled.')
return
elsif is_ev && charging_schedule.nil? && discharging_schedule.nil?
runner.registerWarning('Electric vehicle battery specified with no charging/discharging schedule provided; battery will not be modeled.')
return
end

obj_name = battery.id
Expand All @@ -48,7 +59,7 @@ def self.apply(runner, model, nbeds, pv_systems, battery, schedules_file, unit_m

return if rated_power_output <= 0 || nominal_capacity_kwh <= 0 || battery.nominal_voltage <= 0

if battery.is_shared_system
if !is_ev && battery.is_shared_system
# Apportion to single dwelling unit by # bedrooms
fail if battery.number_of_bedrooms_served.to_f <= nbeds.to_f # EPvalidator.xml should prevent this

Expand All @@ -57,9 +68,16 @@ def self.apply(runner, model, nbeds, pv_systems, battery, schedules_file, unit_m
rated_power_output = rated_power_output * nbeds.to_f / battery.number_of_bedrooms_served.to_f
end

if not ev_charger.nil?
charging_power = ev_charger.charging_power
else
charging_power = rated_power_output
end

nominal_capacity_kwh *= unit_multiplier
usable_capacity_kwh *= unit_multiplier
rated_power_output *= unit_multiplier
charging_power *= unit_multiplier

is_outside = (battery.location == HPXML::LocationOutside)
if not is_outside
Expand Down Expand Up @@ -113,24 +131,36 @@ def self.apply(runner, model, nbeds, pv_systems, battery, schedules_file, unit_m
elcs.setFullyChargedCellVoltage(default_nominal_cell_voltage)
elcs.setCellVoltageatEndofExponentialZone(default_nominal_cell_voltage)

elcds = model.getElectricLoadCenterDistributions
elcds = elcds.select { |elcd| elcd.inverter.is_initialized } # i.e., not generators
if elcds.empty?
if is_ev
# EVs always get their own ELCD, not PV
elcd = OpenStudio::Model::ElectricLoadCenterDistribution.new(model)
elcd.setName('Battery elec load center dist')
elcd.setName("#{obj_name} elec load center dist")
elcd.setElectricalBussType('AlternatingCurrentWithStorage')
elcs.setInitialFractionalStateofCharge(maximum_storage_state_of_charge_fraction)
else
elcd = elcds[0] # i.e., pv

elcd.setElectricalBussType('DirectCurrentWithInverterACStorage')
elcd.setStorageOperationScheme('TrackFacilityElectricDemandStoreExcessOnSite')
elcds = model.getElectricLoadCenterDistributions
elcds = elcds.select { |elcd| elcd.inverter.is_initialized } # i.e., not generators
# Use PV ELCD if present
elcds.each do |elcd_|
if elcd_.name.to_s.include? "PVSystem"
elcd = elcd_
elcd.setElectricalBussType('DirectCurrentWithInverterACStorage')
elcd.setStorageOperationScheme('TrackFacilityElectricDemandStoreExcessOnSite')
break
end
end
if elcds.empty?
elcd = OpenStudio::Model::ElectricLoadCenterDistribution.new(model)
elcd.setName("#{obj_name} elec load center dist")
elcd.setElectricalBussType('AlternatingCurrentWithStorage')
end
end

elcd.setMinimumStorageStateofChargeFraction(minimum_storage_state_of_charge_fraction)
elcd.setMaximumStorageStateofChargeFraction(maximum_storage_state_of_charge_fraction)
elcd.setElectricalStorage(elcs)
elcd.setDesignStorageControlDischargePower(rated_power_output)
elcd.setDesignStorageControlChargePower(rated_power_output)
elcd.setDesignStorageControlChargePower(charging_power)

if (not charging_schedule.nil?) && (not discharging_schedule.nil?)
elcd.setStorageOperationScheme('TrackChargeDischargeSchedules')
Expand Down
5 changes: 5 additions & 0 deletions HPXMLtoOpenStudio/resources/constants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,11 @@ def self.ObjectNameBatteryLossesAdjustment
return 'battery losses adjustment'
end


def self.ObjectNameEVBatteryDischargeOffset
return 'ev battery discharge offset'
end

# TODO
#
# @return [TODO] TODO
Expand Down