Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Fix unavailable period less than 24 hours #1921

Merged
merged 4 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ __Bugfixes__
- Fixes zero occupants specified for one unit in a whole MF building from being treated like zero occupants for every unit.
- Fixes using detailed schedules with higher resolution (e.g., 10-min data) than the simulation timestep (e.g., 60-min).
- Fixes possible heating/cooling spikes when using maximum power ratio detailed schedule for variable-speed HVAC systems.
- Fixes unavailable periods for two consecutive, but partial, days.

## OpenStudio-HPXML v1.9.1

Expand Down
8 changes: 4 additions & 4 deletions HPXMLtoOpenStudio/measure.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
<schema_version>3.1</schema_version>
<name>hpxm_lto_openstudio</name>
<uid>b1543b30-9465-45ff-ba04-1d1f85e763bc</uid>
<version_id>bd5c3b3f-ccda-4dd5-b16a-1b4f7a4d69e4</version_id>
<version_modified>2025-02-03T19:22:20Z</version_modified>
<version_id>dcdd6d2f-de10-4569-b98b-358e8bbb5b6d</version_id>
<version_modified>2025-02-06T17:32:22Z</version_modified>
<xml_checksum>D8922A73</xml_checksum>
<class_name>HPXMLtoOpenStudio</class_name>
<display_name>HPXML to OpenStudio Translator</display_name>
Expand Down Expand Up @@ -597,7 +597,7 @@
<filename>schedules.rb</filename>
<filetype>rb</filetype>
<usage_type>resource</usage_type>
<checksum>467EB413</checksum>
<checksum>FD22C411</checksum>
</file>
<file>
<filename>simcontrols.rb</filename>
Expand Down Expand Up @@ -729,7 +729,7 @@
<filename>test_schedules.rb</filename>
<filetype>rb</filetype>
<usage_type>test</usage_type>
<checksum>BDA04315</checksum>
<checksum>61FCB2FA</checksum>
</file>
<file>
<filename>test_simcontrols.rb</filename>
Expand Down
108 changes: 37 additions & 71 deletions HPXMLtoOpenStudio/resources/schedules.rb
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,10 @@ def self.get_unavailable_periods(runner, schedule_name, unavailable_periods)
end

# Add unavailable period rules to the OpenStudio Schedule object.
# An unavailable period rule is an OpenStudio ScheduleRule object.
# Each OpenStudio ScheduleRule stores start month/day, end month/day, and day (24 hour) schedule.
# The unavailable period (i.e., number of consecutive days, whether starting/ending in the middle of the day, etc.) determines
# the number of ScheduleRule objects that are needed, as well as the start, end, and day schedule fields that are set.
#
# @param model [OpenStudio::Model::Model] OpenStudio Model object
# @param schedule [OpenStudio::Model::ScheduleRuleset] the OpenStudio Schedule object for which to set unavailable period rules
Expand Down Expand Up @@ -691,82 +695,44 @@ def self.set_unavailable_periods(model, schedule, sch_name, unavailable_periods)
begin_day_schedule = schedule.getDaySchedules(date_s, date_s)[0]
end_day_schedule = schedule.getDaySchedules(date_e, date_e)[0]

outage_days = day_e - day_s
if outage_days == 0 # outage is less than 1 calendar day (need 1 outage rule)
Model.add_schedule_ruleset_rule(
schedule,
start_date: date_s,
end_date: date_e,
hourly_values: (0..23).map { |h| (h < period.begin_hour) || (h >= period.end_hour) ? begin_day_schedule.getValue(OpenStudio::Time.new(0, h + 1, 0, 0)) : value }
)
else # outage is at least 1 calendar day
if period.begin_hour == 0 && period.end_hour == 24 # 1 outage rule
Model.add_schedule_ruleset_rule(
schedule,
start_date: date_s,
end_date: date_e,
hourly_values: [value] * 24
)
elsif (period.begin_hour == 0 && period.end_hour != 24) || (period.begin_hour != 0 && period.end_hour == 24) # 2 outage rules
# [[start_date, end_date, hourly_values], ...]
schedule_ruleset_rules = []

