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

Add Load Flexibility measure #1259

Draft
wants to merge 43 commits into
base: resstock-args-refactor
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
5d68279
Initial measure draft
rajeee Jun 17, 2024
f407433
Test calling measure through upgrade
rajeee Jun 17, 2024
40142ea
Pass building info
rajeee Jun 18, 2024
23203d7
Commit flake8 file
rajeee Jun 18, 2024
d078a6e
demand response
Jun 20, 2024
758bc6f
on-peak hour
Jun 20, 2024
4aeb5d1
Add unit test
rajeee Jun 25, 2024
0cad8f9
adjust on-peak duration
Jul 24, 2024
f4dcde1
options lookup
Jul 24, 2024
87d7f43
unit test
Jul 26, 2024
8932ce8
unit test
Jul 26, 2024
e1df54a
Improve data structure
rajeee Aug 2, 2024
f4cfaa8
unit test
Aug 6, 2024
9441a9f
unit test
Aug 6, 2024
3e4a246
Update test files
rajeee Aug 13, 2024
3e0fe10
Merge branch 'load_flexibility' of https://github.com/NREL/resstock i…
rajeee Aug 13, 2024
fbad4bd
Some refactoring
rajeee Aug 14, 2024
d552b60
Merge branch 'develop' into load_flexibility
rajeee Aug 14, 2024
8401818
Some refactoring
rajeee Aug 16, 2024
1c724c0
Some missing files
rajeee Aug 16, 2024
1ae7ef1
unit test
Aug 31, 2024
c189cfe
unit test
Sep 5, 2024
6bd3241
unit test
Sep 5, 2024
659198f
revert testing upgrades yaml changes
rajeee Oct 8, 2024
32d990f
Make the test files run as script
rajeee Oct 8, 2024
b9670d6
Merge branch 'develop' into load_flexibility
rajeee Oct 8, 2024
56895df
Add python unit tests
rajeee Oct 8, 2024
c034212
Add python unit tests fix
rajeee Oct 8, 2024
faa4ff9
Run unit tests through openstudio directly
rajeee Oct 9, 2024
331437d
Changes to make it work with latest OS-HPXML
rajeee Oct 14, 2024
7db1464
Merge branch 'resstock-args-refactor' into load_flexibility
rajeee Oct 15, 2024
a0c4ce7
Move create schedule to PostHPXML measure
rajeee Oct 15, 2024
6337168
Rename resource file
rajeee Oct 15, 2024
121b23f
Modify setpoints
rajeee Nov 4, 2024
cb2047c
More updates
rajeee Nov 5, 2024
e3e3a9e
Merge branch 'resstock-args-refactor' into load_flexibility
rajeee Nov 5, 2024
67ba473
Commit options lookup update and yaml changes
rajeee Nov 5, 2024
d842b56
Revert workflow generator version update
rajeee Nov 5, 2024
abe9c0b
Revert HPXML changes
rajeee Nov 5, 2024
6345b33
Remove standalond flexbility measure
rajeee Nov 5, 2024
8989216
Remove all changes from hpxml-measures
rajeee Nov 5, 2024
f80bcb0
Remove all changes from hpxml-measures
rajeee Nov 5, 2024
2cc34bb
Remove byebug
rajeee Nov 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 103 additions & 25 deletions measures/ResStockArgumentsPostHPXML/measure.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
# see the URL below for information on how to write OpenStudio measures
# http://nrel.github.io/OpenStudio-user-documentation/reference/measure_writing_guide/

require_relative 'resources/hvac_flexibility/detailed_schedule_generator'
require_relative 'resources/hvac_flexibility/setpoint_modifier'

# start the measure
class ResStockArgumentsPostHPXML < OpenStudio::Measure::ModelMeasure
# human readable name
Expand Down Expand Up @@ -30,11 +33,47 @@ def arguments(model) # rubocop:disable Lint/UnusedMethodArgument
arg.setDescription('Absolute/relative path of the HPXML file.')
args << arg

# Add args for flexibility inputs. Use hours format for the duration and minutes for the random offset. Offsets are degree F.
arg = OpenStudio::Measure::OSArgument.makeIntegerArgument('loadflex_peak_duration_hours', false)
arg.setDisplayName('Load Flexibility: Peak Duration (hours)')
arg.setDescription('Duration of the peak period in hours.')
arg.setDefaultValue(0)
args << arg

