diff --git a/docs/read_the_docs/source/basic_tutorial/architecture.rst b/docs/read_the_docs/source/basic_tutorial/architecture.rst index 5431cc7ccb..480df09a57 100644 --- a/docs/read_the_docs/source/basic_tutorial/architecture.rst +++ b/docs/read_the_docs/source/basic_tutorial/architecture.rst @@ -65,6 +65,7 @@ The BuildExistingModel and ApplyUpgrade meta measures call the following model m 1 ResStockArguments Model No ResStock 2 BuildResidentialHPXML Model No OS-HPXML 3 BuildResidentialScheduleFile Model No OS-HPXML + 4 ResStockArgumentsPostHPXML Model No ResStock ===== ============================= ================== ========= ============= ========================== .. _model-measures: @@ -117,6 +118,16 @@ They contribute to the generation of the model. :start-after: :end-before: < +**ResStockArgumentsPostHPXML** + + .. include:: ../../../../measures/ResStockArgumentsPostHPXML/measure.xml + :start-after: + :end-before: < + + .. include:: ../../../../measures/ResStockArgumentsPostHPXML/measure.xml + :start-after: + :end-before: < + .. _tutorial-apply-upgrade: **ApplyUpgrade** diff --git a/docs/read_the_docs/source/changelog/changelog_dev.rst b/docs/read_the_docs/source/changelog/changelog_dev.rst index f4439797ae..6f9f68d21d 100644 --- a/docs/read_the_docs/source/changelog/changelog_dev.rst +++ b/docs/read_the_docs/source/changelog/changelog_dev.rst @@ -141,3 +141,19 @@ Development Changelog Description: The purpose of this PR is to set a value in ResStockArguments for apartment units based on the type/size of MF building and where the unit is located (lower, middle, or upper story). + + .. change:: + :tags: workflow, feature + :pullreq: 929 + + **Date**: 2024-10-14 + + Title: + New ResStockArgumentsPostHPXML measure + + Description: + This measure is added to the workflow to post-process the output of the BuildResidentialHPXML and BuildResidentialScheduleFile measures. + + Assignees: Joe Robertson, Rajendra Adhikari + + GitHub: `pull request 929 `_ diff --git a/measures/ApplyUpgrade/measure.rb b/measures/ApplyUpgrade/measure.rb index cee8480132..e9ed4481fb 100644 --- a/measures/ApplyUpgrade/measure.rb +++ b/measures/ApplyUpgrade/measure.rb @@ -519,11 +519,21 @@ def run(model, runner, user_arguments) register_logs(runner, new_runner) return false end + measures['ResStockArgumentsPostHPXML'] = [{}] if !measures.keys.include?('ResStockArgumentsPostHPXML') + measures['ResStockArgumentsPostHPXML'][0]['hpxml_path'] = hpxml_path + measures['ResStockArgumentsPostHPXML'][0]['output_csv_path'] = File.expand_path('../schedules.csv') + measures['ResStockArgumentsPostHPXML'][0]['building_id'] = values['building_id'] + measures_hash = { 'ResStockArgumentsPostHPXML' => measures['ResStockArgumentsPostHPXML'] } + if not apply_measures(measures_dir, measures_hash, new_runner, model, true, 'OpenStudio::Measure::ModelMeasure', nil) + register_logs(runner, new_runner) + return false + end # Specify measures to run measures_to_apply_hash = { measures_dir => {} } - upgrade_measures = measures.keys - ['ResStockArguments', 'BuildResidentialHPXML', 'BuildResidentialScheduleFile'] + upgrade_measures = measures.keys - ['ResStockArguments', 'BuildResidentialHPXML', 'BuildResidentialScheduleFile', + 'ResStockArgumentsPostHPXML'] upgrade_measures.each do |upgrade_measure| measures_to_apply_hash[measures_dir][upgrade_measure] = measures[upgrade_measure] end @@ -537,7 +547,6 @@ def run(model, runner, user_arguments) register_logs(runner, new_runner) return false end - # Copy upgraded.xml to home.xml for downstream HPXMLtoOpenStudio # This will overwrite home.xml from BuildExistingModel # We need upgraded.xml (and not just home.xml) for UpgradeCosts @@ -545,6 +554,7 @@ def run(model, runner, user_arguments) FileUtils.cp(hpxml_path, in_path) register_logs(runner, resstock_arguments_runner) + register_logs(runner, new_runner) return true end diff --git a/measures/BuildExistingModel/measure.rb b/measures/BuildExistingModel/measure.rb index 01edee27cd..89aa79130a 100644 --- a/measures/BuildExistingModel/measure.rb +++ b/measures/BuildExistingModel/measure.rb @@ -752,6 +752,16 @@ def run(model, runner, user_arguments) end end + measures['ResStockArgumentsPostHPXML'] = [{}] if !measures.keys.include?('ResStockArgumentsPostHPXML') + measures['ResStockArgumentsPostHPXML'][0]['hpxml_path'] = hpxml_path + measures['ResStockArgumentsPostHPXML'][0]['output_csv_path'] = File.expand_path('../schedules.csv') + measures['ResStockArgumentsPostHPXML'][0]['building_id'] = args[:building_id] + measures_hash = { 'ResStockArgumentsPostHPXML' => measures['ResStockArgumentsPostHPXML'] } + if not apply_measures(measures_dir, measures_hash, new_runner, model, true, 'OpenStudio::Measure::ModelMeasure', nil) + register_logs(runner, new_runner) + return false + end + # Copy existing.xml to home.xml for downstream HPXMLtoOpenStudio # We need existing.xml (and not just home.xml) for UpgradeCosts in_path = File.expand_path('../home.xml') @@ -806,6 +816,7 @@ def run(model, runner, user_arguments) end register_logs(runner, resstock_arguments_runner) + register_logs(runner, new_runner) return true end diff --git a/measures/ResStockArguments/README.md b/measures/ResStockArguments/README.md index cf05f089bf..80ac4c04cd 100644 --- a/measures/ResStockArguments/README.md +++ b/measures/ResStockArguments/README.md @@ -6,7 +6,7 @@ ## Description Measure that pre-processes the arguments passed to the BuildResidentialHPXML and BuildResidentialScheduleFile measures. -Passes in all arguments from the options lookup, processes them, and then registers values to the runner to be used by other measures. +Passes in all ResStockArguments arguments from the options lookup, processes them, and then registers values to the runner to be used by other measures. ## Arguments diff --git a/measures/ResStockArguments/measure.rb b/measures/ResStockArguments/measure.rb index 9e14aea8db..974859c551 100644 --- a/measures/ResStockArguments/measure.rb +++ b/measures/ResStockArguments/measure.rb @@ -21,7 +21,7 @@ def description # human readable description of modeling approach def modeler_description - return 'Passes in all arguments from the options lookup, processes them, and then registers values to the runner to be used by other measures.' + return 'Passes in all ResStockArguments arguments from the options lookup, processes them, and then registers values to the runner to be used by other measures.' end # define the arguments that the user will input diff --git a/measures/ResStockArgumentsPostHPXML/measure.rb b/measures/ResStockArgumentsPostHPXML/measure.rb new file mode 100644 index 0000000000..ace3a59a9b --- /dev/null +++ b/measures/ResStockArgumentsPostHPXML/measure.rb @@ -0,0 +1,184 @@ +# frozen_string_literal: true + +# 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 + def name + # Measure name should be the title case of the class name. + return 'ResStock Arguments Post-HPXML' + end + + # human readable description + def description + return 'Measure that post-processes the output of the BuildResidentialHPXML and BuildResidentialScheduleFile measures.' + end + + # human readable description of modeling approach + def modeler_description + return 'Passes in all ResStockArgumentsPostHPXML arguments from the options lookup, processes them, and then modifies output of other measures.' + end + + # define the arguments that the user will input + def arguments(model) # rubocop:disable Lint/UnusedMethodArgument + args = OpenStudio::Measure::OSArgumentVector.new + + arg = OpenStudio::Measure::OSArgument.makeStringArgument('hpxml_path', false) + arg.setDisplayName('HPXML File Path') + 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.makeDoubleArgument('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.makeDoubleArgument('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(0) + 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 + + # define what happens when the measure is run + def run(model, runner, user_arguments) + super(model, runner, user_arguments) + + # use the built-in error checking + if !runner.validateUserArguments(arguments(model), user_arguments) + return false + end + + # 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? + hpxml_path = File.expand_path(File.join(File.dirname(__FILE__), hpxml_path)) + end + unless File.exist?(hpxml_path) && hpxml_path.downcase.end_with?('.xml') + fail "'#{hpxml_path}' does not exist or is not an .xml file." + end + + hpxml = HPXML.new(hpxml_path: hpxml_path) + + # 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 + + # Write out the modified hpxml + XMLHelper.write_file(doc, hpxml_path) + runner.registerInfo("Wrote file: #{hpxml_path} with modified schedules.") + true + end + + def skip_load_flexibility?(args) + args[:loadflex_peak_duration_hours] == 0 && args[:loadflex_pre_peak_duration_hours] == 0 + end + + 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 + + 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 + epw_path = Location.get_epw_path(hpxml_bldg, args[:hpxml_path]) + weather = WeatherFile.new(epw_path: epw_path, runner: runner, hpxml: hpxml) + schedule_modifier = HVACScheduleModifier.new(state: state, + sim_year: sim_year, + weather: weather, + epw_path: epw_path, + 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 + + 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).to_i, + peak_offset: args[:loadflex_peak_offset], + pre_peak_duration_steps: (args[:loadflex_pre_peak_duration_hours] * 60 / minutes_per_step).to_i, + pre_peak_offset: args[:loadflex_pre_peak_offset], + random_shift_steps: random_shift_steps + ) + end + + 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 << 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 + +# register the measure to be used by the application +ResStockArgumentsPostHPXML.new.registerWithApplication diff --git a/measures/ResStockArgumentsPostHPXML/measure.xml b/measures/ResStockArgumentsPostHPXML/measure.xml new file mode 100644 index 0000000000..95b31c5502 --- /dev/null +++ b/measures/ResStockArgumentsPostHPXML/measure.xml @@ -0,0 +1,56 @@ + + + 3.0 + res_stock_arguments_post_hpxml + db102ce5-ac96-4ef9-90d3-abbe53478716 + e9a3773b-66b5-48a6-95e5-4ce42262a640 + 20221103T232039Z + 2C38F48B + ResStockArgumentsPostHPXML + ResStock Arguments Post-HPXML + Measure that post-processes the output of the BuildResidentialHPXML and BuildResidentialScheduleFile measures. + Passes in all ResStockArgumentsPostHPXML arguments from the options lookup, processes them, and then modifies output of other measures. + + + hpxml_path + HPXML File Path + Absolute/relative path of the HPXML file. + String + false + false + + + output_csv_path + Schedules: Output CSV Path + Absolute/relative path of the csv file containing user-specified occupancy schedules. Relative paths are relative to the HPXML output path. + String + false + false + + + + + + Whole Building.Space Types + + + + Measure Type + ModelMeasure + string + + + + + + OpenStudio + 3.3.0 + 3.3.0 + + measure.rb + rb + script + 5A9AF6DF + + + diff --git a/measures/ResStockArgumentsPostHPXML/resources/hvac_flexibility/detailed_schedule_generator.rb b/measures/ResStockArgumentsPostHPXML/resources/hvac_flexibility/detailed_schedule_generator.rb new file mode 100644 index 0000000000..b7274fb5d4 --- /dev/null +++ b/measures/ResStockArgumentsPostHPXML/resources/hvac_flexibility/detailed_schedule_generator.rb @@ -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 diff --git a/measures/ResStockArgumentsPostHPXML/resources/hvac_flexibility/setpoint_modifier.rb b/measures/ResStockArgumentsPostHPXML/resources/hvac_flexibility/setpoint_modifier.rb new file mode 100644 index 0000000000..38a6adca48 --- /dev/null +++ b/measures/ResStockArgumentsPostHPXML/resources/hvac_flexibility/setpoint_modifier.rb @@ -0,0 +1,146 @@ +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:, weather:, epw_path:, minutes_per_step:, runner:) + @state = state + @minutes_per_step = minutes_per_step + @runner = runner + @weather = weather + @epw_path = epw_path + @daily_avg_temps = _get_daily_avg_temps + @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) + day_type = _get_day_type(index) + if day_type == 'heating' + heating_setpoint[index] += _get_setpoint_offset(index, 'heating', offset_times, flexibility_inputs) + # If the offset causes the set points to be inverted, adjust the cooling setpoint to correct it + # This can happen during pre-heating if originally the cooling and heating setpoints were the same + if heating_setpoint[index] > cooling_setpoint[index] + cooling_setpoint[index] = heating_setpoint[index] + end + else + cooling_setpoint[index] += _get_setpoint_offset(index, 'cooling', offset_times, flexibility_inputs) + # If the offset causes the set points to be inverted, adjust the heating setpoint to correct it + # This can happen during pre-cooling if originally the cooling and heating setpoints were the same + if heating_setpoint[index] > cooling_setpoint[index] + heating_setpoint[index] = cooling_setpoint[index] + end + end + 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 _get_day_type(index) + day = index % @steps_in_day + if @daily_avg_temps[day] < 66.0 + return 'heating' + else + return 'cooling' + end + end + + def _get_daily_avg_temps + epw_file = OpenStudio::EpwFile.new(@epw_path, true) + daily_avg_temps = [] + hourly_temps = [] + epw_file.data.each_with_index do |epwdata, rownum| + begin + db_temp = epwdata.dryBulbTemperature.get + rescue + fail "Cannot retrieve dryBulbTemperature from the EPW for hour #{rownum + 1}." + end + hourly_temps << db_temp + if (rownum + 1) % (24 * epw_file.recordsPerHour) == 0 + daily_avg_temps << hourly_temps.sum / hourly_temps.length + hourly_temps = [] + end + end + daily_avg_temps.map { |temp| UnitConversions.convert(temp, 'C', 'F') } + 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 diff --git a/measures/ResStockArgumentsPostHPXML/resources/hvac_flexibility/state_summer_peak_hour_dict.json b/measures/ResStockArgumentsPostHPXML/resources/hvac_flexibility/state_summer_peak_hour_dict.json new file mode 100644 index 0000000000..3b962787fd --- /dev/null +++ b/measures/ResStockArgumentsPostHPXML/resources/hvac_flexibility/state_summer_peak_hour_dict.json @@ -0,0 +1,52 @@ +{ + "HI": 18, + "AK": 18, + "IA": 18, + "WV": 18, + "MA": 18, + "VT": 19, + "WI": 19, + "CT": 18, + "TN": 18, + "NY": 19, + "UT": 19, + "VA": 19, + "SD": 21, + "OH": 19, + "SC": 19, + "MT": 18, + "MS": 18, + "NM": 19, + "NJ": 18, + "GA": 19, + "CA": 18, + "IL": 19, + "AZ": 19, + "TX": 18, + "NH": 18, + "DE": 18, + "WA": 18, + "OR": 18, + "MI": 19, + "MN": 19, + "WY": 20, + "IN": 19, + "OK": 17, + "NV": 18, + "KS": 19, + "PA": 18, + "MO": 18, + "ME": 18, + "KY": 19, + "CO": 18, + "NC": 18, + "AL": 18, + "ND": 19, + "AR": 18, + "FL": 18, + "ID": 19, + "LA": 18, + "MD": 18, + "NE": 17, + "RI": 19 +} \ No newline at end of file diff --git a/measures/ResStockArgumentsPostHPXML/resources/hvac_flexibility/state_winter_peak_hour_dict.json b/measures/ResStockArgumentsPostHPXML/resources/hvac_flexibility/state_winter_peak_hour_dict.json new file mode 100644 index 0000000000..cd3e9d43eb --- /dev/null +++ b/measures/ResStockArgumentsPostHPXML/resources/hvac_flexibility/state_winter_peak_hour_dict.json @@ -0,0 +1,52 @@ +{ + "HI": 18, + "AK": 18, + "IA": 18, + "WV": 18, + "MA": 18, + "VT": 18, + "WI": 18, + "CT": 19, + "TN": 18, + "NY": 19, + "UT": 18, + "VA": 18, + "SD": 18, + "OH": 18, + "SC": 6, + "MT": 19, + "MS": 18, + "NM": 19, + "NJ": 18, + "GA": 19, + "CA": 18, + "IL": 18, + "AZ": 18, + "TX": 18, + "NH": 18, + "DE": 18, + "WA": 18, + "OR": 18, + "MI": 19, + "MN": 19, + "WY": 9, + "IN": 19, + "OK": 6, + "NV": 19, + "KS": 19, + "PA": 18, + "MO": 18, + "ME": 18, + "KY": 18, + "CO": 19, + "NC": 18, + "AL": 18, + "ND": 20, + "AR": 18, + "FL": 18, + "ID": 19, + "LA": 18, + "MD": 18, + "NE": 17, + "RI": 18 +} \ No newline at end of file diff --git a/measures/ResStockArgumentsPostHPXML/tests/test_hvac_load_flexibility.rb b/measures/ResStockArgumentsPostHPXML/tests/test_hvac_load_flexibility.rb new file mode 100644 index 0000000000..36dadb685f --- /dev/null +++ b/measures/ResStockArgumentsPostHPXML/tests/test_hvac_load_flexibility.rb @@ -0,0 +1,136 @@ +require 'csv' +require 'parallel' +require 'openstudio' +require_relative '../../../resources/buildstock' +require_relative '../../../resources/hpxml-measures/HPXMLtoOpenStudio/resources/minitest_helper' +require_relative '../../../resources/hpxml-measures/HPXMLtoOpenStudio/resources/hpxml' +require_relative '../resources/hvac_flexibility/setpoint_modifier.rb' +require_relative '../measure.rb' +require 'pathname' + + +class ResStockArgumentsPostHPXMLTest < Minitest::Test + def setup + @runner = OpenStudio::Measure::OSRunner.new(OpenStudio::WorkflowJSON.new) + parent_path = File.expand_path("../../../../", __FILE__) + epw_path = File.join(parent_path, "resources/hpxml-measures/weather/USA_CO_Denver.Intl.AP.725650_TMY3.epw") + weather = WeatherFile.new(epw_path: epw_path, runner: nil) + @schedule_modifier_15 = HVACScheduleModifier.new(state:'CO', + sim_year:2024, + weather: weather, + epw_path: epw_path, + minutes_per_step:15, + runner:@runner) + + @schedule_modifier_60 = HVACScheduleModifier.new(state:'CO', + sim_year:2024, + weather: weather, + epw_path: epw_path, + minutes_per_step:60, + runner:@runner) + @non_leap_modifier = HVACScheduleModifier.new(state:'CO', + sim_year:2023, + weather: weather, + epw_path: epw_path, + minutes_per_step:15, + runner:@runner) + end + + def test_get_peak_hour + assert_equal(18, @schedule_modifier_15._get_peak_hour(month: 6)) + assert_equal(19, @schedule_modifier_15._get_peak_hour(month: 1)) + end + + def test_get_month + assert_equal(2, @schedule_modifier_15._get_month(index: 31 * 24 * 4 + 28 * 24 * 4 + 1)) # First 15 minutes of Feb 29 + assert_equal(2, @schedule_modifier_60._get_month(index: 31 * 24 + 28 * 24 + 1)) # First 1 hour of Feb 29 + assert_equal(3, @non_leap_modifier._get_month(index: 31 * 24 *4 + 28 * 24 * 4 + 1)) # First 15 minutes of March 1 + + assert_equal(12, @schedule_modifier_15._get_month(index: 366 * 24 * 4 - 1)) + assert_equal(12, @non_leap_modifier._get_month(index: 365 * 24 * 4 - 1)) + + assert_equal(1, @schedule_modifier_15._get_month(index: 0)) + assert_equal(1, @non_leap_modifier._get_month(index: 0)) + end + + def test_modify_setpoints + heating_setpoints = [71] * 366 * 24 * 4 + + setpoints = { + heating_setpoint: [71] * 366 * 24 * 4, + cooling_setpoint: [78] * 366 * 24 * 4 + } + flexibility_inputs = FlexibilityInputs.new( + random_shift_steps: 0, + pre_peak_duration_steps: 4 * 4, + peak_duration_steps: 2 * 4, + pre_peak_offset: 3, + peak_offset: 4 + ) + + modified_setpoints_15 = @schedule_modifier_15.modify_setpoints(setpoints, flexibility_inputs) + + winter_peak = 4 * @schedule_modifier_15._get_peak_hour(month: 1) + summer_peak = 4 * @schedule_modifier_15._get_peak_hour(month: 7) + summer_midnight = 8 * 30 * 24 * 4 + + assert_equal(71, modified_setpoints_15[:heating_setpoint][0]) + assert_equal(78, modified_setpoints_15[:cooling_setpoint][0]) + assert_equal(78 + 4, modified_setpoints_15[:cooling_setpoint][winter_peak]) # peak offset + assert_equal(71 - 4, modified_setpoints_15[:heating_setpoint][winter_peak]) # peak offset + assert_equal(78 - 3, modified_setpoints_15[:cooling_setpoint][winter_peak - 1]) # pre-peak offset + assert_equal(71 + 3, modified_setpoints_15[:heating_setpoint][winter_peak - 1]) # pre-peak offset + + assert_equal(71, modified_setpoints_15[:heating_setpoint][summer_midnight]) + assert_equal(78, modified_setpoints_15[:cooling_setpoint][summer_midnight]) + assert_equal(78 + 4, modified_setpoints_15[:cooling_setpoint][summer_midnight + summer_peak]) # peak offset + assert_equal(71 - 4, modified_setpoints_15[:heating_setpoint][summer_midnight + summer_peak]) # peak offset + assert_equal(78 - 3, modified_setpoints_15[:cooling_setpoint][summer_midnight + summer_peak - 1]) # pre-peak offset + assert_equal(71 + 3, modified_setpoints_15[:heating_setpoint][summer_midnight + summer_peak - 1]) # pre-peak offset + + flexibility_inputs = FlexibilityInputs.new( + random_shift_steps: 2, + pre_peak_duration_steps: 4 * 4, + peak_duration_steps: 2 * 4, + pre_peak_offset: 3, + peak_offset: 4 + ) + modified_setpoints_15 = @schedule_modifier_15.modify_setpoints(setpoints, flexibility_inputs) + assert_equal(71, modified_setpoints_15[:heating_setpoint][0]) + assert_equal(78, modified_setpoints_15[:cooling_setpoint][0]) + assert_equal(78 + 4, modified_setpoints_15[:cooling_setpoint][winter_peak + 2]) # peak offset + assert_equal(71 - 4, modified_setpoints_15[:heating_setpoint][winter_peak + 2]) # peak offset + assert_equal(78 - 3, modified_setpoints_15[:cooling_setpoint][winter_peak + 2 - 4 * 4]) # start of pre-peak offset + assert_equal(71 + 3, modified_setpoints_15[:heating_setpoint][winter_peak + 2 - 1]) # end of pre-peak offset + + assert_equal(71, modified_setpoints_15[:heating_setpoint][summer_midnight]) + assert_equal(78, modified_setpoints_15[:cooling_setpoint][summer_midnight]) + assert_equal(78 + 4, modified_setpoints_15[:cooling_setpoint][summer_midnight + summer_peak + 2]) # start of peak period + assert_equal(71 - 4, modified_setpoints_15[:heating_setpoint][summer_midnight + summer_peak + 2 + 2 * 4 - 1]) # end of peak period + assert_equal(78 - 0, modified_setpoints_15[:cooling_setpoint][summer_midnight + summer_peak + 2 - 4 * 4 - 1]) # before pre-peak period + assert_equal(71 + 0, modified_setpoints_15[:heating_setpoint][summer_midnight + summer_peak + 2 + 2 * 4]) # after peak period + + flexibility_inputs = FlexibilityInputs.new( + random_shift_steps: -2, + pre_peak_duration_steps: 0, + peak_duration_steps: 2 * 4, + pre_peak_offset: 3, # unused since pre_peak_duration_steps is 0 + peak_offset: 2 + ) + modified_setpoints_15 = @schedule_modifier_15.modify_setpoints(setpoints, flexibility_inputs) + assert_equal(71, modified_setpoints_15[:heating_setpoint][0]) + assert_equal(78, modified_setpoints_15[:cooling_setpoint][0]) + assert_equal(78 + 2, modified_setpoints_15[:cooling_setpoint][winter_peak - 2]) # peak offset + assert_equal(71 - 2, modified_setpoints_15[:heating_setpoint][winter_peak - 2]) # peak offset + assert_equal(78 - 0, modified_setpoints_15[:cooling_setpoint][winter_peak - 2 - 1]) # end of pre-peak period + assert_equal(71 + 0, modified_setpoints_15[:heating_setpoint][winter_peak - 2 - 1]) # end of pre-peak period + + assert_equal(71, modified_setpoints_15[:heating_setpoint][summer_midnight]) + assert_equal(78, modified_setpoints_15[:cooling_setpoint][summer_midnight]) + assert_equal(78 + 2, modified_setpoints_15[:cooling_setpoint][summer_midnight + summer_peak - 2]) # peak offset + assert_equal(71 - 2, modified_setpoints_15[:heating_setpoint][summer_midnight + summer_peak - 2]) # peak offset + assert_equal(78 - 0, modified_setpoints_15[:cooling_setpoint][summer_midnight + summer_peak - 2 - 1]) # before peak period + assert_equal(71 + 0, modified_setpoints_15[:heating_setpoint][summer_midnight + summer_peak - 2 - 1]) # before peak period + end + +end diff --git a/project_national/national_upgrades.yml b/project_national/national_upgrades.yml index 02051a3b63..a6fc0123a9 100644 --- a/project_national/national_upgrades.yml +++ b/project_national/national_upgrades.yml @@ -276,6 +276,11 @@ upgrades: - *pv_system_size_5_pt_0_kwdc - *pv_orientation_south + - upgrade_name: Short Mild Load Flexibility + options: + - option: LoadFlexibility|short_mild + + eagle: n_jobs: 3 minutes_per_sim: 5 diff --git a/project_national/pre_peak_sensitivity.yml b/project_national/pre_peak_sensitivity.yml new file mode 100644 index 0000000000..b25f159eb9 --- /dev/null +++ b/project_national/pre_peak_sensitivity.yml @@ -0,0 +1,155 @@ +schema_version: "0.5" +os_version: 3.9.0 +os_sha: c77fbb9569 +buildstock_directory: ../ # Relative to this file or absolute +project_directory: project_national # Relative to buildstock_directory +# output_directory: /Volumes/LaCie/load_flexibility/pre_peak_sensitivity_dec16_redo +output_directory: /projects/rescore/load_flex/pre_peak_sensitivity_dec16_redo +# weather_files_path: /Users/radhikar/Documents/BuildStock_TMY3_FIPS.zip +weather_files_path: kfs2/shared-projects/buildstock/weather/BuildStock_2020_FIPS.zip +# weather_files_path: c:/OpenStudio/BuildStock_TMY3_FIPS.zip + +sampler: + type: residential_quota + args: + n_datapoints: 1000 + +kestrel: + n_jobs: 30 + minutes_per_sim: 1 + account: rescore + postprocessing: + time: 120 + n_workers: 5 + n_procs: 20 + sampling: + time: 30 + +workflow_generator: + type: residential_hpxml + version: 2024.07.20 + args: + build_existing_model: + simulation_control_timestep: 15 + simulation_control_run_period_begin_month: 1 + simulation_control_run_period_begin_day_of_month: 1 + simulation_control_run_period_end_month: 12 + simulation_control_run_period_end_day_of_month: 31 + simulation_control_run_period_calendar_year: 2007 + + simulation_output_report: + timeseries_frequency: timestep + include_timeseries_total_consumptions: true + include_timeseries_fuel_consumptions: true + include_timeseries_end_use_consumptions: true + include_timeseries_emissions: false + include_timeseries_emission_fuels: false + include_timeseries_emission_end_uses: false + include_timeseries_hot_water_uses: true + include_timeseries_total_loads: true + include_timeseries_component_loads: true + include_timeseries_unmet_hours: true + include_timeseries_zone_temperatures: true + include_timeseries_airflows: true + include_timeseries_weather: true + include_timeseries_resilience: true + output_variables: + - name: Zone Mean Air Temperature + - name: Zone Thermostat Air Temperature + - name: Zone Mean Radiant Temperature + - name: Zone Operative Temperature + - name: Zone Air Relative Humidity + - name: Zone Air Heat Balance Air Energy Storage Rate + - name: Zone Air Heat Balance Deviation Rate + - name: Zone Air System Sensible Heating Rate + - name: Zone Air System Sensible Cooling Rate + - name: Zone Total Internal Total Heating Energy + - name: Surface Inside Face Temperature + - name: Surface Heat Storage Energy + - name: Surface Outside Face Temperature + - name: Zone Air Heat Balance Air Energy Storage Rate + - name: Space Total Internal Total Heating Energy + - name: People Sensible Heating Rate + - name: People Total Heating Rate + - name: Site Outdoor Air Humidity Ratio + - name: Site Outdoor Air Drybulb Temperature + - name: Site Outdoor Air Wetbulb Temperature + + + reporting_measures: + - measure_dir_name: QOIReport + + server_directory_cleanup: + retain_schedules_csv: true + +baseline: + n_buildings_represented: 139647020 # American Community Survey 2021 5-year, B25001, does not include territories ( 138765649 without AK and HI ) + skip_sims: True + +upgrades: + # - upgrade_name: scenario1 + # options: + # - option: LoadFlexibility|scenario1 + - upgrade_name: scenario2 + options: + - option: LoadFlexibility|scenario2 + - upgrade_name: scenario3 + options: + - option: LoadFlexibility|scenario3 + # - upgrade_name: scenario4 + # options: + # - option: LoadFlexibility|scenario4 + # - upgrade_name: scenario5 + # options: + # - option: LoadFlexibility|scenario5 + # - upgrade_name: scenario6 + # options: + # - option: LoadFlexibility|scenario6 + # - upgrade_name: scenario7 + # options: + # - option: LoadFlexibility|scenario7 + - upgrade_name: scenario8 + options: + - option: LoadFlexibility|scenario8 + - upgrade_name: scenario9 + options: + - option: LoadFlexibility|scenario9 + # - upgrade_name: scenario10 + # options: + # - option: LoadFlexibility|scenario10 + # - upgrade_name: scenario11 + # options: + # - option: LoadFlexibility|scenario11 + # - upgrade_name: scenario12 + # options: + # - option: LoadFlexibility|scenario12 + - upgrade_name: scenario13 + options: + - option: LoadFlexibility|scenario13 + - upgrade_name: scenario14 + options: + - option: LoadFlexibility|scenario14 + # - upgrade_name: scenario15 + # options: + # - option: LoadFlexibility|scenario15 + # - upgrade_name: scenario16 + # options: + # - option: LoadFlexibility|scenario16 + # - upgrade_name: scenario17 + # options: + # - option: LoadFlexibility|scenario17 + +postprocessing: + keep_individual_timeseries: true + partition_columns: + - State + - County + aws: + region_name: us-west-2 + s3: + bucket: resstock-core + prefix: load_flexibility/test_runs + athena: + glue_service_role: service-role/AWSGlueServiceRole-default + database_name: load_flexibility + max_crawling_time: 1200 diff --git a/project_testing/testing_upgrades.yml b/project_testing/testing_upgrades.yml index 058656b022..091ca4b6f9 100644 --- a/project_testing/testing_upgrades.yml +++ b/project_testing/testing_upgrades.yml @@ -318,6 +318,10 @@ upgrades: - *pv_system_size_5_pt_0_kwdc - *pv_orientation_south + - upgrade_name: Short Mild Load Flexibility + options: + - option: LoadFlexibility|short_mild + eagle: n_jobs: 3 minutes_per_sim: 5 diff --git a/resources/buildstock.rb b/resources/buildstock.rb index 0087654416..6969bb0ccd 100644 --- a/resources/buildstock.rb +++ b/resources/buildstock.rb @@ -429,6 +429,19 @@ def update_args_hash(hash, key, args) end end +def register_logs(runner, new_runner) + new_runner.result.warnings.each do |warning| + runner.registerWarning(warning.logMessage) + end + new_runner.result.info.each do |info| + runner.registerInfo(info.logMessage) + end + new_runner.result.errors.each do |error| + runner.registerError(error.logMessage) + end + return +end + class RunOSWs require 'openstudio' require 'csv' diff --git a/resources/options_lookup.tsv b/resources/options_lookup.tsv index 9903e4d72a..d32cb8ffd9 100644 --- a/resources/options_lookup.tsv +++ b/resources/options_lookup.tsv @@ -13971,3 +13971,24 @@ Windows "Triple, Low-E, Insulated, Argon, L-Gain" ResStockArguments window_ufact Windows "Triple, Low-E, Non-metal, Air, L-Gain" ResStockArguments window_ufactor=0.29 window_shgc=0.26 skylight_ufactor=0.37 skylight_shgc=0.3 skylight_storm_type=auto window_exterior_shading_type=auto window_exterior_shading_summer=auto window_exterior_shading_winter=auto window_natvent_availability=auto window_shading_summer_season=auto window_insect_screens=auto Windows No Windows ResStockArguments window_ufactor=0.84 window_shgc=0.63 skylight_ufactor=0.37 skylight_shgc=0.3 skylight_storm_type=auto window_exterior_shading_type=auto window_exterior_shading_summer=auto window_exterior_shading_winter=auto window_natvent_availability=auto window_shading_summer_season=auto window_insect_screens=auto Windows Void + +LoadFlexibility scenario1 ResStockArgumentsPostHPXML loadflex_peak_duration_hours=4 loadflex_peak_offset=2 loadflex_pre_peak_duration_hours=0 loadflex_pre_peak_offset=2 +LoadFlexibility scenario2 ResStockArgumentsPostHPXML loadflex_peak_duration_hours=4 loadflex_peak_offset=2 loadflex_pre_peak_duration_hours=0.25 loadflex_pre_peak_offset=2 +LoadFlexibility scenario3 ResStockArgumentsPostHPXML loadflex_peak_duration_hours=4 loadflex_peak_offset=2 loadflex_pre_peak_duration_hours=0.5 loadflex_pre_peak_offset=2 +LoadFlexibility scenario4 ResStockArgumentsPostHPXML loadflex_peak_duration_hours=4 loadflex_peak_offset=2 loadflex_pre_peak_duration_hours=1 loadflex_pre_peak_offset=2 +LoadFlexibility scenario5 ResStockArgumentsPostHPXML loadflex_peak_duration_hours=4 loadflex_peak_offset=2 loadflex_pre_peak_duration_hours=2 loadflex_pre_peak_offset=2 +LoadFlexibility scenario6 ResStockArgumentsPostHPXML loadflex_peak_duration_hours=4 loadflex_peak_offset=2 loadflex_pre_peak_duration_hours=4 loadflex_pre_peak_offset=2 +LoadFlexibility scenario7 ResStockArgumentsPostHPXML loadflex_peak_duration_hours=4 loadflex_peak_offset=4 loadflex_pre_peak_duration_hours=0 loadflex_pre_peak_offset=4 +LoadFlexibility scenario8 ResStockArgumentsPostHPXML loadflex_peak_duration_hours=4 loadflex_peak_offset=4 loadflex_pre_peak_duration_hours=0.25 loadflex_pre_peak_offset=4 +LoadFlexibility scenario9 ResStockArgumentsPostHPXML loadflex_peak_duration_hours=4 loadflex_peak_offset=4 loadflex_pre_peak_duration_hours=0.5 loadflex_pre_peak_offset=4 +LoadFlexibility scenario10 ResStockArgumentsPostHPXML loadflex_peak_duration_hours=4 loadflex_peak_offset=4 loadflex_pre_peak_duration_hours=1 loadflex_pre_peak_offset=4 +LoadFlexibility scenario11 ResStockArgumentsPostHPXML loadflex_peak_duration_hours=4 loadflex_peak_offset=4 loadflex_pre_peak_duration_hours=2 loadflex_pre_peak_offset=4 +LoadFlexibility scenario12 ResStockArgumentsPostHPXML loadflex_peak_duration_hours=4 loadflex_peak_offset=4 loadflex_pre_peak_duration_hours=4 loadflex_pre_peak_offset=4 +LoadFlexibility scenario13 ResStockArgumentsPostHPXML loadflex_peak_duration_hours=4 loadflex_peak_offset=4 loadflex_pre_peak_duration_hours=0.25 loadflex_pre_peak_offset=6 +LoadFlexibility scenario14 ResStockArgumentsPostHPXML loadflex_peak_duration_hours=4 loadflex_peak_offset=4 loadflex_pre_peak_duration_hours=0.5 loadflex_pre_peak_offset=6 +LoadFlexibility scenario15 ResStockArgumentsPostHPXML loadflex_peak_duration_hours=4 loadflex_peak_offset=4 loadflex_pre_peak_duration_hours=1 loadflex_pre_peak_offset=6 +LoadFlexibility scenario16 ResStockArgumentsPostHPXML loadflex_peak_duration_hours=4 loadflex_peak_offset=4 loadflex_pre_peak_duration_hours=2 loadflex_pre_peak_offset=6 +LoadFlexibility scenario17 ResStockArgumentsPostHPXML loadflex_peak_duration_hours=4 loadflex_peak_offset=4 loadflex_pre_peak_duration_hours=4 loadflex_pre_peak_offset=6 + + +LoadFlexibility no_pre_peak_medium_moderate ResStockArgumentsPostHPXML loadflex_peak_duration_hours=1 loadflex_peak_offset=2 loadflex_pre_peak_duration_hours= loadflex_pre_peak_offset=