unavail_days = day_e - day_s
if unavail_days == 0 # unavailable period is less than 1 calendar day (need 1 unavailable period rule)
schedule_ruleset_rules << [date_s, date_e, (0..23).map { |h| (h < period.begin_hour) || (h >= period.end_hour) ? begin_day_schedule.getValue(OpenStudio::Time.new(0, h + 1, 0, 0)) : value }]
else # unavailable period is at least 1 calendar day
if period.begin_hour == 0 && period.end_hour == 24 # 1 unavailable period rule
schedule_ruleset_rules << [date_s, date_e, [value] * 24]
elsif (period.begin_hour == 0 && period.end_hour != 24) || (period.begin_hour != 0 && period.end_hour == 24) # 2 unavailable period rules
if period.begin_hour == 0 && period.end_hour != 24
# last day
Model.add_schedule_ruleset_rule(
schedule,
start_date: date_e,
end_date: date_e,
hourly_values: (0..23).map { |h| (h >= period.end_hour) ? end_day_schedule.getValue(OpenStudio::Time.new(0, h + 1, 0, 0)) : value }
)

# all other days
Model.add_schedule_ruleset_rule(
schedule,
start_date: date_s,
end_date: OpenStudio::Date::fromDayOfYear(day_e - 1, year),
hourly_values: [value] * 24
)
schedule_ruleset_rules << [date_e, date_e, (0..23).map { |h| (h >= period.end_hour) ? end_day_schedule.getValue(OpenStudio::Time.new(0, h + 1, 0, 0)) : value }] # last day
schedule_ruleset_rules << [date_s, OpenStudio::Date::fromDayOfYear(day_e - 1, year), [value] * 24] # all other days
elsif period.begin_hour != 0 && period.end_hour == 24
# first day
Model.add_schedule_ruleset_rule(
schedule,
start_date: date_s,
end_date: date_s,
hourly_values: (0..23).map { |h| (h < period.begin_hour) ? begin_day_schedule.getValue(OpenStudio::Time.new(0, h + 1, 0, 0)) : value }
)

# all other days
Model.add_schedule_ruleset_rule(
schedule,
start_date: OpenStudio::Date::fromDayOfYear(day_s + 1, year),
end_date: date_e,
hourly_values: [value] * 24
)
schedule_ruleset_rules << [date_s, date_s, (0..23).map { |h| (h < period.begin_hour) ? begin_day_schedule.getValue(OpenStudio::Time.new(0, h + 1, 0, 0)) : value }] # first day
schedule_ruleset_rules << [OpenStudio::Date::fromDayOfYear(day_s + 1, year), date_e, [value] * 24]
end
else # 2 or 3 unavailable period rules
if unavail_days == 1 # 2 unavailable period rules
schedule_ruleset_rules << [date_s, date_s, (0..23).map { |h| (h < period.begin_hour) ? begin_day_schedule.getValue(OpenStudio::Time.new(0, h + 1, 0, 0)) : value }] # first day
schedule_ruleset_rules << [date_e, date_e, (0..23).map { |h| (h >= period.end_hour) ? end_day_schedule.getValue(OpenStudio::Time.new(0, h + 1, 0, 0)) : value }] # last day
else # 3 unavailable period rules
schedule_ruleset_rules << [date_s, date_s, (0..23).map { |h| (h < period.begin_hour) ? begin_day_schedule.getValue(OpenStudio::Time.new(0, h + 1, 0, 0)) : value }] # first day
schedule_ruleset_rules << [OpenStudio::Date::fromDayOfYear(day_s + 1, year), OpenStudio::Date::fromDayOfYear(day_e - 1, year), [value] * 24] # all other days
schedule_ruleset_rules << [date_e, date_e, (0..23).map { |h| (h >= period.end_hour) ? end_day_schedule.getValue(OpenStudio::Time.new(0, h + 1, 0, 0)) : value }] # last day
end
else # 3 outage rules
# first day
Model.add_schedule_ruleset_rule(
schedule,
start_date: date_s,
end_date: date_s,
hourly_values: (0..23).map { |h| (h < period.begin_hour) ? begin_day_schedule.getValue(OpenStudio::Time.new(0, h + 1, 0, 0)) : value }
)