arg = OpenStudio::Measure::OSArgument.makeIntegerArgument('loadflex_peak_offset', false)
arg.setDisplayName('Load Flexibility: Peak Offset (deg F)')
arg.setDescription('Offset of the peak period in degrees Fahrenheit.')
arg.setDefaultValue(0)
args << arg

arg = OpenStudio::Measure::OSArgument.makeIntegerArgument('loadflex_pre_peak_duration_hours', false)
arg.setDisplayName('Load Flexibility: Pre-Peak Duration (hours)')
arg.setDescription('Duration of the pre-peak period in hours.')
arg.setDefaultValue(0)
args << arg

arg = OpenStudio::Measure::OSArgument.makeIntegerArgument('loadflex_pre_peak_offset', false)
arg.setDisplayName('Load Flexibility: Pre-Peak Offset (deg F)')
arg.setDescription('Offset of the pre-peak period in degrees Fahrenheit.')
arg.setDefaultValue(0)
args << arg

arg = OpenStudio::Measure::OSArgument.makeIntegerArgument('loadflex_random_shift_minutes', false)
arg.setDisplayName('Load Flexibility: Random Shift (minutes)')
arg.setDescription('Number of minutes to randomly shift the peak period. If minutes less than timestep, will be assumed to be 0.')
arg.setDefaultValue(30)
args << arg

arg = OpenStudio::Measure::OSArgument::makeStringArgument('output_csv_path', false)
arg.setDisplayName('Schedules: Output CSV Path')
arg.setDescription('Absolute/relative path of the csv file containing user-specified occupancy schedules. Relative paths are relative to the HPXML output path.')
args << arg

arg = OpenStudio::Measure::OSArgument.makeIntegerArgument('building_id', false)
arg.setDisplayName('Building Unit ID')
arg.setDescription('The building unit number (between 1 and the number of samples).')
args << arg

return args
end

Expand All @@ -49,6 +88,10 @@ def run(model, runner, user_arguments)

# assign the user inputs to variables
args = runner.getArgumentValues(arguments(model), user_arguments)
if skip_load_flexibility?(args)
runner.registerInfo('Skipping ResStockArgumentsPostHPXML since load flexibility inputs are 0.')
return true
end

hpxml_path = args[:hpxml_path]
unless (Pathname.new hpxml_path).absolute?
Expand All @@ -58,43 +101,78 @@ def run(model, runner, user_arguments)
fail "'#{hpxml_path}' does not exist or is not an .xml file."
end

_hpxml = HPXML.new(hpxml_path: hpxml_path)
hpxml = HPXML.new(hpxml_path: hpxml_path)

# init
new_schedules = {}
# Parse the HPXML document
doc = XMLHelper.parse_file(hpxml_path)
hpxml_doc = XMLHelper.get_element(doc, '/HPXML')
doc_buildings = XMLHelper.get_elements(hpxml_doc, 'Building')

# Process each building
doc_buildings.each_with_index do |building, index|
schedule = create_schedule(hpxml, hpxml_path, runner, index)
modified_schedule = modify_schedule(hpxml, index, args, runner, schedule)
schedules_filepath = write_schedule(modified_schedule, args[:output_csv_path], index)
update_hpxml_schedule_filepath(building, schedules_filepath)
end

# TODO: populate new_schedules
# Write out the modified hpxml
XMLHelper.write_file(doc, hpxml_path)
runner.registerInfo("Wrote file: #{hpxml_path} with modified schedules.")
true
end

# return if not writing schedules
return true if new_schedules.empty?
def skip_load_flexibility?(args)
args[:loadflex_peak_duration_hours] == 0 && args[:loadflex_pre_peak_duration_hours] == 0
end

# write schedules
schedules_filepath = File.join(File.dirname(args[:output_csv_path].get), 'schedules2.csv')
write_new_schedules(new_schedules, schedules_filepath)
def create_schedule(hpxml, hpxml_path, runner, building_index)
generator = HVACScheduleGenerator.new(hpxml, hpxml_path, runner, building_index)
generator.get_heating_cooling_setpoint_schedule
end

