Skip to content

Commit fe64eb4

Browse files
authored
Merge pull request #1921 from NREL/fix-2-day-partial-unavailable-period
Fix unavailable period less than 24 hours
2 parents e033ebd + 68ffb9f commit fe64eb4

File tree

4 files changed

+99
-75
lines changed

4 files changed

+99
-75
lines changed

Diff for: Changelog.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ __Bugfixes__
77
- Fixes zero occupants specified for one unit in a whole MF building from being treated like zero occupants for every unit.
88
- Fixes using detailed schedules with higher resolution (e.g., 10-min data) than the simulation timestep (e.g., 60-min).
99
- Fixes possible heating/cooling spikes when using maximum power ratio detailed schedule for variable-speed HVAC systems.
10+
- Fixes unavailable periods for two consecutive, but partial, days.
1011

1112
## OpenStudio-HPXML v1.9.1
1213

Diff for: HPXMLtoOpenStudio/measure.xml

+4-4
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
<schema_version>3.1</schema_version>
44
<name>hpxm_lto_openstudio</name>
55
<uid>b1543b30-9465-45ff-ba04-1d1f85e763bc</uid>
6-
<version_id>bd5c3b3f-ccda-4dd5-b16a-1b4f7a4d69e4</version_id>
7-
<version_modified>2025-02-03T19:22:20Z</version_modified>
6+
<version_id>dcdd6d2f-de10-4569-b98b-358e8bbb5b6d</version_id>
7+
<version_modified>2025-02-06T17:32:22Z</version_modified>
88
<xml_checksum>D8922A73</xml_checksum>
99
<class_name>HPXMLtoOpenStudio</class_name>
1010
<display_name>HPXML to OpenStudio Translator</display_name>
@@ -597,7 +597,7 @@
597597
<filename>schedules.rb</filename>
598598
<filetype>rb</filetype>
599599
<usage_type>resource</usage_type>
600-
<checksum>467EB413</checksum>
600+
<checksum>FD22C411</checksum>
601601
</file>
602602
<file>
603603
<filename>simcontrols.rb</filename>
@@ -729,7 +729,7 @@
729729
<filename>test_schedules.rb</filename>
730730
<filetype>rb</filetype>
731731
<usage_type>test</usage_type>
732-
<checksum>BDA04315</checksum>
732+
<checksum>61FCB2FA</checksum>
733733
</file>
734734
<file>
735735
<filename>test_simcontrols.rb</filename>

Diff for: HPXMLtoOpenStudio/resources/schedules.rb

+37-71
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,10 @@ def self.get_unavailable_periods(runner, schedule_name, unavailable_periods)
649649
end
650650

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