# all other days
Model.add_schedule_ruleset_rule(
schedule,
start_date: OpenStudio::Date::fromDayOfYear(day_s + 1, year),
end_date: OpenStudio::Date::fromDayOfYear(day_e - 1, year),
hourly_values: [value] * 24
)

# last day
Model.add_schedule_ruleset_rule(
schedule,
start_date: date_e,
end_date: date_e,
hourly_values: (0..23).map { |h| (h >= period.end_hour) ? end_day_schedule.getValue(OpenStudio::Time.new(0, h + 1, 0, 0)) : value }
)
end
end

schedule_ruleset_rules.each do |schedule_ruleset_rule|
start_date, end_date, hourly_values = schedule_ruleset_rule
Model.add_schedule_ruleset_rule(
schedule,
start_date: start_date,
end_date: end_date,
hourly_values: hourly_values
)
end
end
end

Expand Down
57 changes: 57 additions & 0 deletions HPXMLtoOpenStudio/tests/test_schedules.rb
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,38 @@ def test_simple_power_outage_schedules
assert_in_epsilon(8760 * get_available_hrs_ratio(unavailable_month_hrs), get_annual_equivalent_full_load_hrs(model, Constants::ObjectTypeMechanicalVentilationHouseFan + ' schedule'), @tol)
end

def test_simple_power_outage_schedules_overnight
args_hash = {}
hpxml_path = File.absolute_path(File.join(sample_files_dir, 'base-schedules-simple-power-outage.xml'))
hpxml = HPXML.new(hpxml_path: hpxml_path)
hpxml.header.unavailable_periods[0].begin_month = 12
hpxml.header.unavailable_periods[0].begin_day = 14
hpxml.header.unavailable_periods[0].begin_hour = 20
hpxml.header.unavailable_periods[0].end_month = 12
hpxml.header.unavailable_periods[0].end_day = 15
hpxml.header.unavailable_periods[0].end_hour = 6
XMLHelper.write_file(hpxml.to_doc(), @tmp_hpxml_path)
args_hash['hpxml_path'] = @tmp_hpxml_path
model, _hpxml, _hpxml_bldg = _test_measure(args_hash)

unavailable_month_hrs = { 11 => 10.0 }

