From 19740a71b2755f3340c1c8905d8182d23510d2d9 Mon Sep 17 00:00:00 2001 From: Rajendra Adhikari Date: Thu, 20 Jun 2024 15:53:40 -0500 Subject: [PATCH 01/13] Generate EV schedules --- BuildResidentialScheduleFile/measure.rb | 3 +- .../resources/schedules.rb | 66 ++++++++++++++++++- .../test_build_residential_schedule_file.rb | 17 +++++ HPXMLtoOpenStudio/resources/schedules.rb | 4 +- 4 files changed, 86 insertions(+), 4 deletions(-) diff --git a/BuildResidentialScheduleFile/measure.rb b/BuildResidentialScheduleFile/measure.rb index 094c50c604..1c6684a745 100644 --- a/BuildResidentialScheduleFile/measure.rb +++ b/BuildResidentialScheduleFile/measure.rb @@ -184,7 +184,8 @@ def create_schedules(runner, hpxml, hpxml_bldg, epw_file, args) get_generator_inputs(hpxml_bldg, epw_file, args) args[:resources_path] = File.join(File.dirname(__FILE__), 'resources') - schedule_generator = ScheduleGenerator.new(runner: runner, epw_file: epw_file, **args) + schedule_generator = ScheduleGenerator.new(runner: runner, hpxml_bldg: hpxml_bldg, + epw_file: epw_file, **args) success = schedule_generator.create(args: args) return false if not success diff --git a/BuildResidentialScheduleFile/resources/schedules.rb b/BuildResidentialScheduleFile/resources/schedules.rb index 68fb3bfb94..15bfa0727a 100644 --- a/BuildResidentialScheduleFile/resources/schedules.rb +++ b/BuildResidentialScheduleFile/resources/schedules.rb @@ -5,6 +5,7 @@ class ScheduleGenerator def initialize(runner:, + hpxml_bldg:, epw_file:, state:, column_names: nil, @@ -19,6 +20,7 @@ def initialize(runner:, debug:, **) @runner = runner + @hpxml_bldg = hpxml_bldg @epw_file = epw_file @state = state @column_names = column_names @@ -576,7 +578,7 @@ def create_stochastic_schedules(args:) @schedules[SchedulesFile::Columns[:HotWaterFixtures].name] = [showers, sinks, baths].transpose.map { |flow| flow.reduce(:+) } fixtures_peak_flow = @schedules[SchedulesFile::Columns[:HotWaterFixtures].name].max @schedules[SchedulesFile::Columns[:HotWaterFixtures].name] = @schedules[SchedulesFile::Columns[:HotWaterFixtures].name].map { |flow| flow / fixtures_peak_flow } - + fill_ev_battery_schedule(all_simulated_values, prng) return true end @@ -917,4 +919,66 @@ def get_building_america_lighting_schedule(epw_file) return lighting_sch end + def _get_ev_battery_schedule(away_schedule, hours_driven_per_year) + total_driving_minutes_per_year = hours_driven_per_year * 60 + + expanded_away_schedule = away_schedule.flat_map { |status| [status] * 15 } + + charging_schedule = [] + discharging_schedule = [] + driving_minutes_used = 0 + + chunk_counts = expanded_away_schedule.chunk(&:itself).map { |value, elements| [value, elements.size] } + total_away_minutes = chunk_counts.map {|value, size| value * size}.sum + + chunk_counts.each do |is_away, activity_minutes| + if is_away == 1 + current_chunk_proportion = (1.0 * activity_minutes) / total_away_minutes + + expected_driving_time = (total_driving_minutes_per_year * current_chunk_proportion).round + max_driving_time = [expected_driving_time, total_driving_minutes_per_year - driving_minutes_used].min + + max_possible_driving_time = (activity_minutes * 0.8).ceil + actual_driving_time = [max_driving_time, max_possible_driving_time].min + + idle_time = activity_minutes - actual_driving_time + first_half_driving = (actual_driving_time / 2.0).ceil + second_half_driving = actual_driving_time - first_half_driving + + discharging_schedule.concat([-1] * first_half_driving) # Start driving + discharging_schedule.concat([0] * idle_time) # Idle in the middle + discharging_schedule.concat([-1] * second_half_driving) # End driving + charging_schedule.concat([0] * activity_minutes) + + driving_minutes_used += actual_driving_time + else + charging_schedule.concat([1] * activity_minutes) + discharging_schedule.concat([0] * activity_minutes) + end + end + + if driving_minutes_used < total_driving_minutes_per_year + @runner.registerError("The occupant has less away hours than hours EV is driven") + raise "Insufficient away time for required driving hours" + end + return charging_schedule, discharging_schedule + end + + def fill_ev_battery_schedule(markov_chain_simulation_result, prng) + # randomly pick 1 occupant + # TODO: determine the occupant based on best match for miles driven and occupant behavior + occupant_number = prng.rand(@num_occupants) + away_index = 5 # Index of away activity in the markov-chain simulator + away_schedule = markov_chain_simulation_result[occupant_number].column(away_index) + if bldg_properties.nil? + hours_driven_per_year = 500 + else + hours_driven_per_year = bldg_properties['ev_effective_hours_per_year'] + end + charging_schedule, discharging_schedule = _get_ev_battery_schedule(away_schedule, hours_driven_per_year) + agg_charging_schedule = aggregate_array(charging_schedule, @minutes_per_step).map { |val| val / 60.0 } + agg_discharging_schedule = aggregate_array(discharging_schedule, @minutes_per_step).map { |val| val / 60.0 } + @schedules[SchedulesFile::Columns[:EVBatteryCharging].name] = agg_charging_schedule + @schedules[SchedulesFile::Columns[:EVBatteryDischarging].name] = agg_discharging_schedule + end end diff --git a/BuildResidentialScheduleFile/tests/test_build_residential_schedule_file.rb b/BuildResidentialScheduleFile/tests/test_build_residential_schedule_file.rb index c23b0a354c..ec53202336 100644 --- a/BuildResidentialScheduleFile/tests/test_build_residential_schedule_file.rb +++ b/BuildResidentialScheduleFile/tests/test_build_residential_schedule_file.rb @@ -279,6 +279,23 @@ def test_zero_occupants assert_empty(hpxml.buildings[0].header.schedules_filepaths) end + def test_ev_battery + num_occupants = 0.0 + + hpxml = _create_hpxml('base-battery-ev.xml') + hpxml.buildings[0].building_occupancy.number_of_residents = num_occupants + XMLHelper.write_file(hpxml.to_doc, @tmp_hpxml_path) + + @args_hash['output_csv_path'] = File.absolute_path(File.join(@tmp_output_path, 'occupancy-stochastic.csv')) + _hpxml, result = _test_measure() + + info_msgs = result.info.map { |x| x.logMessage } + assert(1, info_msgs.size) + assert(info_msgs.any? { |info_msg| info_msg.include?('Number of occupants set to zero; skipping generation of stochastic schedules.') }) + assert(!File.exist?(@args_hash['output_csv_path'])) + assert_empty(hpxml.buildings[0].header.schedules_filepaths) + end + def test_multiple_buildings hpxml = _create_hpxml('base-bldgtype-mf-whole-building.xml') hpxml.buildings.each do |hpxml_bldg| diff --git a/HPXMLtoOpenStudio/resources/schedules.rb b/HPXMLtoOpenStudio/resources/schedules.rb index e1f1e241a0..9e191428f8 100644 --- a/HPXMLtoOpenStudio/resources/schedules.rb +++ b/HPXMLtoOpenStudio/resources/schedules.rb @@ -1432,8 +1432,8 @@ def initialize(name, used_by_unavailable_periods, can_be_stochastic, type) BatteryCharging: Column.new('battery_charging', false, false, nil), BatteryDischarging: Column.new('battery_discharging', false, false, nil), EVBattery: Column.new('ev_battery', false, false, :neg_one_to_one), - EVBatteryCharging: Column.new('ev_battery_charging', false, false, nil), - EVBatteryDischarging: Column.new('ev_battery_discharging', false, false, nil), + EVBatteryCharging: Column.new('ev_battery_charging', false, true, nil), + EVBatteryDischarging: Column.new('ev_battery_discharging', false, true, nil), HVAC: Column.new('hvac', true, false, nil), WaterHeater: Column.new('water_heater', true, false, nil), Dehumidifier: Column.new('dehumidifier', true, false, nil), From d5db465f20a9ce367cac9704ec4f85607690ce2b Mon Sep 17 00:00:00 2001 From: Rajendra Adhikari Date: Mon, 24 Jun 2024 10:44:56 -0500 Subject: [PATCH 02/13] Fix test and read from HPXML --- BuildResidentialScheduleFile/measure.rb | 2 +- .../resources/schedules.rb | 42 ++++++++++++------- .../test_build_residential_schedule_file.rb | 13 +++--- 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/BuildResidentialScheduleFile/measure.rb b/BuildResidentialScheduleFile/measure.rb index bd2b12eae7..a91b16a55c 100644 --- a/BuildResidentialScheduleFile/measure.rb +++ b/BuildResidentialScheduleFile/measure.rb @@ -208,7 +208,7 @@ def create_schedules(runner, hpxml, hpxml_bldg, epw_file, weather, args) get_generator_inputs(hpxml_bldg, epw_file, args) args[:resources_path] = File.join(File.dirname(__FILE__), 'resources') - schedule_generator = ScheduleGenerator.new(runner: runner, epw_file: epw_file, **args) + schedule_generator = ScheduleGenerator.new(runner: runner, hpxml_bldg: hpxml_bldg, epw_file: epw_file, **args) success = schedule_generator.create(args: args, weather: weather) return false if not success diff --git a/BuildResidentialScheduleFile/resources/schedules.rb b/BuildResidentialScheduleFile/resources/schedules.rb index 6c4464b504..55e595e4d0 100644 --- a/BuildResidentialScheduleFile/resources/schedules.rb +++ b/BuildResidentialScheduleFile/resources/schedules.rb @@ -108,7 +108,7 @@ def create_stochastic_schedules(args:, weather:) # initialize a random number generator prng = Random.new(@random_seed) - + @num_occupants = args[:geometry_num_occupants].to_i # pre-load the probability distribution csv files for speed cluster_size_prob_map = read_activity_cluster_size_probs(resources_path: args[:resources_path]) event_duration_prob_map = read_event_duration_probs(resources_path: args[:resources_path]) @@ -1097,7 +1097,7 @@ def _get_ev_battery_schedule(away_schedule, hours_driven_per_year) if is_away == 1 current_chunk_proportion = (1.0 * activity_minutes) / total_away_minutes - expected_driving_time = (total_driving_minutes_per_year * current_chunk_proportion).round + expected_driving_time = (total_driving_minutes_per_year * current_chunk_proportion).ceil max_driving_time = [expected_driving_time, total_driving_minutes_per_year - driving_minutes_used].min max_possible_driving_time = (activity_minutes * 0.8).ceil @@ -1107,37 +1107,47 @@ def _get_ev_battery_schedule(away_schedule, hours_driven_per_year) first_half_driving = (actual_driving_time / 2.0).ceil second_half_driving = actual_driving_time - first_half_driving - discharging_schedule.concat([-1] * first_half_driving) # Start driving - discharging_schedule.concat([0] * idle_time) # Idle in the middle - discharging_schedule.concat([-1] * second_half_driving) # End driving - charging_schedule.concat([0] * activity_minutes) + discharging_schedule += [1] * first_half_driving # Start driving + discharging_schedule += [0] * idle_time # Idle in the middle + discharging_schedule += [1] * second_half_driving # End driving + charging_schedule += [0] * activity_minutes driving_minutes_used += actual_driving_time else - charging_schedule.concat([1] * activity_minutes) - discharging_schedule.concat([0] * activity_minutes) + charging_schedule += [1] * activity_minutes + discharging_schedule += [0] * activity_minutes end end if driving_minutes_used < total_driving_minutes_per_year - @runner.registerError("The occupant has less away hours than hours EV is driven") - raise "Insufficient away time for required driving hours" + msg = "Insufficient away minutes (#{total_away_minutes}) for required driving minutes (#{hours_driven_per_year * 60})" + msg += "Only #{driving_minutes_used} minutes was used." + @runner.registerError(msg) + raise msg end return charging_schedule, discharging_schedule end def fill_ev_battery_schedule(markov_chain_simulation_result, prng) + if @hpxml_bldg.vehicles.nil? + return + end + vehicle = @hpxml_bldg.vehicles[0] + + if vehicle.instance_variable_defined?(:@miles_per_year) + miles_per_year = vehicle.miles_per_year or 10000 + else + miles_per_year = 5000 + end + average_mph = 40 + hours_per_year = miles_per_year / average_mph + # randomly pick 1 occupant # TODO: determine the occupant based on best match for miles driven and occupant behavior occupant_number = prng.rand(@num_occupants) away_index = 5 # Index of away activity in the markov-chain simulator away_schedule = markov_chain_simulation_result[occupant_number].column(away_index) - if bldg_properties.nil? - hours_driven_per_year = 500 - else - hours_driven_per_year = bldg_properties['ev_effective_hours_per_year'] - end - charging_schedule, discharging_schedule = _get_ev_battery_schedule(away_schedule, hours_driven_per_year) + charging_schedule, discharging_schedule = _get_ev_battery_schedule(away_schedule, hours_per_year) agg_charging_schedule = aggregate_array(charging_schedule, @minutes_per_step).map { |val| val / 60.0 } agg_discharging_schedule = aggregate_array(discharging_schedule, @minutes_per_step).map { |val| val / 60.0 } @schedules[SchedulesFile::Columns[:EVBatteryCharging].name] = agg_charging_schedule diff --git a/BuildResidentialScheduleFile/tests/test_build_residential_schedule_file.rb b/BuildResidentialScheduleFile/tests/test_build_residential_schedule_file.rb index 51134118fc..e648f3c217 100644 --- a/BuildResidentialScheduleFile/tests/test_build_residential_schedule_file.rb +++ b/BuildResidentialScheduleFile/tests/test_build_residential_schedule_file.rb @@ -333,7 +333,7 @@ def test_zero_occupants end def test_ev_battery - num_occupants = 0.0 + num_occupants = 1.0 hpxml = _create_hpxml('base-battery-ev.xml') hpxml.buildings[0].building_occupancy.number_of_residents = num_occupants @@ -341,12 +341,11 @@ def test_ev_battery @args_hash['output_csv_path'] = File.absolute_path(File.join(@tmp_output_path, 'occupancy-stochastic.csv')) _hpxml, result = _test_measure() - - info_msgs = result.info.map { |x| x.logMessage } - assert(1, info_msgs.size) - assert(info_msgs.any? { |info_msg| info_msg.include?('Number of occupants set to zero; skipping generation of stochastic schedules.') }) - assert(!File.exist?(@args_hash['output_csv_path'])) - assert_empty(hpxml.buildings[0].header.schedules_filepaths) + sf = SchedulesFile.new(schedules_paths: _hpxml.buildings[0].header.schedules_filepaths, + year: @year, + output_path: @tmp_schedule_file_path) + assert_in_epsilon(6201, sf.annual_equivalent_full_load_hrs(col_name: SchedulesFile::Columns[:EVBatteryCharging].name, schedules: sf.tmp_schedules), @tol) + assert_in_epsilon(125, sf.annual_equivalent_full_load_hrs(col_name: SchedulesFile::Columns[:EVBatteryDischarging].name, schedules: sf.tmp_schedules), @tol) end def test_multiple_buildings From d44e1bcfea4381a220ebacfc6a2a1c9eed93e4a6 Mon Sep 17 00:00:00 2001 From: Rajendra Adhikari Date: Mon, 1 Jul 2024 17:54:22 -0500 Subject: [PATCH 03/13] Handle schedule conflict --- HPXMLtoOpenStudio/resources/schedules.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/HPXMLtoOpenStudio/resources/schedules.rb b/HPXMLtoOpenStudio/resources/schedules.rb index 5634b4addd..6bf0559926 100644 --- a/HPXMLtoOpenStudio/resources/schedules.rb +++ b/HPXMLtoOpenStudio/resources/schedules.rb @@ -1995,6 +1995,7 @@ def initialize(runner: nil, return if schedules_paths.empty? @year = year + @runner = runner import(schedules_paths) create_battery_charging_discharging_schedules expand_schedules @@ -2035,6 +2036,7 @@ def includes_col_name(col_name) def import(schedules_paths) num_hrs_in_year = Constants.NumHoursInYear(@year) @schedules = {} + col2path = {} schedules_paths.each do |schedules_path| columns = CSV.read(schedules_path).transpose columns.each do |col| @@ -2050,7 +2052,11 @@ def import(schedules_paths) end if @schedules.keys.include? col_name - fail "Schedule column name '#{col_name}' is duplicated. [context: #{schedules_path}]" + if col2path[col_name] == schedules_path + fail "Schedule column name '#{col_name}' is duplicated. [context: #{schedules_path}]" + else + @runner.registerWarning("Schedule column name '#{col_name}' already exist in #{col2path[col_name]}. Overwriting with #{schedules_path}.") + end end if column.type == :frac @@ -2085,8 +2091,8 @@ def import(schedules_paths) unless valid_num_rows.include? values.length fail "Schedule has invalid number of rows (#{values.length}) for column '#{col_name}'. Must be one of: #{valid_num_rows.reverse.join(', ')}. [context: #{@schedules_path}]" end - @schedules[col_name] = values + col2path[col_name] = schedules_path end end end From fd0dd492dee2d7b81d62a4fae6189a7a3c11f3c2 Mon Sep 17 00:00:00 2001 From: Rajendra Adhikari Date: Thu, 18 Jul 2024 15:56:43 -0500 Subject: [PATCH 04/13] Binary occupancy --- BuildResidentialScheduleFile/resources/schedules.rb | 13 +++++++++++-- HPXMLtoOpenStudio/resources/schedules.rb | 1 + 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/BuildResidentialScheduleFile/resources/schedules.rb b/BuildResidentialScheduleFile/resources/schedules.rb index 55e595e4d0..cc55661ff5 100644 --- a/BuildResidentialScheduleFile/resources/schedules.rb +++ b/BuildResidentialScheduleFile/resources/schedules.rb @@ -202,6 +202,8 @@ def create_stochastic_schedules(args:, sleep_schedule = [] away_schedule = [] idle_schedule = [] + away_occupants = [] # Binary representation of the presence of accupant. Each bit represents presence of one occupant + # fill in the yearly time_step resolution schedule for plug/lighting and ceiling fan based on weekday/weekend sch # States are: 0='sleeping', 1='shower', 2='laundry', 3='cooking', 4='dishwashing', 5='absent', 6='nothingAtHome' @@ -216,6 +218,7 @@ def create_stochastic_schedules(args:, sleep_schedule << sum_across_occupants(all_simulated_values, 0, index_15).to_f / args[:geometry_num_occupants] away_schedule << sum_across_occupants(all_simulated_values, 5, index_15).to_f / args[:geometry_num_occupants] idle_schedule << sum_across_occupants(all_simulated_values, 6, index_15).to_f / args[:geometry_num_occupants] + away_occupants << sum_across_occupants(all_simulated_values, 5, index_15, binary_sum: true) active_occupancy_percentage = 1 - (away_schedule[-1] + sleep_schedule[-1]) @schedules[SchedulesFile::Columns[:PlugLoadsOther].name][day * @steps_in_day + step] = get_value_from_daily_sch(plugload_other_weekday_sch, plugload_other_weekend_sch, plugload_other_monthly_multiplier, month, is_weekday, minute, active_occupancy_percentage) @schedules[SchedulesFile::Columns[:PlugLoadsTV].name][day * @steps_in_day + step] = get_value_from_daily_sch(plugload_tv_weekday_sch, plugload_tv_weekend_sch, plugload_tv_monthly_multiplier, month, is_weekday, minute, active_occupancy_percentage) @@ -613,6 +616,8 @@ def create_stochastic_schedules(args:, @schedules[SchedulesFile::Columns[:Dishwasher].name] = dw_power_sch.map { |power| power / dw_peak_power } @schedules[SchedulesFile::Columns[:Occupants].name] = away_schedule.map { |i| 1.0 - i } + max_num = (2**@num_occupants - 1).to_i + @schedules[SchedulesFile::Columns[:PresentOccupants].name] = away_occupants.map { |i| (i ^ (max_num)) } if @debug @schedules[SchedulesFile::Columns[:Sleeping].name] = sleep_schedule @@ -899,10 +904,14 @@ def gaussian_rand(prng, mean, std, min = nil, max = nil) # @param time_index [TODO] TODO # @param max_clip [TODO] TODO # @return [TODO] TODO - def sum_across_occupants(all_simulated_values, activity_index, time_index, max_clip: nil) + def sum_across_occupants(all_simulated_values, activity_index, time_index, max_clip: nil, binary_sum: false) sum = 0 + multiplier = 1 all_simulated_values.size.times do |i| - sum += all_simulated_values[i][time_index, activity_index] + sum += all_simulated_values[i][time_index, activity_index] * multiplier + if binary_sum + multiplier *= 2 + end end if (not max_clip.nil?) && (sum > max_clip) sum = max_clip diff --git a/HPXMLtoOpenStudio/resources/schedules.rb b/HPXMLtoOpenStudio/resources/schedules.rb index 6bf0559926..8c072141f1 100644 --- a/HPXMLtoOpenStudio/resources/schedules.rb +++ b/HPXMLtoOpenStudio/resources/schedules.rb @@ -1930,6 +1930,7 @@ def initialize(name, used_by_unavailable_periods, can_be_stochastic, type) # periods CSV (e.g., hvac), and/or C) EnergyPlus-specific schedules (e.g., battery_charging). Columns = { Occupants: Column.new('occupants', true, true, :frac), + PresentOccupants: Column.new('present_occupants', true, true, :int), LightingInterior: Column.new('lighting_interior', true, true, :frac), LightingExterior: Column.new('lighting_exterior', true, false, :frac), LightingGarage: Column.new('lighting_garage', true, true, :frac), From 5230cc0ad39df0e4794a1c1a55ae1ea7ccc988cd Mon Sep 17 00:00:00 2001 From: aspeake1 Date: Mon, 16 Sep 2024 11:01:16 -0600 Subject: [PATCH 05/13] fix bug from merge conflict --- BuildResidentialScheduleFile/resources/schedules.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/BuildResidentialScheduleFile/resources/schedules.rb b/BuildResidentialScheduleFile/resources/schedules.rb index b0eb987869..c70df51edf 100644 --- a/BuildResidentialScheduleFile/resources/schedules.rb +++ b/BuildResidentialScheduleFile/resources/schedules.rb @@ -1155,6 +1155,7 @@ def fill_ev_battery_schedule(markov_chain_simulation_result, prng) agg_discharging_schedule = aggregate_array(discharging_schedule, @minutes_per_step).map { |val| val / 60.0 } @schedules[SchedulesFile::Columns[:EVBatteryCharging].name] = agg_charging_schedule @schedules[SchedulesFile::Columns[:EVBatteryDischarging].name] = agg_discharging_schedule + end # Get the weekday/weekend schedule fractions for TV plug loads and monthly multipliers for interior lighting, dishwasher, clothes washer/dryer, cooking range, and other/TV plug loads. # From 3c867be6e4a3fbc671a8dabf85e01189c014dc1f Mon Sep 17 00:00:00 2001 From: Rajendra Adhikari Date: Thu, 19 Sep 2024 16:24:28 -0500 Subject: [PATCH 06/13] Remove epw arguments --- BuildResidentialScheduleFile/measure.rb | 2 +- BuildResidentialScheduleFile/resources/schedules.rb | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/BuildResidentialScheduleFile/measure.rb b/BuildResidentialScheduleFile/measure.rb index b40ed77d92..d1e9f211d4 100644 --- a/BuildResidentialScheduleFile/measure.rb +++ b/BuildResidentialScheduleFile/measure.rb @@ -205,7 +205,7 @@ def create_schedules(runner, hpxml, hpxml_bldg, weather, args) get_generator_inputs(hpxml_bldg, weather, args) args[:resources_path] = File.join(File.dirname(__FILE__), 'resources') - schedule_generator = ScheduleGenerator.new(runner: runner, hpxml_bldg: hpxml_bldg, epw_file: epw_file, **args) + schedule_generator = ScheduleGenerator.new(runner: runner, hpxml_bldg: hpxml_bldg, **args) success = schedule_generator.create(args: args, weather: weather) return false if not success diff --git a/BuildResidentialScheduleFile/resources/schedules.rb b/BuildResidentialScheduleFile/resources/schedules.rb index c70df51edf..e5a1795383 100644 --- a/BuildResidentialScheduleFile/resources/schedules.rb +++ b/BuildResidentialScheduleFile/resources/schedules.rb @@ -20,7 +20,6 @@ class ScheduleGenerator # @param append_output [Boolean] If true and the output CSV file already exists, appends columns to the file rather than overwriting it. The existing output CSV file must have the same number of rows (i.e., timeseries frequency) as the new columns being appended. def initialize(runner:, hpxml_bldg:, - epw_file:, state:, column_names: nil, random_seed: nil, @@ -36,7 +35,6 @@ def initialize(runner:, **) @runner = runner @hpxml_bldg = hpxml_bldg - @epw_file = epw_file @state = state @column_names = column_names @random_seed = random_seed From b102371faf29325d2967920f46c36dba8496c20e Mon Sep 17 00:00:00 2001 From: Rajendra Adhikari Date: Fri, 20 Sep 2024 23:06:52 -0500 Subject: [PATCH 07/13] Handle hours per week --- BuildResidentialScheduleFile/resources/schedules.rb | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/BuildResidentialScheduleFile/resources/schedules.rb b/BuildResidentialScheduleFile/resources/schedules.rb index e5a1795383..ac4793d931 100644 --- a/BuildResidentialScheduleFile/resources/schedules.rb +++ b/BuildResidentialScheduleFile/resources/schedules.rb @@ -1083,7 +1083,7 @@ def get_building_america_lighting_schedule(time_zone_utc_offset, latitude, longi end def _get_ev_battery_schedule(away_schedule, hours_driven_per_year) - total_driving_minutes_per_year = hours_driven_per_year * 60 + total_driving_minutes_per_year = (hours_driven_per_year * 60).ceil expanded_away_schedule = away_schedule.flat_map { |status| [status] * 15 } @@ -1135,13 +1135,8 @@ def fill_ev_battery_schedule(markov_chain_simulation_result, prng) end vehicle = @hpxml_bldg.vehicles[0] - if vehicle.instance_variable_defined?(:@miles_per_year) - miles_per_year = vehicle.miles_per_year or 10000 - else - miles_per_year = 5000 - end - average_mph = 40 - hours_per_year = miles_per_year / average_mph + hours_per_week = vehicle.hours_per_week + hours_per_year = hours_per_week * 52 # randomly pick 1 occupant # TODO: determine the occupant based on best match for miles driven and occupant behavior From d15baa1866321e9a58fa38b483040e602a785863 Mon Sep 17 00:00:00 2001 From: Rajendra Adhikari Date: Fri, 20 Sep 2024 23:49:36 -0500 Subject: [PATCH 08/13] Output EV occupant occupancy --- .../resources/schedules.rb | 23 ++++++++----------- HPXMLtoOpenStudio/resources/schedules.rb | 2 +- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/BuildResidentialScheduleFile/resources/schedules.rb b/BuildResidentialScheduleFile/resources/schedules.rb index ac4793d931..f3ed82328d 100644 --- a/BuildResidentialScheduleFile/resources/schedules.rb +++ b/BuildResidentialScheduleFile/resources/schedules.rb @@ -100,6 +100,9 @@ def create_stochastic_schedules(args:, # initialize a random number generator prng = Random.new(@random_seed) @num_occupants = args[:geometry_num_occupants].to_i + # randomly pick 1 occupant as the EV owner + # TODO: determine the occupant based on best match for miles driven and occupant behavior + @ev_occupant_number = prng.rand(@num_occupants) # pre-load the probability distribution csv files for speed cluster_size_prob_map = read_activity_cluster_size_probs(resources_path: args[:resources_path]) event_duration_prob_map = read_event_duration_probs(resources_path: args[:resources_path]) @@ -193,7 +196,7 @@ def create_stochastic_schedules(args:, sleep_schedule = [] away_schedule = [] idle_schedule = [] - away_occupants = [] # Binary representation of the presence of accupant. Each bit represents presence of one occupant + ev_occupant_away_schedule = [] # fill in the yearly time_step resolution schedule for plug/lighting and ceiling fan based on weekday/weekend sch @@ -208,8 +211,8 @@ def create_stochastic_schedules(args:, index_15 = (minute / 15).to_i sleep_schedule << sum_across_occupants(all_simulated_values, 0, index_15).to_f / args[:geometry_num_occupants] away_schedule << sum_across_occupants(all_simulated_values, 5, index_15).to_f / args[:geometry_num_occupants] + ev_occupant_away_schedule << all_simulated_values[@ev_occupant_number][index_15, 5] idle_schedule << sum_across_occupants(all_simulated_values, 6, index_15).to_f / args[:geometry_num_occupants] - away_occupants << sum_across_occupants(all_simulated_values, 5, index_15, binary_sum: true) active_occupancy_percentage = 1 - (away_schedule[-1] + sleep_schedule[-1]) @schedules[SchedulesFile::Columns[:PlugLoadsOther].name][day * @steps_in_day + step] = get_value_from_daily_sch(plugload_other_weekday_sch, plugload_other_weekend_sch, plugload_other_monthly_multiplier, month, is_weekday, minute, active_occupancy_percentage) @schedules[SchedulesFile::Columns[:PlugLoadsTV].name][day * @steps_in_day + step] = get_value_from_daily_sch(plugload_tv_weekday_sch, plugload_tv_weekend_sch, plugload_tv_monthly_multiplier, month, is_weekday, minute, active_occupancy_percentage) @@ -607,8 +610,7 @@ def create_stochastic_schedules(args:, @schedules[SchedulesFile::Columns[:Dishwasher].name] = dw_power_sch.map { |power| power / dw_peak_power } @schedules[SchedulesFile::Columns[:Occupants].name] = away_schedule.map { |i| 1.0 - i } - max_num = (2**@num_occupants - 1).to_i - @schedules[SchedulesFile::Columns[:PresentOccupants].name] = away_occupants.map { |i| (i ^ (max_num)) } + @schedules[SchedulesFile::Columns[:EVOccupant].name] = ev_occupant_away_schedule.map { |i| 1.0 - i } if @debug @schedules[SchedulesFile::Columns[:Sleeping].name] = sleep_schedule @@ -895,14 +897,10 @@ def gaussian_rand(prng, mean, std, min = 0.1, max = nil) # @param time_index [TODO] TODO # @param max_clip [TODO] TODO # @return [TODO] TODO - def sum_across_occupants(all_simulated_values, activity_index, time_index, max_clip: nil, binary_sum: false) + def sum_across_occupants(all_simulated_values, activity_index, time_index, max_clip: nil) sum = 0 - multiplier = 1 all_simulated_values.size.times do |i| - sum += all_simulated_values[i][time_index, activity_index] * multiplier - if binary_sum - multiplier *= 2 - end + sum += all_simulated_values[i][time_index, activity_index] end if (not max_clip.nil?) && (sum > max_clip) sum = max_clip @@ -1138,11 +1136,8 @@ def fill_ev_battery_schedule(markov_chain_simulation_result, prng) hours_per_week = vehicle.hours_per_week hours_per_year = hours_per_week * 52 - # randomly pick 1 occupant - # TODO: determine the occupant based on best match for miles driven and occupant behavior - occupant_number = prng.rand(@num_occupants) away_index = 5 # Index of away activity in the markov-chain simulator - away_schedule = markov_chain_simulation_result[occupant_number].column(away_index) + away_schedule = markov_chain_simulation_result[@ev_occupant_number].column(away_index) charging_schedule, discharging_schedule = _get_ev_battery_schedule(away_schedule, hours_per_year) agg_charging_schedule = aggregate_array(charging_schedule, @minutes_per_step).map { |val| val / 60.0 } agg_discharging_schedule = aggregate_array(discharging_schedule, @minutes_per_step).map { |val| val / 60.0 } diff --git a/HPXMLtoOpenStudio/resources/schedules.rb b/HPXMLtoOpenStudio/resources/schedules.rb index 6a4737f9ce..01ea7d97bb 100644 --- a/HPXMLtoOpenStudio/resources/schedules.rb +++ b/HPXMLtoOpenStudio/resources/schedules.rb @@ -1120,7 +1120,7 @@ def initialize(name, used_by_unavailable_periods, can_be_stochastic, type) # periods CSV (e.g., hvac), and/or C) EnergyPlus-specific schedules (e.g., battery_charging). Columns = { Occupants: Column.new('occupants', true, true, :frac), - PresentOccupants: Column.new('present_occupants', true, true, :int), + EVOccupant: Column.new('ev_occupant', true, true, :int), LightingInterior: Column.new('lighting_interior', true, true, :frac), LightingExterior: Column.new('lighting_exterior', true, false, :frac), LightingGarage: Column.new('lighting_garage', true, true, :frac), From b077dcc5043d0281ae80687aa83a8fccb881ddf2 Mon Sep 17 00:00:00 2001 From: Rajendra Adhikari Date: Wed, 23 Oct 2024 17:16:37 -0500 Subject: [PATCH 09/13] Bug fixes and enhancement --- .../resources/schedules.rb | 188 +++++++++++------- HPXMLtoOpenStudio/resources/schedules.rb | 1 + 2 files changed, 113 insertions(+), 76 deletions(-) diff --git a/BuildResidentialScheduleFile/resources/schedules.rb b/BuildResidentialScheduleFile/resources/schedules.rb index f3ed82328d..7ebd2e92aa 100644 --- a/BuildResidentialScheduleFile/resources/schedules.rb +++ b/BuildResidentialScheduleFile/resources/schedules.rb @@ -98,11 +98,8 @@ def create_stochastic_schedules(args:, schedules_csv_data = get_schedules_csv_data() # initialize a random number generator - prng = Random.new(@random_seed) + @prng = Random.new(@random_seed) @num_occupants = args[:geometry_num_occupants].to_i - # randomly pick 1 occupant as the EV owner - # TODO: determine the occupant based on best match for miles driven and occupant behavior - @ev_occupant_number = prng.rand(@num_occupants) # pre-load the probability distribution csv files for speed cluster_size_prob_map = read_activity_cluster_size_probs(resources_path: args[:resources_path]) event_duration_prob_map = read_event_duration_probs(resources_path: args[:resources_path]) @@ -117,7 +114,7 @@ def create_stochastic_schedules(args:, # shape of all_simulated_values is [2, 35040, 7] occupancy_types_probabilities = Schedule.validate_values(Constants::OccupancyTypesProbabilities, 4, 'occupancy types probabilities') for _n in 1..args[:geometry_num_occupants] - occ_type_id = weighted_random(prng, occupancy_types_probabilities) + occ_type_id = weighted_random(occupancy_types_probabilities) init_prob_file_weekday = args[:resources_path] + "/weekday/mkv_chain_initial_prob_cluster_#{occ_type_id}.csv" initial_prob_weekday = CSV.read(init_prob_file_weekday) initial_prob_weekday = initial_prob_weekday.map { |x| x[0].to_f } @@ -150,11 +147,11 @@ def create_stochastic_schedules(args:, j = 0 state_prob = initial_prob # [] shape = 1x7. probability of transitioning to each of the 7 states while j < @mkc_ts_per_day do - active_state = weighted_random(prng, state_prob) # Randomly pick the next state + active_state = weighted_random(state_prob) # Randomly pick the next state state_vector = [0] * 7 # there are 7 states state_vector[active_state] = 1 # Transition to the new state # sample the duration of the state, and skip markov-chain based state transition until the end of the duration - activity_duration = sample_activity_duration(prng, activity_duration_prob_map, occ_type_id, active_state, day_type, j / 4) + activity_duration = sample_activity_duration(activity_duration_prob_map, occ_type_id, active_state, day_type, j / 4) for _i in 1..activity_duration # repeat the same activity for the duration times simulated_values << state_vector @@ -196,8 +193,9 @@ def create_stochastic_schedules(args:, sleep_schedule = [] away_schedule = [] idle_schedule = [] - ev_occupant_away_schedule = [] - + ev_occupant_presence = [] + present_occupants = [] # Binary representation of the presence of accupant. Each bit represents presence of one occupant + @ev_occupant_number = get_ev_occupant_number(all_simulated_values) # fill in the yearly time_step resolution schedule for plug/lighting and ceiling fan based on weekday/weekend sch # States are: 0='sleeping', 1='shower', 2='laundry', 3='cooking', 4='dishwashing', 5='absent', 6='nothingAtHome' @@ -211,7 +209,8 @@ def create_stochastic_schedules(args:, index_15 = (minute / 15).to_i sleep_schedule << sum_across_occupants(all_simulated_values, 0, index_15).to_f / args[:geometry_num_occupants] away_schedule << sum_across_occupants(all_simulated_values, 5, index_15).to_f / args[:geometry_num_occupants] - ev_occupant_away_schedule << all_simulated_values[@ev_occupant_number][index_15, 5] + ev_occupant_presence << (1 - all_simulated_values[@ev_occupant_number][index_15, 5]) + present_occupants << get_present_occupants(all_simulated_values, index_15) idle_schedule << sum_across_occupants(all_simulated_values, 6, index_15).to_f / args[:geometry_num_occupants] active_occupancy_percentage = 1 - (away_schedule[-1] + sleep_schedule[-1]) @schedules[SchedulesFile::Columns[:PlugLoadsOther].name][day * @steps_in_day + step] = get_value_from_daily_sch(plugload_other_weekday_sch, plugload_other_weekend_sch, plugload_other_monthly_multiplier, month, is_weekday, minute, active_occupancy_percentage) @@ -265,22 +264,22 @@ def create_stochastic_schedules(args:, cluster_per_day = (total_clusters / @total_days_in_year).to_i sink_flow_rate_mean = Constants::SinkFlowRateMean sink_flow_rate_std = Constants::SinkFlowRateStd - sink_flow_rate = gaussian_rand(prng, sink_flow_rate_mean, sink_flow_rate_std) + sink_flow_rate = gaussian_rand(sink_flow_rate_mean, sink_flow_rate_std) @total_days_in_year.times do |day| for _n in 1..cluster_per_day todays_probable_steps = sink_activity_probable_mins[day * @mkc_ts_per_day..((day + 1) * @mkc_ts_per_day - 1)] todays_probablities = todays_probable_steps.map.with_index { |p, i| p * hourly_onset_prob[i / @mkc_ts_per_hour] } prob_sum = todays_probablities.sum(0) normalized_probabilities = todays_probablities.map { |p| p * 1 / prob_sum } - cluster_start_index = weighted_random(prng, normalized_probabilities) + cluster_start_index = weighted_random(normalized_probabilities) if sink_activity_probable_mins[cluster_start_index] != 0 sink_activity_probable_mins[cluster_start_index] = 0 # mark the 15-min interval as unavailable for another sink event end - num_events = weighted_random(prng, events_per_cluster_probs) + 1 + num_events = weighted_random(events_per_cluster_probs) + 1 start_min = cluster_start_index * 15 end_min = (cluster_start_index + 1) * 15 for _i in 1..num_events - duration = weighted_random(prng, sink_duration_probs) + 1 + duration = weighted_random(sink_duration_probs) + 1 if start_min + duration > end_min then duration = (end_min - start_min) end sink_activity_sch.fill(sink_flow_rate, (day * 1440) + start_min, duration) start_min += duration + sink_minutes_between_event_gap # Two minutes gap between sink activity @@ -318,8 +317,8 @@ def create_stochastic_schedules(args:, m = 0 shower_activity_sch = [0] * mins_in_year bath_activity_sch = [0] * mins_in_year - bath_flow_rate = gaussian_rand(prng, bath_flow_rate_mean, bath_flow_rate_std) - shower_flow_rate = gaussian_rand(prng, shower_flow_rate_mean, shower_flow_rate_std) + bath_flow_rate = gaussian_rand(bath_flow_rate_mean, bath_flow_rate_std) + shower_flow_rate = gaussian_rand(shower_flow_rate_mean, shower_flow_rate_std) # States are: 'sleeping','shower','laundry','cooking', 'dishwashing', 'absent', 'nothingAtHome' step = 0 while step < mkc_steps_in_a_year @@ -327,10 +326,10 @@ def create_stochastic_schedules(args:, shower_state = sum_across_occupants(all_simulated_values, 1, step) step_jump = 1 for _n in 1..shower_state.to_i - r = prng.rand + r = @prng.rand if r <= bath_ratio # fill in bath for this time - duration = gaussian_rand(prng, bath_duration_mean, bath_duration_std) + duration = gaussian_rand(bath_duration_mean, bath_duration_std) int_duration = duration.ceil # since we are rounding duration to integer minute, we compensate by scaling flow rate flow_rate = bath_flow_rate * duration / int_duration @@ -344,11 +343,11 @@ def create_stochastic_schedules(args:, step_jump = [step_jump, 1 + (m / 15)].max # jump additional step if the bath occupies multiple 15-min slots else # fill in the shower - num_events = sample_activity_cluster_size(prng, cluster_size_prob_map, 'shower') + num_events = sample_activity_cluster_size(cluster_size_prob_map, 'shower') start_min = step * 15 m = 0 num_events.times do - duration = sample_event_duration(prng, event_duration_prob_map, 'shower') + duration = sample_event_duration(event_duration_prob_map, 'shower') int_duration = duration.ceil flow_rate = shower_flow_rate * duration / int_duration # since we are rounding duration to integer minute, we compensate by scaling flow rate @@ -382,7 +381,7 @@ def create_stochastic_schedules(args:, dw_minutes_between_event_gap = Constants::HotWaterDishwasherMinutesBetweenEventGap dw_activity_sch = [0] * mins_in_year m = 0 - dw_flow_rate = gaussian_rand(prng, dw_flow_rate_mean, dw_flow_rate_std) + dw_flow_rate = gaussian_rand(dw_flow_rate_mean, dw_flow_rate_std) # States are: 'sleeping','shower','laundry','cooking', 'dishwashing', 'absent', 'nothingAtHome' # Fill in dw_water draw schedule @@ -391,11 +390,11 @@ def create_stochastic_schedules(args:, dish_state = sum_across_occupants(all_simulated_values, 4, step, max_clip: 1) step_jump = 1 if dish_state > 0 - cluster_size = sample_activity_cluster_size(prng, cluster_size_prob_map, 'hot_water_dishwasher') + cluster_size = sample_activity_cluster_size(cluster_size_prob_map, 'hot_water_dishwasher') start_minute = step * 15 m = 0 cluster_size.times do - duration = sample_event_duration(prng, event_duration_prob_map, 'hot_water_dishwasher') + duration = sample_event_duration(event_duration_prob_map, 'hot_water_dishwasher') int_duration = duration.ceil flow_rate = dw_flow_rate * duration / int_duration int_duration.times do @@ -422,7 +421,7 @@ def create_stochastic_schedules(args:, cw_activity_sch = [0] * mins_in_year # this is the clothes_washer water draw schedule cw_load_size_probability = Schedule.validate_values(Constants::HotWaterClothesWasherLoadSizeProbability, 4, 'hot_water_clothes_washer_load_size_probability') m = 0 - cw_flow_rate = gaussian_rand(prng, cw_flow_rate_mean, cw_flow_rate_std) + cw_flow_rate = gaussian_rand(cw_flow_rate_mean, cw_flow_rate_std) # States are: 'sleeping','shower','laundry','cooking', 'dishwashing', 'absent', 'nothingAtHome' step = 0 # Fill in clothes washer water draw schedule based on markov-chain state 2 (laundry) @@ -430,13 +429,13 @@ def create_stochastic_schedules(args:, clothes_state = sum_across_occupants(all_simulated_values, 2, step, max_clip: 1) step_jump = 1 if clothes_state > 0 - num_loads = weighted_random(prng, cw_load_size_probability) + 1 + num_loads = weighted_random(cw_load_size_probability) + 1 start_minute = step * 15 m = 0 num_loads.times do - cluster_size = sample_activity_cluster_size(prng, cluster_size_prob_map, 'hot_water_clothes_washer') + cluster_size = sample_activity_cluster_size(cluster_size_prob_map, 'hot_water_clothes_washer') cluster_size.times do - duration = sample_event_duration(prng, event_duration_prob_map, 'hot_water_clothes_washer') + duration = sample_event_duration(event_duration_prob_map, 'hot_water_clothes_washer') int_duration = duration.ceil flow_rate = cw_flow_rate * duration.to_f / int_duration int_duration.times do @@ -474,7 +473,7 @@ def create_stochastic_schedules(args:, dish_state = sum_across_occupants(all_simulated_values, 4, step, max_clip: 1) step_jump = 1 if (dish_state > 0) && (last_state == 0) # last_state == 0 prevents consecutive dishwasher power without gap - duration_15min, avg_power = sample_appliance_duration_power(prng, appliance_power_dist_map, 'dishwasher') + duration_15min, avg_power = sample_appliance_duration_power(appliance_power_dist_map, 'dishwasher') month = (start_time + step * 15 * 60).month duration_min = (duration_15min * 15 * hot_water_dishwasher_monthly_multiplier[month - 1]).to_i @@ -500,8 +499,8 @@ def create_stochastic_schedules(args:, clothes_state = sum_across_occupants(all_simulated_values, 2, step, max_clip: 1) step_jump = 1 if (clothes_state > 0) && (last_state == 0) # last_state == 0 prevents consecutive washer power without gap - cw_duration_15min, cw_avg_power = sample_appliance_duration_power(prng, appliance_power_dist_map, 'clothes_washer') - cd_duration_15min, cd_avg_power = sample_appliance_duration_power(prng, appliance_power_dist_map, 'clothes_dryer') + cw_duration_15min, cw_avg_power = sample_appliance_duration_power(appliance_power_dist_map, 'clothes_washer') + cd_duration_15min, cd_avg_power = sample_appliance_duration_power(appliance_power_dist_map, 'clothes_dryer') month = (start_time + step * 15 * 60).month cd_duration_min = (cd_duration_15min * 15 * clothes_dryer_monthly_multiplier[month - 1]).to_i @@ -529,7 +528,7 @@ def create_stochastic_schedules(args:, cooking_state = sum_across_occupants(all_simulated_values, 3, step, max_clip: 1) step_jump = 1 if (cooking_state > 0) && (last_state == 0) # last_state == 0 prevents consecutive cooking power without gap - duration_15min, avg_power = sample_appliance_duration_power(prng, appliance_power_dist_map, 'cooking') + duration_15min, avg_power = sample_appliance_duration_power(appliance_power_dist_map, 'cooking') month = (start_time + step * 15 * 60).month duration_min = (duration_15min * 15 * cooking_monthly_multiplier[month - 1]).to_i duration = [duration_min, mins_in_year - step * 15].min @@ -544,21 +543,21 @@ def create_stochastic_schedules(args:, # showers, sinks, baths - random_offset = (prng.rand * 2 * offset_range).to_i - offset_range + random_offset = (@prng.rand * 2 * offset_range).to_i - offset_range shower_activity_sch = shower_activity_sch.rotate(random_offset) shower_activity_sch = apply_monthly_offsets(array: shower_activity_sch, weekday_monthly_shift_dict: weekday_monthly_shift_dict, weekend_monthly_shift_dict: weekend_monthly_shift_dict) shower_activity_sch = aggregate_array(shower_activity_sch, @minutes_per_step) shower_peak_flow = shower_activity_sch.max showers = shower_activity_sch.map { |flow| flow / shower_peak_flow } - random_offset = (prng.rand * 2 * offset_range).to_i - offset_range + random_offset = (@prng.rand * 2 * offset_range).to_i - offset_range sink_activity_sch = sink_activity_sch.rotate(-4 * 60 + random_offset) # 4 am shifting sink_activity_sch = apply_monthly_offsets(array: sink_activity_sch, weekday_monthly_shift_dict: weekday_monthly_shift_dict, weekend_monthly_shift_dict: weekend_monthly_shift_dict) sink_activity_sch = aggregate_array(sink_activity_sch, @minutes_per_step) sink_peak_flow = sink_activity_sch.max sinks = sink_activity_sch.map { |flow| flow / sink_peak_flow } - random_offset = (prng.rand * 2 * offset_range).to_i - offset_range + random_offset = (@prng.rand * 2 * offset_range).to_i - offset_range bath_activity_sch = bath_activity_sch.rotate(random_offset) bath_activity_sch = apply_monthly_offsets(array: bath_activity_sch, weekday_monthly_shift_dict: weekday_monthly_shift_dict, weekend_monthly_shift_dict: weekend_monthly_shift_dict) bath_activity_sch = aggregate_array(bath_activity_sch, @minutes_per_step) @@ -567,42 +566,42 @@ def create_stochastic_schedules(args:, # hot water dishwasher/clothes washer/fixtures, cooking range, clothes washer/dryer, dishwasher, occupants - random_offset = (prng.rand * 2 * offset_range).to_i - offset_range + random_offset = (@prng.rand * 2 * offset_range).to_i - offset_range dw_activity_sch = dw_activity_sch.rotate(random_offset) dw_activity_sch = apply_monthly_offsets(array: dw_activity_sch, weekday_monthly_shift_dict: weekday_monthly_shift_dict, weekend_monthly_shift_dict: weekend_monthly_shift_dict) dw_activity_sch = aggregate_array(dw_activity_sch, @minutes_per_step) dw_peak_flow = dw_activity_sch.max @schedules[SchedulesFile::Columns[:HotWaterDishwasher].name] = dw_activity_sch.map { |flow| flow / dw_peak_flow } - random_offset = (prng.rand * 2 * offset_range).to_i - offset_range + random_offset = (@prng.rand * 2 * offset_range).to_i - offset_range cw_activity_sch = cw_activity_sch.rotate(random_offset) cw_activity_sch = apply_monthly_offsets(array: cw_activity_sch, weekday_monthly_shift_dict: weekday_monthly_shift_dict, weekend_monthly_shift_dict: weekend_monthly_shift_dict) cw_activity_sch = aggregate_array(cw_activity_sch, @minutes_per_step) cw_peak_flow = cw_activity_sch.max @schedules[SchedulesFile::Columns[:HotWaterClothesWasher].name] = cw_activity_sch.map { |flow| flow / cw_peak_flow } - random_offset = (prng.rand * 2 * offset_range).to_i - offset_range + random_offset = (@prng.rand * 2 * offset_range).to_i - offset_range cooking_power_sch = cooking_power_sch.rotate(random_offset) cooking_power_sch = apply_monthly_offsets(array: cooking_power_sch, weekday_monthly_shift_dict: weekday_monthly_shift_dict, weekend_monthly_shift_dict: weekend_monthly_shift_dict) cooking_power_sch = aggregate_array(cooking_power_sch, @minutes_per_step) cooking_peak_power = cooking_power_sch.max @schedules[SchedulesFile::Columns[:CookingRange].name] = cooking_power_sch.map { |power| power / cooking_peak_power } - random_offset = (prng.rand * 2 * offset_range).to_i - offset_range + random_offset = (@prng.rand * 2 * offset_range).to_i - offset_range cw_power_sch = cw_power_sch.rotate(random_offset) cw_power_sch = apply_monthly_offsets(array: cw_power_sch, weekday_monthly_shift_dict: weekday_monthly_shift_dict, weekend_monthly_shift_dict: weekend_monthly_shift_dict) cw_power_sch = aggregate_array(cw_power_sch, @minutes_per_step) cw_peak_power = cw_power_sch.max @schedules[SchedulesFile::Columns[:ClothesWasher].name] = cw_power_sch.map { |power| power / cw_peak_power } - random_offset = (prng.rand * 2 * offset_range).to_i - offset_range + random_offset = (@prng.rand * 2 * offset_range).to_i - offset_range cd_power_sch = cd_power_sch.rotate(random_offset) cd_power_sch = apply_monthly_offsets(array: cd_power_sch, weekday_monthly_shift_dict: weekday_monthly_shift_dict, weekend_monthly_shift_dict: weekend_monthly_shift_dict) cd_power_sch = aggregate_array(cd_power_sch, @minutes_per_step) cd_peak_power = cd_power_sch.max @schedules[SchedulesFile::Columns[:ClothesDryer].name] = cd_power_sch.map { |power| power / cd_peak_power } - random_offset = (prng.rand * 2 * offset_range).to_i - offset_range + random_offset = (@prng.rand * 2 * offset_range).to_i - offset_range dw_power_sch = dw_power_sch.rotate(random_offset) dw_power_sch = apply_monthly_offsets(array: dw_power_sch, weekday_monthly_shift_dict: weekday_monthly_shift_dict, weekend_monthly_shift_dict: weekend_monthly_shift_dict) dw_power_sch = aggregate_array(dw_power_sch, @minutes_per_step) @@ -610,7 +609,8 @@ def create_stochastic_schedules(args:, @schedules[SchedulesFile::Columns[:Dishwasher].name] = dw_power_sch.map { |power| power / dw_peak_power } @schedules[SchedulesFile::Columns[:Occupants].name] = away_schedule.map { |i| 1.0 - i } - @schedules[SchedulesFile::Columns[:EVOccupant].name] = ev_occupant_away_schedule.map { |i| 1.0 - i } + @schedules[SchedulesFile::Columns[:EVOccupant].name] = ev_occupant_presence + @schedules[SchedulesFile::Columns[:PresentOccupants].name] = present_occupants if @debug @schedules[SchedulesFile::Columns[:Sleeping].name] = sleep_schedule @@ -619,7 +619,7 @@ def create_stochastic_schedules(args:, @schedules[SchedulesFile::Columns[:HotWaterFixtures].name] = [showers, sinks, baths].transpose.map { |flow| flow.sum } fixtures_peak_flow = @schedules[SchedulesFile::Columns[:HotWaterFixtures].name].max @schedules[SchedulesFile::Columns[:HotWaterFixtures].name] = @schedules[SchedulesFile::Columns[:HotWaterFixtures].name].map { |flow| flow / fixtures_peak_flow } - fill_ev_battery_schedule(all_simulated_values, prng) + fill_ev_battery_schedule(all_simulated_values) return true end @@ -703,11 +703,10 @@ def read_appliance_power_dist(resources_path:) # TODO # - # @param prng [Random] Random number generator object # @param power_dist_map [TODO] TODO # @param appliance_name [TODO] TODO # @return [TODO] TODO - def sample_appliance_duration_power(prng, power_dist_map, appliance_name) + def sample_appliance_duration_power(power_dist_map, appliance_name) # returns number number of 15-min interval the appliance runs, and the average 15-min power duration_vals, consumption_vals = power_dist_map[appliance_name] if @consumption_row.nil? @@ -717,11 +716,11 @@ def sample_appliance_duration_power(prng, power_dist_map, appliance_name) @duration_row = {} end if !@consumption_row.has_key?(appliance_name) - @consumption_row[appliance_name] = (prng.rand * consumption_vals.size).to_i - @duration_row[appliance_name] = (prng.rand * duration_vals.size).to_i + @consumption_row[appliance_name] = (@prng.rand * consumption_vals.size).to_i + @duration_row[appliance_name] = (@prng.rand * duration_vals.size).to_i end power = consumption_vals[@consumption_row[appliance_name]] - sample = prng.rand(0..(duration_vals[@duration_row[appliance_name]].length - 1)) + sample = @prng.rand(0..(duration_vals[@duration_row[appliance_name]].length - 1)) duration = duration_vals[@duration_row[appliance_name]][sample] return [duration, power] end @@ -787,37 +786,34 @@ def read_activity_duration_prob(resources_path:) # TODO # - # @param prng [Random] Random number generator object # @param cluster_size_prob_map [TODO] TODO # @param activity_type_name [TODO] TODO # @return [TODO] TODO - def sample_activity_cluster_size(prng, cluster_size_prob_map, activity_type_name) + def sample_activity_cluster_size(cluster_size_prob_map, activity_type_name) cluster_size_probabilities = cluster_size_prob_map[activity_type_name] - return weighted_random(prng, cluster_size_probabilities) + 1 + return weighted_random(cluster_size_probabilities) + 1 end # TODO # - # @param prng [Random] Random number generator object # @param duration_probabilites_map [TODO] TODO # @param event_type [TODO] TODO # @return [TODO] TODO - def sample_event_duration(prng, duration_probabilites_map, event_type) + def sample_event_duration(duration_probabilites_map, event_type) durations = duration_probabilites_map[event_type][0] probabilities = duration_probabilites_map[event_type][1] - return durations[weighted_random(prng, probabilities)] + return durations[weighted_random(probabilities)] end # TODO # - # @param prng [Random] Random number generator object # @param activity_duration_prob_map [TODO] TODO # @param occ_type_id [TODO] TODO # @param activity [TODO] TODO # @param day_type [TODO] TODO # @param hour [TODO] TODO # @return [TODO] TODO - def sample_activity_duration(prng, activity_duration_prob_map, occ_type_id, activity, day_type, hour) + def sample_activity_duration(activity_duration_prob_map, occ_type_id, activity, day_type, hour) # States are: 'sleeping', 'shower', 'laundry', 'cooking', 'dishwashing', 'absent', 'nothingAtHome' if hour < 8 time_of_day = 'morning' @@ -840,7 +836,7 @@ def sample_activity_duration(prng, activity_duration_prob_map, occ_type_id, acti end durations = activity_duration_prob_map["#{occ_type_id}_#{activity_name}_#{day_type}_#{time_of_day}"][0] probabilities = activity_duration_prob_map["#{occ_type_id}_#{activity_name}_#{day_type}_#{time_of_day}"][1] - return durations[weighted_random(prng, probabilities)] + return durations[weighted_random(probabilities)] end # TODO @@ -873,15 +869,14 @@ def export(schedules_path:) # TODO # - # @param prng [Random] Random number generator object # @param mean [TODO] TODO # @param std [TODO] TODO # @param min [TODO] TODO # @param max [TODO] TODO # @return [TODO] TODO - def gaussian_rand(prng, mean, std, min = 0.1, max = nil) - t = 2 * Math::PI * prng.rand - r = Math.sqrt(-2 * Math.log(1 - prng.rand)) + def gaussian_rand(mean, std, min = 0.1, max = nil) + t = 2 * Math::PI * @prng.rand + r = Math.sqrt(-2 * Math.log(1 - @prng.rand)) scale = std * r x = mean + scale * Math.cos(t) if (not min.nil?) && (x < min) then x = min end @@ -908,6 +903,53 @@ def sum_across_occupants(all_simulated_values, activity_index, time_index, max_c return sum end + # TODO + # + # @param all_simulated_values [TODO] The Markov-chain activity array + # @param time_index [int] time index in the array + # @return [int] The integer whose binary representation indicates the presence of occupants. Bit 0 + # is presence of the first occupant, bit 1 is the presence of the second occupant, etc. + def get_present_occupants(all_simulated_values, time_index) + sum = 0 + multiplier = 1 + all_simulated_values.size.times do |i| + # Since all_simulated_values[i][time_index, 5]) is 1 when the occupant is away, we need to subtract it from 1 + sum += (1 - all_simulated_values[i][time_index, 5]) * multiplier + multiplier *= 2 + end + return sum + end + + + # Define get_ev_occupant_number function + # TODO + # + # @param all_simulated_values [TODO] TODO + def get_ev_occupant_number(all_simulated_values) + if @hpxml_bldg.vehicles.nil? + return 0 + end + vehicle = @hpxml_bldg.vehicles[0] + hours_per_year = (vehicle.hours_per_week / 7) * UnitConversions.convert(1, 'yr', 'day') + + occupant_away_hours_per_year = [] + all_simulated_values.size.times do |i| + occupant_away_hours_per_year[i] = all_simulated_values[i].column(5).sum() / 4 + end + # Only keep occupants whose 80% (the portion available for driving) away hours are sufficent to meet hours_per_year + elligible_occupant = occupant_away_hours_per_year.each_with_index.filter{|value, _| value * 0.8 > hours_per_year} + if elligible_occupant.empty? + # if nobody has enough away hours, find the index of the occupant with the highest away hours + _, ev_occupant = occupant_away_hours_per_year.each_with_index.max_by{|value, _| value} + return ev_occupant + else + # return the index of a random elligible occupant + _, ev_occupant = elligible_occupant.sample(random: @prng) + return ev_occupant + end + end + + # TODO # # @param arr [TODO] TODO @@ -949,11 +991,10 @@ def get_value_from_daily_sch(weekday_sch, weekend_sch, monthly_multiplier, month # TODO # - # @param prng [Random] Random number generator object # @param weights [TODO] TODO # @return [TODO] TODO - def weighted_random(prng, weights) - n = prng.rand + def weighted_random(weights) + n = @prng.rand cum_weights = 0 weights.each_with_index do |w, index| cum_weights += w @@ -1082,25 +1123,24 @@ def get_building_america_lighting_schedule(time_zone_utc_offset, latitude, longi def _get_ev_battery_schedule(away_schedule, hours_driven_per_year) total_driving_minutes_per_year = (hours_driven_per_year * 60).ceil - expanded_away_schedule = away_schedule.flat_map { |status| [status] * 15 } - charging_schedule = [] discharging_schedule = [] driving_minutes_used = 0 - + byebug chunk_counts = expanded_away_schedule.chunk(&:itself).map { |value, elements| [value, elements.size] } total_away_minutes = chunk_counts.map {|value, size| value * size}.sum - + extra_drive_minutes = 0 # accumulator for keeping track of extra driving minutes used due to ceil to upper integer chunk_counts.each do |is_away, activity_minutes| if is_away == 1 current_chunk_proportion = (1.0 * activity_minutes) / total_away_minutes - expected_driving_time = (total_driving_minutes_per_year * current_chunk_proportion).ceil + expected_driving_time = (total_driving_minutes_per_year * current_chunk_proportion - extra_drive_minutes).ceil max_driving_time = [expected_driving_time, total_driving_minutes_per_year - driving_minutes_used].min max_possible_driving_time = (activity_minutes * 0.8).ceil actual_driving_time = [max_driving_time, max_possible_driving_time].min + extra_drive_minutes += actual_driving_time - total_driving_minutes_per_year * current_chunk_proportion idle_time = activity_minutes - actual_driving_time first_half_driving = (actual_driving_time / 2.0).ceil @@ -1117,25 +1157,21 @@ def _get_ev_battery_schedule(away_schedule, hours_driven_per_year) discharging_schedule += [0] * activity_minutes end end - if driving_minutes_used < total_driving_minutes_per_year msg = "Insufficient away minutes (#{total_away_minutes}) for required driving minutes (#{hours_driven_per_year * 60})" msg += "Only #{driving_minutes_used} minutes was used." - @runner.registerError(msg) - raise msg + @runner.registerWarning(msg) end + return charging_schedule, discharging_schedule end - def fill_ev_battery_schedule(markov_chain_simulation_result, prng) + def fill_ev_battery_schedule(markov_chain_simulation_result) if @hpxml_bldg.vehicles.nil? return end vehicle = @hpxml_bldg.vehicles[0] - - hours_per_week = vehicle.hours_per_week - hours_per_year = hours_per_week * 52 - + hours_per_year = (vehicle.hours_per_week / 7) * UnitConversions.convert(1, 'yr', 'day') away_index = 5 # Index of away activity in the markov-chain simulator away_schedule = markov_chain_simulation_result[@ev_occupant_number].column(away_index) charging_schedule, discharging_schedule = _get_ev_battery_schedule(away_schedule, hours_per_year) diff --git a/HPXMLtoOpenStudio/resources/schedules.rb b/HPXMLtoOpenStudio/resources/schedules.rb index 01ea7d97bb..8c9ea9c6e7 100644 --- a/HPXMLtoOpenStudio/resources/schedules.rb +++ b/HPXMLtoOpenStudio/resources/schedules.rb @@ -1121,6 +1121,7 @@ def initialize(name, used_by_unavailable_periods, can_be_stochastic, type) Columns = { Occupants: Column.new('occupants', true, true, :frac), EVOccupant: Column.new('ev_occupant', true, true, :int), + PresentOccupants: Column.new('present_occupants', true, true, :int), LightingInterior: Column.new('lighting_interior', true, true, :frac), LightingExterior: Column.new('lighting_exterior', true, false, :frac), LightingGarage: Column.new('lighting_garage', true, true, :frac), From 8131ce2f33f46b1affee93c80c32e56c32e1f8e8 Mon Sep 17 00:00:00 2001 From: Rajendra Adhikari Date: Thu, 24 Oct 2024 07:32:05 -0500 Subject: [PATCH 10/13] Remove byebug line --- BuildResidentialScheduleFile/resources/schedules.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/BuildResidentialScheduleFile/resources/schedules.rb b/BuildResidentialScheduleFile/resources/schedules.rb index 7fff0f3f79..188575b202 100644 --- a/BuildResidentialScheduleFile/resources/schedules.rb +++ b/BuildResidentialScheduleFile/resources/schedules.rb @@ -1130,7 +1130,6 @@ def _get_ev_battery_schedule(away_schedule, hours_driven_per_year) charging_schedule = [] discharging_schedule = [] driving_minutes_used = 0 - byebug chunk_counts = expanded_away_schedule.chunk(&:itself).map { |value, elements| [value, elements.size] } total_away_minutes = chunk_counts.map {|value, size| value * size}.sum extra_drive_minutes = 0 # accumulator for keeping track of extra driving minutes used due to ceil to upper integer From bffc05be127bdfd354852c676d925584c7333ba4 Mon Sep 17 00:00:00 2001 From: Rajendra Adhikari Date: Tue, 12 Nov 2024 12:34:24 -0600 Subject: [PATCH 11/13] Schedule aggregation bug fix --- BuildResidentialScheduleFile/resources/schedules.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BuildResidentialScheduleFile/resources/schedules.rb b/BuildResidentialScheduleFile/resources/schedules.rb index 188575b202..c4d5ba2ccd 100644 --- a/BuildResidentialScheduleFile/resources/schedules.rb +++ b/BuildResidentialScheduleFile/resources/schedules.rb @@ -1177,8 +1177,8 @@ def fill_ev_battery_schedule(markov_chain_simulation_result) away_index = 5 # Index of away activity in the markov-chain simulator away_schedule = markov_chain_simulation_result[@ev_occupant_number].column(away_index) charging_schedule, discharging_schedule = _get_ev_battery_schedule(away_schedule, hours_per_year) - agg_charging_schedule = aggregate_array(charging_schedule, @minutes_per_step).map { |val| val / 60.0 } - agg_discharging_schedule = aggregate_array(discharging_schedule, @minutes_per_step).map { |val| val / 60.0 } + agg_charging_schedule = aggregate_array(charging_schedule, @minutes_per_step).map { |val| val / @minutes_per_step } + agg_discharging_schedule = aggregate_array(discharging_schedule, @minutes_per_step).map { |val| val / @minutes_per_step } @schedules[SchedulesFile::Columns[:EVBatteryCharging].name] = agg_charging_schedule @schedules[SchedulesFile::Columns[:EVBatteryDischarging].name] = agg_discharging_schedule end From 2204599f2bc745b5cf5d04ea4b5f8fcc63690e0d Mon Sep 17 00:00:00 2001 From: Rajendra Adhikari Date: Wed, 20 Nov 2024 15:47:39 -0600 Subject: [PATCH 12/13] Use floating division to prevent rounding to zero --- BuildResidentialScheduleFile/resources/schedules.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BuildResidentialScheduleFile/resources/schedules.rb b/BuildResidentialScheduleFile/resources/schedules.rb index c4d5ba2ccd..d105dde0b8 100644 --- a/BuildResidentialScheduleFile/resources/schedules.rb +++ b/BuildResidentialScheduleFile/resources/schedules.rb @@ -1177,8 +1177,8 @@ def fill_ev_battery_schedule(markov_chain_simulation_result) away_index = 5 # Index of away activity in the markov-chain simulator away_schedule = markov_chain_simulation_result[@ev_occupant_number].column(away_index) charging_schedule, discharging_schedule = _get_ev_battery_schedule(away_schedule, hours_per_year) - agg_charging_schedule = aggregate_array(charging_schedule, @minutes_per_step).map { |val| val / @minutes_per_step } - agg_discharging_schedule = aggregate_array(discharging_schedule, @minutes_per_step).map { |val| val / @minutes_per_step } + agg_charging_schedule = aggregate_array(charging_schedule, @minutes_per_step).map { |val| val.to_f / @minutes_per_step } + agg_discharging_schedule = aggregate_array(discharging_schedule, @minutes_per_step).map { |val| val.to_f / @minutes_per_step } @schedules[SchedulesFile::Columns[:EVBatteryCharging].name] = agg_charging_schedule @schedules[SchedulesFile::Columns[:EVBatteryDischarging].name] = agg_discharging_schedule end From 41ec1dbc1ed7852dc14f6e937294f9331c7c2475 Mon Sep 17 00:00:00 2001 From: Rajendra Adhikari Date: Wed, 20 Nov 2024 19:15:44 -0600 Subject: [PATCH 13/13] No EV bug fix and formatting --- .../resources/schedules.rb | 18 +++++++++--------- .../test_build_residential_schedule_file.rb | 4 ++-- HPXMLtoOpenStudio/resources/schedules.rb | 1 + 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/BuildResidentialScheduleFile/resources/schedules.rb b/BuildResidentialScheduleFile/resources/schedules.rb index d105dde0b8..b49cec3fdd 100644 --- a/BuildResidentialScheduleFile/resources/schedules.rb +++ b/BuildResidentialScheduleFile/resources/schedules.rb @@ -923,15 +923,15 @@ def get_present_occupants(all_simulated_values, time_index) return sum end - # Define get_ev_occupant_number function # TODO # # @param all_simulated_values [TODO] TODO def get_ev_occupant_number(all_simulated_values) - if @hpxml_bldg.vehicles.nil? + if @hpxml_bldg.vehicles.to_a.empty? return 0 end + vehicle = @hpxml_bldg.vehicles[0] hours_per_year = (vehicle.hours_per_week / 7) * UnitConversions.convert(1, 'yr', 'day') @@ -940,10 +940,10 @@ def get_ev_occupant_number(all_simulated_values) occupant_away_hours_per_year[i] = all_simulated_values[i].column(5).sum() / 4 end # Only keep occupants whose 80% (the portion available for driving) away hours are sufficent to meet hours_per_year - elligible_occupant = occupant_away_hours_per_year.each_with_index.filter{|value, _| value * 0.8 > hours_per_year} + elligible_occupant = occupant_away_hours_per_year.each_with_index.filter { |value, _| value * 0.8 > hours_per_year } if elligible_occupant.empty? # if nobody has enough away hours, find the index of the occupant with the highest away hours - _, ev_occupant = occupant_away_hours_per_year.each_with_index.max_by{|value, _| value} + _, ev_occupant = occupant_away_hours_per_year.each_with_index.max_by { |value, _| value } return ev_occupant else # return the index of a random elligible occupant @@ -952,7 +952,6 @@ def get_ev_occupant_number(all_simulated_values) end end - # TODO # # @param arr [TODO] TODO @@ -1131,8 +1130,8 @@ def _get_ev_battery_schedule(away_schedule, hours_driven_per_year) discharging_schedule = [] driving_minutes_used = 0 chunk_counts = expanded_away_schedule.chunk(&:itself).map { |value, elements| [value, elements.size] } - total_away_minutes = chunk_counts.map {|value, size| value * size}.sum - extra_drive_minutes = 0 # accumulator for keeping track of extra driving minutes used due to ceil to upper integer + total_away_minutes = chunk_counts.map { |value, size| value * size }.sum + extra_drive_minutes = 0 # accumulator for keeping track of extra driving minutes used due to ceil to upper integer chunk_counts.each do |is_away, activity_minutes| if is_away == 1 current_chunk_proportion = (1.0 * activity_minutes) / total_away_minutes @@ -1169,12 +1168,13 @@ def _get_ev_battery_schedule(away_schedule, hours_driven_per_year) end def fill_ev_battery_schedule(markov_chain_simulation_result) - if @hpxml_bldg.vehicles.nil? + if @hpxml_bldg.vehicles.to_a.empty? return end + vehicle = @hpxml_bldg.vehicles[0] hours_per_year = (vehicle.hours_per_week / 7) * UnitConversions.convert(1, 'yr', 'day') - away_index = 5 # Index of away activity in the markov-chain simulator + away_index = 5 # Index of away activity in the markov-chain simulator away_schedule = markov_chain_simulation_result[@ev_occupant_number].column(away_index) charging_schedule, discharging_schedule = _get_ev_battery_schedule(away_schedule, hours_per_year) agg_charging_schedule = aggregate_array(charging_schedule, @minutes_per_step).map { |val| val.to_f / @minutes_per_step } diff --git a/BuildResidentialScheduleFile/tests/test_build_residential_schedule_file.rb b/BuildResidentialScheduleFile/tests/test_build_residential_schedule_file.rb index 90d9039ad4..20f8987d02 100644 --- a/BuildResidentialScheduleFile/tests/test_build_residential_schedule_file.rb +++ b/BuildResidentialScheduleFile/tests/test_build_residential_schedule_file.rb @@ -340,8 +340,8 @@ def test_ev_battery XMLHelper.write_file(hpxml.to_doc, @tmp_hpxml_path) @args_hash['output_csv_path'] = File.absolute_path(File.join(@tmp_output_path, 'occupancy-stochastic.csv')) - _hpxml, result = _test_measure() - sf = SchedulesFile.new(schedules_paths: _hpxml.buildings[0].header.schedules_filepaths, + hpxml, _result = _test_measure() + sf = SchedulesFile.new(schedules_paths: hpxml.buildings[0].header.schedules_filepaths, year: @year, output_path: @tmp_schedule_file_path) assert_in_epsilon(6201, sf.annual_equivalent_full_load_hrs(col_name: SchedulesFile::Columns[:EVBatteryCharging].name, schedules: sf.tmp_schedules), @tol) diff --git a/HPXMLtoOpenStudio/resources/schedules.rb b/HPXMLtoOpenStudio/resources/schedules.rb index 8e4d0e73e8..a39b20b201 100644 --- a/HPXMLtoOpenStudio/resources/schedules.rb +++ b/HPXMLtoOpenStudio/resources/schedules.rb @@ -1181,6 +1181,7 @@ def import(schedules_paths) unless valid_num_rows.include? values.length fail "Schedule has invalid number of rows (#{values.length}) for column '#{col_name}'. Must be one of: #{valid_num_rows.reverse.join(', ')}. [context: #{@schedules_path}]" end + @schedules[col_name] = values col2path[col_name] = schedules_path end