694-
outage_days = day_e - day_s
695-
if outage_days == 0 # outage is less than 1 calendar day (need 1 outage rule)
696-
Model.add_schedule_ruleset_rule(
697-
schedule,
698-
start_date: date_s,
699-
end_date: date_e,
700-
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 }
701-
)
702-
else # outage is at least 1 calendar day
703-
if period.begin_hour == 0 && period.end_hour == 24 # 1 outage rule
704-
Model.add_schedule_ruleset_rule(
705-
schedule,
706-
start_date: date_s,
707-
end_date: date_e,
708-
hourly_values: [value] * 24
709-
)
710-
elsif (period.begin_hour == 0 && period.end_hour != 24) || (period.begin_hour != 0 && period.end_hour == 24) # 2 outage rules
698+
# [[start_date, end_date, hourly_values], ...]
699+
schedule_ruleset_rules = []
700+
701+
unavail_days = day_e - day_s
702+
if unavail_days == 0 # unavailable period is less than 1 calendar day (need 1 unavailable period rule)
703+
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 }]
704+
else # unavailable period is at least 1 calendar day
705+
if period.begin_hour == 0 && period.end_hour == 24 # 1 unavailable period rule
706+
schedule_ruleset_rules << [date_s, date_e, [value] * 24]
707+
elsif (period.begin_hour == 0 && period.end_hour != 24) || (period.begin_hour != 0 && period.end_hour == 24) # 2 unavailable period rules
711708
if period.begin_hour == 0 && period.end_hour != 24
712-
# last day
713-
Model.add_schedule_ruleset_rule(
714-
schedule,
715-
start_date: date_e,
716-
end_date: date_e,
717-
hourly_values: (0..23).map { |h| (h >= period.end_hour) ? end_day_schedule.getValue(OpenStudio::Time.new(0, h + 1, 0, 0)) : value }
718-
)
719-
720-
# all other days
721-
Model.add_schedule_ruleset_rule(
722-
schedule,
723-
start_date: date_s,
724-
end_date: OpenStudio::Date::fromDayOfYear(day_e - 1, year),
725-
hourly_values: [value] * 24
726-
)
709+
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
710+
schedule_ruleset_rules << [date_s, OpenStudio::Date::fromDayOfYear(day_e - 1, year), [value] * 24] # all other days
727711
elsif period.begin_hour != 0 && period.end_hour == 24
728-
# first day
729-
Model.add_schedule_ruleset_rule(
730-
schedule,
731-
start_date: date_s,
732-
end_date: date_s,
733-
hourly_values: (0..23).map { |h| (h < period.begin_hour) ? begin_day_schedule.getValue(OpenStudio::Time.new(0, h + 1, 0, 0)) : value }
734-
)
735-
736-
# all other days
737-
Model.add_schedule_ruleset_rule(
738-
schedule,
739-
start_date: OpenStudio::Date::fromDayOfYear(day_s + 1, year),
740-
end_date: date_e,
741-
hourly_values: [value] * 24
742-
)
712+
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
713+
schedule_ruleset_rules << [OpenStudio::Date::fromDayOfYear(day_s + 1, year), date_e, [value] * 24]
714+
end
715+
else # 2 or 3 unavailable period rules
716+
if unavail_days == 1 # 2 unavailable period rules
717+
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
718+
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
719+
else # 3 unavailable period rules
720+
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
721+
schedule_ruleset_rules << [OpenStudio::Date::fromDayOfYear(day_s + 1, year), OpenStudio::Date::fromDayOfYear(day_e - 1, year), [value] * 24] # all other days
722+
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
743723
end
744-
else # 3 outage rules
745-
# first day
746-
Model.add_schedule_ruleset_rule(
747-
schedule,
748-
start_date: date_s,
749-
end_date: date_s,
750-
hourly_values: (0..23).map { |h| (h < period.begin_hour) ? begin_day_schedule.getValue(OpenStudio::Time.new(0, h + 1, 0, 0)) : value }
751-
)
752-
753-
# all other days
754-
Model.add_schedule_ruleset_rule(
755-
schedule,
756-
start_date: OpenStudio::Date::fromDayOfYear(day_s + 1, year),
757-
end_date: OpenStudio::Date::fromDayOfYear(day_e - 1, year),
758-
hourly_values: [value] * 24
759-
)
760-
761-
# last day
762-
Model.add_schedule_ruleset_rule(
763-
schedule,
764-
start_date: date_e,
765-
end_date: date_e,
766-
hourly_values: (0..23).map { |h| (h >= period.end_hour) ? end_day_schedule.getValue(OpenStudio::Time.new(0, h + 1, 0, 0)) : value }
767-
)
768724
end
769725
end
726+
727+
schedule_ruleset_rules.each do |schedule_ruleset_rule|
728+
start_date, end_date, hourly_values = schedule_ruleset_rule
729+
Model.add_schedule_ruleset_rule(
730+
schedule,
731+
start_date: start_date,
732+
end_date: end_date,
733+
hourly_values: hourly_values
734+
)
735+
end
770736
end
771737
end
772738

Diff for: HPXMLtoOpenStudio/tests/test_schedules.rb