# modify the hpxml with the schedules path
doc = XMLHelper.parse_file(hpxml_path)
extension = XMLHelper.create_elements_as_needed(XMLHelper.get_element(doc, '/HPXML'), ['SoftwareInfo', 'extension'])
schedules_filepaths = XMLHelper.get_values(extension, 'SchedulesFilePath', :string)
if !schedules_filepaths.include?(schedules_filepath)
XMLHelper.add_element(extension, 'SchedulesFilePath', schedules_filepath, :string)

# write out the modified hpxml
XMLHelper.write_file(doc, hpxml_path)
runner.registerInfo("Wrote file: #{hpxml_path}")
end
def modify_schedule(hpxml, building_index, args, runner, schedule)
minutes_per_step = hpxml.header.timestep
hpxml_bldg = hpxml.buildings[building_index]
building_id = (args[:building_id] or 0).to_i
state = hpxml_bldg.state_code
sim_year = hpxml.header.sim_calendar_year
schedule_modifier = HVACScheduleModifier.new(state: state,
sim_year: sim_year,
minutes_per_step: minutes_per_step,
runner: runner)
flexibility_inputs = get_flexibility_inputs(args, minutes_per_step, building_id)
schedule_modifier.modify_setpoints(schedule, flexibility_inputs)
end

return true
def get_flexibility_inputs(args, minutes_per_step, building_id)
srand(building_id)
max_random_shift_steps = (args[:loadflex_random_shift_minutes] / minutes_per_step).to_i
random_shift_steps = rand(-max_random_shift_steps..max_random_shift_steps)
FlexibilityInputs.new(
peak_duration_steps: args[:loadflex_peak_duration_hours] * 60 / minutes_per_step,
peak_offset: args[:loadflex_peak_offset],
pre_peak_duration_steps: args[:loadflex_pre_peak_duration_hours] * 60 / minutes_per_step,
pre_peak_offset: args[:loadflex_pre_peak_offset],
random_shift_steps: random_shift_steps
)
end

def write_new_schedules(schedules, schedules_filepath)
def write_schedule(schedule, output_csv_path, building_index)
schedules_filepath = File.join(File.dirname(output_csv_path), "detailed_schedules_#{building_index + 1}.csv")
CSV.open(schedules_filepath, 'w') do |csv|
csv << schedules.keys
rows = schedules.values.transpose
rows.each do |row|
csv << schedule.keys
schedule.values.transpose.each do |row|
csv << row.map { |x| '%.3g' % x }
end
end
return schedules_filepath
end

def update_hpxml_schedule_filepath(building, new_schedule_filepath)
building_extension = XMLHelper.create_elements_as_needed(building, ['BuildingDetails', 'BuildingSummary', 'extension'])
existing_schedules_filepaths = XMLHelper.get_values(building_extension, 'SchedulesFilePath', :string)
XMLHelper.add_element(building_extension, 'SchedulesFilePath', new_schedule_filepath, :string) unless existing_schedules_filepaths.include?(new_schedule_filepath)
end
end

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
require 'openstudio'
require_relative '../../../../resources/hpxml-measures/HPXMLtoOpenStudio/resources/meta_measure'
require_relative '../../../../resources/hpxml-measures/HPXMLtoOpenStudio/resources/constants'
require 'openstudio'
require 'pathname'
require 'oga'
require 'json'

Dir["#{File.dirname(__FILE__)}/../../../../resources/hpxml-measures/BuildResidentialScheduleFile/resources/*.rb"].each do |resource_file|
require resource_file
end
Dir["#{File.dirname(__FILE__)}/../../../../resources/hpxml-measures/HPXMLtoOpenStudio/resources/*.rb"].each do |resource_file|
next if resource_file.include? 'minitest_helper.rb'
require resource_file
end

class HVACScheduleGenerator

def initialize(hpxml, hpxml_path, runner, building_index)
@hpxml_path = hpxml_path
@hpxml = hpxml
@hpxml_bldg = @hpxml.buildings[building_index]
@epw_path = Location.get_epw_path(@hpxml_bldg, @hpxml_path)
@runner = runner
@weather = WeatherFile.new(epw_path: @epw_path, runner: @runner, hpxml: @hpxml)
@sim_year = Location.get_sim_calendar_year(@hpxml.header.sim_calendar_year, @weather)
@total_days_in_year = Calendar.num_days_in_year(@sim_year)
@sim_start_day = DateTime.new(@sim_year, 1, 1)
@minutes_per_step = @hpxml.header.timestep
@steps_in_day = 24 * 60 / @minutes_per_step
end