assert_in_epsilon(6020, get_annual_equivalent_full_load_hrs(model, Constants::ObjectTypeOccupants + ' schedule'), @tol)
assert_in_epsilon(3049 * get_available_hrs_ratio(unavailable_month_hrs, @default_schedules_csv_data[SchedulesFile::Columns[:LightingInterior].name]['InteriorMonthlyScheduleMultipliers']), get_annual_equivalent_full_load_hrs(model, Constants::ObjectTypeLightingInterior + ' schedule'), @tol)
assert_in_epsilon(2895 * get_available_hrs_ratio(unavailable_month_hrs, @default_schedules_csv_data[SchedulesFile::Columns[:LightingInterior].name]['InteriorMonthlyScheduleMultipliers']), get_annual_equivalent_full_load_hrs(model, Constants::ObjectTypeLightingExterior + ' schedule'), @tol)
assert_in_epsilon(6673 * get_available_hrs_ratio(unavailable_month_hrs, @default_schedules_csv_data[SchedulesFile::Columns[:Refrigerator].name]['MonthlyScheduleMultipliers']), get_annual_equivalent_full_load_hrs(model, Constants::ObjectTypeRefrigerator + ' schedule'), @tol)
assert_in_epsilon(2441 * get_available_hrs_ratio(unavailable_month_hrs, @default_schedules_csv_data[SchedulesFile::Columns[:CookingRange].name]['MonthlyScheduleMultipliers']), get_annual_equivalent_full_load_hrs(model, Constants::ObjectTypeCookingRange + ' schedule'), @tol)
assert_in_epsilon(3285 * get_available_hrs_ratio(unavailable_month_hrs, @default_schedules_csv_data[SchedulesFile::Columns[:Dishwasher].name]['MonthlyScheduleMultipliers']), get_annual_equivalent_full_load_hrs(model, Constants::ObjectTypeDishwasher + ' schedule'), @tol)
assert_in_epsilon(4248 * get_available_hrs_ratio(unavailable_month_hrs, @default_schedules_csv_data[SchedulesFile::Columns[:ClothesWasher].name]['MonthlyScheduleMultipliers']), get_annual_equivalent_full_load_hrs(model, Constants::ObjectTypeClothesWasher + ' schedule'), @tol)
assert_in_epsilon(4502 * get_available_hrs_ratio(unavailable_month_hrs, @default_schedules_csv_data[SchedulesFile::Columns[:ClothesDryer].name]['MonthlyScheduleMultipliers']), get_annual_equivalent_full_load_hrs(model, Constants::ObjectTypeClothesDryer + ' schedule'), @tol)
assert_in_epsilon(6880 * get_available_hrs_ratio(unavailable_month_hrs, @default_schedules_csv_data[SchedulesFile::Columns[:PlugLoadsOther].name]['MonthlyScheduleMultipliers']), get_annual_equivalent_full_load_hrs(model, Constants::ObjectTypeMiscPlugLoads + ' schedule'), @tol)
assert_in_epsilon(3373 * get_available_hrs_ratio(unavailable_month_hrs, @default_schedules_csv_data[SchedulesFile::Columns[:PlugLoadsTV].name]['MonthlyScheduleMultipliers']), get_annual_equivalent_full_load_hrs(model, Constants::ObjectTypeMiscTelevision + ' schedule'), @tol)
assert_in_epsilon(4204 * get_available_hrs_ratio(unavailable_month_hrs, @default_schedules_csv_data[SchedulesFile::Columns[:HotWaterFixtures].name]['WaterFixturesMonthlyScheduleMultipliers']), get_annual_equivalent_full_load_hrs(model, Constants::ObjectTypeFixtures + ' schedule'), @tol)
assert_in_epsilon(4244 * get_available_hrs_ratio(unavailable_month_hrs, @default_schedules_csv_data[SchedulesFile::Columns[:HotWaterRecirculationPump].name]['RecirculationPumpMonthlyScheduleMultipliers']), get_annual_equivalent_full_load_hrs(model, Constants::ObjectTypeHotWaterRecircPump + ' schedule'), @tol)
assert_in_epsilon(5000, get_annual_equivalent_full_load_hrs(model, Constants::ObjectTypeGeneralWaterUse + ' schedule'), @tol)
assert_in_epsilon(8760 * get_available_hrs_ratio(unavailable_month_hrs), get_annual_equivalent_full_load_hrs(model, Constants::ObjectTypeMechanicalVentilationHouseFan + ' schedule'), @tol)
end

def test_stochastic_schedules
args_hash = {}
args_hash['hpxml_path'] = File.absolute_path(File.join(sample_files_dir, 'base-schedules-detailed-occupancy-stochastic.xml'))
Expand Down Expand Up @@ -580,6 +612,31 @@ def test_set_unavailable_periods_lighting
_test_day_schedule(schedule, end_month, end_day, year, 0, end_hour)
_test_day_schedule(schedule, end_month, end_day + 1, year, nil, nil)

# 2 calendar days, partial first and last day
begin_month = 12
begin_day = 14
begin_hour = 20
end_month = 12
end_day = 15
end_hour = 6

model, hpxml, _hpxml_bldg = _test_measure(args_hash)
year = model.getYearDescription.assumedYear

schedule = model.getScheduleRulesets.find { |schedule| schedule.name.to_s == sch_name }
unavailable_periods = _add_unavailable_period(hpxml, 'Power Outage', begin_month, begin_day, begin_hour, end_month, end_day, end_hour) # note the change of end month/day

schedule_rules = schedule.scheduleRules
Schedule.set_unavailable_periods(model, schedule, sch_name, unavailable_periods)
unavailable_schedule_rules = schedule.scheduleRules - schedule_rules

assert_equal(2, unavailable_schedule_rules.size)

_test_day_schedule(schedule, begin_month, begin_day - 1, year, nil, nil)
_test_day_schedule(schedule, begin_month, begin_day, year, begin_hour, 24)
_test_day_schedule(schedule, end_month, end_day, year, 0, end_hour)
_test_day_schedule(schedule, end_month, end_day + 1, year, nil, nil)

# wrap around
begin_month = 12
begin_day = 1
Expand Down