+57
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,38 @@ def test_simple_power_outage_schedules
187187
assert_in_epsilon(8760 * get_available_hrs_ratio(unavailable_month_hrs), get_annual_equivalent_full_load_hrs(model, Constants::ObjectTypeMechanicalVentilationHouseFan + ' schedule'), @tol)
188188
end
189189

190+
def test_simple_power_outage_schedules_overnight
191+
args_hash = {}
192+
hpxml_path = File.absolute_path(File.join(sample_files_dir, 'base-schedules-simple-power-outage.xml'))
193+
hpxml = HPXML.new(hpxml_path: hpxml_path)
194+
hpxml.header.unavailable_periods[0].begin_month = 12
195+
hpxml.header.unavailable_periods[0].begin_day = 14
196+
hpxml.header.unavailable_periods[0].begin_hour = 20
197+
hpxml.header.unavailable_periods[0].end_month = 12
198+
hpxml.header.unavailable_periods[0].end_day = 15
199+
hpxml.header.unavailable_periods[0].end_hour = 6
200+
XMLHelper.write_file(hpxml.to_doc(), @tmp_hpxml_path)
201+
args_hash['hpxml_path'] = @tmp_hpxml_path
202+
model, _hpxml, _hpxml_bldg = _test_measure(args_hash)
203+
204+
unavailable_month_hrs = { 11 => 10.0 }
205+
206+
assert_in_epsilon(6020, get_annual_equivalent_full_load_hrs(model, Constants::ObjectTypeOccupants + ' schedule'), @tol)
207+
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)
208+
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)
209+
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)
210+
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)
211+
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)
212+
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)
213+
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)
214+
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)
215+
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)
216+
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)
217+
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)
218+
assert_in_epsilon(5000, get_annual_equivalent_full_load_hrs(model, Constants::ObjectTypeGeneralWaterUse + ' schedule'), @tol)
219+
assert_in_epsilon(8760 * get_available_hrs_ratio(unavailable_month_hrs), get_annual_equivalent_full_load_hrs(model, Constants::ObjectTypeMechanicalVentilationHouseFan + ' schedule'), @tol)
220+
end
221+
190222
def test_stochastic_schedules
191223
args_hash = {}
192224
args_hash['hpxml_path'] = File.absolute_path(File.join(sample_files_dir, 'base-schedules-detailed-occupancy-stochastic.xml'))
@@ -580,6 +612,31 @@ def test_set_unavailable_periods_lighting
580612
_test_day_schedule(schedule, end_month, end_day, year, 0, end_hour)
581613
_test_day_schedule(schedule, end_month, end_day + 1, year, nil, nil)
582614

615+
# 2 calendar days, partial first and last day
616+
begin_month = 12
617+
begin_day = 14
618+
begin_hour = 20
619+
end_month = 12
620+
end_day = 15
621+
end_hour = 6
622+
623+
model, hpxml, _hpxml_bldg = _test_measure(args_hash)
624+
year = model.getYearDescription.assumedYear
625+
626+
schedule = model.getScheduleRulesets.find { |schedule| schedule.name.to_s == sch_name }
627+
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
628+
629+
schedule_rules = schedule.scheduleRules
630+
Schedule.set_unavailable_periods(model, schedule, sch_name, unavailable_periods)
631+
unavailable_schedule_rules = schedule.scheduleRules - schedule_rules
632+
633+
assert_equal(2, unavailable_schedule_rules.size)
634+
635+
_test_day_schedule(schedule, begin_month, begin_day - 1, year, nil, nil)
636+
_test_day_schedule(schedule, begin_month, begin_day, year, begin_hour, 24)
637+
_test_day_schedule(schedule, end_month, end_day, year, 0, end_hour)
638+
_test_day_schedule(schedule, end_month, end_day + 1, year, nil, nil)
639+
583640
# wrap around
584641
begin_month = 12
585642
begin_day = 1

0 commit comments

Comments
 (0)