def get_heating_cooling_setpoint_schedule()
@runner.registerInfo("Creating heating and cooling setpoint schedules for building #{@hpxml_path}")
clg_weekday_setpoints, clg_weekend_setpoints, htg_weekday_setpoints, htg_weekend_setpoints = get_heating_cooling_weekday_weekend_setpoints

heating_setpoint = []
cooling_setpoint = []

@total_days_in_year.times do |day|
today = @sim_start_day + day
day_of_week = today.wday
if [0, 6].include?(day_of_week)
heating_setpoint_sch = htg_weekend_setpoints
cooling_setpoint_sch = clg_weekend_setpoints
else
heating_setpoint_sch = htg_weekday_setpoints
cooling_setpoint_sch = clg_weekday_setpoints
end
@steps_in_day.times do |step|
hour = (step * @minutes_per_step) / 60
heating_setpoint << heating_setpoint_sch[day][hour]
cooling_setpoint << cooling_setpoint_sch[day][hour]
end
end
return {heating_setpoint: heating_setpoint, cooling_setpoint: cooling_setpoint}
end

def c2f(setpoint_sch)
setpoint_sch.map { |i| i.map { |j| UnitConversions.convert(j, 'C', 'F') } }
end

def get_heating_cooling_weekday_weekend_setpoints
hvac_control = @hpxml_bldg.hvac_controls[0]
has_ceiling_fan = (@hpxml_bldg.ceiling_fans.size > 0)
hvac_season_days = get_heating_cooling_days(hvac_control)
hvac_control = @hpxml_bldg.hvac_controls[0]
onoff_thermostat_ddb = @hpxml.header.hvac_onoff_thermostat_deadband.to_f
htg_weekday_setpoints, htg_weekend_setpoints = HVAC.get_heating_setpoints(hvac_control, @sim_year, onoff_thermostat_ddb)
clg_weekday_setpoints, clg_weekend_setpoints = HVAC.get_cooling_setpoints(hvac_control, has_ceiling_fan, @sim_year, @weather, onoff_thermostat_ddb)

htg_weekday_setpoints, htg_weekend_setpoints, clg_weekday_setpoints, clg_weekend_setpoints = HVAC.create_setpoint_schedules(@runner, htg_weekday_setpoints, htg_weekend_setpoints, clg_weekday_setpoints, clg_weekend_setpoints, @sim_year, hvac_season_days)
return c2f(clg_weekday_setpoints), c2f(clg_weekend_setpoints), c2f(htg_weekday_setpoints), c2f(htg_weekend_setpoints)
end

def get_heating_cooling_days(hvac_control)
htg_start_month = hvac_control.seasons_heating_begin_month || 1
htg_start_day = hvac_control.seasons_heating_begin_day || 1
htg_end_month = hvac_control.seasons_heating_end_month || 12
htg_end_day = hvac_control.seasons_heating_end_day || 31
clg_start_month = hvac_control.seasons_cooling_begin_month || 1
clg_start_day = hvac_control.seasons_cooling_begin_day || 1
clg_end_month = hvac_control.seasons_cooling_end_month || 12
clg_end_day = hvac_control.seasons_cooling_end_day || 31
heating_days = Calendar.get_daily_season(@sim_year, htg_start_month, htg_start_day, htg_end_month, htg_end_day)
cooling_days = Calendar.get_daily_season(@sim_year, clg_start_month, clg_start_day, clg_end_month, clg_end_day)
return {:clg=>cooling_days, :htg=>heating_days}
end

def main(hpxml_path)
hpxml = HPXML.new(hpxml_path: hpxml_path)
sf = SchedulesFile.new(schedules_paths: hpxml.buildings[0].header.schedules_filepaths,
year: @year,
output_path: @tmp_schedule_file_path)

end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
require 'date'
require 'csv'
require 'json'
require 'openstudio'

Dir["#{File.dirname(__FILE__)}/../../../../resources/hpxml-measures/HPXMLtoOpenStudio/resources/*.rb"].each do |resource_file|
next if resource_file.include? 'minitest_helper.rb'
require resource_file
end

FlexibilityInputs = Struct.new(:peak_duration_steps, :peak_offset, :pre_peak_duration_steps, :pre_peak_offset, :random_shift_steps, keyword_init: true)
DailyPeakIndices = Struct.new(:pre_peak_start_index, :peak_start_index, :peak_end_index)


class HVACScheduleModifier
def initialize(state:, sim_year:, minutes_per_step:, runner:)
@state = state
@minutes_per_step = minutes_per_step
@runner = runner
@sim_year = Location.get_sim_calendar_year(sim_year, @weather)
@total_days_in_year = Calendar.num_days_in_year(@sim_year)
@sim_start_day = DateTime.new(@sim_year, 1, 1)
@steps_in_day = 24 * 60 / @minutes_per_step
@num_timesteps_per_hour = 60 / @minutes_per_step
current_dir = File.dirname(__FILE__)
@summer_peak_hours_dict = JSON.parse(File.read("#{current_dir}/state_summer_peak_hour_dict.json"))
@winter_peak_hours_dict = JSON.parse(File.read("#{current_dir}/state_winter_peak_hour_dict.json"))
end

def modify_setpoints(setpoints, flexibility_inputs)
log_inputs(flexibility_inputs)
heating_setpoint = setpoints[:heating_setpoint].dup
cooling_setpoint = setpoints[:cooling_setpoint].dup
raise "heating_setpoint.length != cooling_setpoint.length" unless heating_setpoint.length == cooling_setpoint.length

total_indices = heating_setpoint.length
total_indices.times do |index|
offset_times = _get_peak_times(index, flexibility_inputs)
heating_setpoint[index] += _get_setpoint_offset(index, 'heating', offset_times, flexibility_inputs)
cooling_setpoint[index] += _get_setpoint_offset(index, 'cooling', offset_times, flexibility_inputs)
end
{ heating_setpoint: heating_setpoint, cooling_setpoint: cooling_setpoint }
end

def _get_peak_times(index, flexibility_inputs)
month = _get_month(index:)
peak_hour = _get_peak_hour(month:)
peak_index = peak_hour * @num_timesteps_per_hour
peak_times = DailyPeakIndices.new
peak_times.peak_start_index = peak_index + flexibility_inputs.random_shift_steps
peak_times.peak_end_index = peak_times.peak_start_index + flexibility_inputs.peak_duration_steps
peak_times.pre_peak_start_index = peak_times.peak_start_index - flexibility_inputs.pre_peak_duration_steps
peak_times
end

def _get_setpoint_offset(index, setpoint_type, offset_times, flexibility_inputs)
case setpoint_type
when 'heating'
pre_peak_offset = flexibility_inputs.pre_peak_offset
peak_offset = -flexibility_inputs.peak_offset
when 'cooling'
pre_peak_offset = -flexibility_inputs.pre_peak_offset
peak_offset = flexibility_inputs.peak_offset
else
raise "Unsupported setpoint type: #{setpoint_type}"
end

index_in_day = index % (24 * @num_timesteps_per_hour)
if offset_times.pre_peak_start_index <= index_in_day && index_in_day < offset_times.peak_start_index
pre_peak_offset
elsif offset_times.peak_start_index <= index_in_day && index_in_day < offset_times.peak_end_index
peak_offset
else
0
end
end

def _get_month(index:)
start_of_year = Date.new(@sim_year, 1, 1)
index_date = start_of_year + (index.to_f / @num_timesteps_per_hour / 24)
index_date.month
end

def _get_peak_hour(month:)
if [6, 7, 8, 9].include?(month)
return @summer_peak_hours_dict[@state]
else
return @winter_peak_hours_dict[@state]
end
end

def log_inputs(inputs)
return unless @runner
@runner.registerInfo("Modifying setpoints ...")
@runner.registerInfo("peak_duration_steps=#{inputs.peak_duration_steps}")
@runner.registerInfo("pre_peak_duration_steps=#{inputs.pre_peak_duration_steps}")
@runner.registerInfo("random_shift_steps=#{inputs.random_shift_steps}")
@runner.registerInfo("pre_peak_offset=#{inputs.pre_peak_offset}")
@runner.registerInfo("peak_offset=#{inputs.peak_offset}")
end
end
Loading
Loading