Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
bef75cf
proof-of-concept of smootherstep schedule processor
eringold Nov 14, 2024
bc66aeb
POC parametric occ schedule data representation and schedule processor
eringold Dec 19, 2024
579731a
derived equipment schedule poc
eringold Jan 14, 2025
ef936dc
add full parametric occ schedule
eringold Mar 13, 2025
543a9d9
update create_complex_schedule
eringold Mar 13, 2025
8991745
occ schedule data to parametric form converter
eringold Mar 13, 2025
ffb7878
add schedule derivations
eringold Aug 18, 2025
b9cd263
Merge branch 'master' into param_sch_refactor
mdahlhausen Dec 19, 2025
9012855
add cleaned schedules json
eringold Jan 2, 2026
d9e3129
script to convert occupancy schedules to parametric form
eringold Jan 2, 2026
4cd7118
Merge branch 'param_sch_refactor' of https://github.com/NREL/openstud…
eringold Jan 2, 2026
ad2827c
setDoPlantSizingCalculation only if SizingPlant objects
mdahlhausen Jan 2, 2026
83b1e64
Update version.rb
mdahlhausen Jan 5, 2026
a91499b
replace ashrae_90_1_schedules with cleaned version
mdahlhausen Jan 6, 2026
e06b4de
Update occ_schedule_converter.rb
mdahlhausen Jan 6, 2026
f76d179
clean up climate zone value getters/setters
mdahlhausen Jan 21, 2026
0c43976
Merge pull request #2077 from NatLabRockies/climate_zone_cleanup
mdahlhausen Jan 21, 2026
69fbaf3
Merge branch 'develop' into param_sch_refactor
mdahlhausen Jan 21, 2026
4ee3123
rename file and move test files
mdahlhausen Jan 23, 2026
6f61664
Update create_parametric_schedule.rb
mdahlhausen Jan 23, 2026
31f1f04
Update .gitignore
mdahlhausen Jan 23, 2026
cd07f1a
Update test_schedules_data.json
mdahlhausen Jan 23, 2026
6447c7e
create_derived_schedule_from_occupancy_schedule
mdahlhausen Jan 27, 2026
6425712
adjust schedule derivation
mdahlhausen Feb 5, 2026
e24970b
make smr/wntr base/peak/response optional
mdahlhausen Feb 5, 2026
e5ae727
typical parametric schedules default
mdahlhausen Feb 13, 2026
87e40d0
add laundry gas equipment
mdahlhausen Feb 18, 2026
74781a8
bug fixes
mdahlhausen Feb 19, 2026
e4c78f3
Create test_schedule_comparison.rb
mdahlhausen Feb 19, 2026
5dd8888
add schedule comparison and adjust default parameters
mdahlhausen Feb 20, 2026
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ lib/openstudio-standards/btap/costing/btap_results/tests/output/

# Miscellaneous
/lib/openstudio-standards/prototypes/common/do_not_edit_metaclasses.rb
/lib/openstudio-standards/schedules/schedule_refactor
/test/refactor/
/data/costing/rs_means_auth
/tmp/
Expand Down
2 changes: 1 addition & 1 deletion data/standards/export_OpenStudio_libraries.rb
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ def export_openstudio_libraries
std_applier.space_type_apply_internal_loads(space_type)

# Schedules
std_applier.space_type_apply_internal_load_schedules(space_type)
std_applier.space_type_apply_standard_internal_load_schedules(space_type)
end
end

Expand Down
1 change: 1 addition & 0 deletions lib/openstudio-standards.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ module OpenstudioStandards

# Schedules Module
require_relative 'openstudio-standards/schedules/create'
require_relative 'openstudio-standards/schedules/create_parametric_schedule'
require_relative 'openstudio-standards/schedules/modify'
require_relative 'openstudio-standards/schedules/information'
require_relative 'openstudio-standards/schedules/parametric'
Expand Down
17 changes: 11 additions & 6 deletions lib/openstudio-standards/create_typical/create_typical.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ module CreateTypical
# @param add_thermostat [Boolean] Add thermostats to thermal zones based on the standards space type
# @param add_refrigeration [Boolean] Add refrigerated cases and walkin refrigeration
# @param refrigeration_template [String] The refrigeration technology level, either 'old', 'new', or 'advanced'
# @param schedule_method [String] The method for creating schedules for internal loads and thermostats. Options are 'prototype' or 'parametric'. 'prototype' uses the default schedules from the legacy DOE Prototype Models. 'parametric' creates schedules based on parametric occupancy schedules and derives internal load schedules from those. 'parametric' is recommend, as it results in more realistic schedules and better alignment between internal loads and occupancy.
# @param modify_wkdy_op_hrs [Boolean] Modify the default weekday hours of operation
# @param wkdy_op_hrs_start_time [Double] Weekday operating hours start time. Enter as a fractional value, e.g. 5:15pm is 17.25. Only used if modify_wkdy_op_hrs is true.
# @param wkdy_op_hrs_duration [Double] Weekday operating hours duration from start time. Enter as a fractional value, e.g. 5:15pm is 17.25. Only used if modify_wkdy_op_hrs is true.
Expand Down Expand Up @@ -78,6 +79,7 @@ def self.create_typical_building_from_model(model,
add_thermostat: true,
add_refrigeration: true,
refrigeration_template: 'new',
schedule_method: 'parametric',
modify_wkdy_op_hrs: false,
wkdy_op_hrs_start_time: 8.0,
wkdy_op_hrs_duration: 8.0,
Expand Down Expand Up @@ -242,11 +244,14 @@ def self.create_typical_building_from_model(model,
end

# apply internal load schedules
standard.space_type_apply_internal_load_schedules(space_type)
if schedule_method == 'prototype'
standard.space_type_apply_standard_internal_load_schedules(space_type)
else # parametric
OpenstudioStandards::Schedules.space_type_apply_parametric_internal_load_schedules(space_type)
end

# extend space type name to include the template. Consider this as well for load defs
space_type.setName("#{space_type.name} - #{template}")
OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "Adding loads to space type named #{space_type.name}")
# Include the template as an additional property on the space type object if present
space_type.additionalProperties.setFeature('template', "#{template}")
end

# warn if spaces in model without space type
Expand Down Expand Up @@ -923,7 +928,7 @@ def self.create_space_types_and_constructions(model,
end

# assign internal load schedules
standard.space_type_apply_internal_load_schedules(space_type)
standard.space_type_apply_standard_internal_load_schedules(space_type)

# assign colors
standard.space_type_apply_rendering_color(space_type)
Expand Down Expand Up @@ -992,7 +997,7 @@ def self.create_space_types_and_constructions(model,
end

# set climate zone
os_climate_zone = climate_zone.gsub('ASHRAE 169-2013-', '')
os_climate_zone = climate_zone.gsub(/ASHRAE .*-.*-/, '')
# trim off letter from climate zone 7 or 8
if (os_climate_zone[0] == '7') || (os_climate_zone[0] == '8')
os_climate_zone = os_climate_zone[0]
Expand Down
2 changes: 1 addition & 1 deletion lib/openstudio-standards/hvac/components/fan.rb
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ def self.create_typical_fan(model, typical_fan,
fan_power_minimum_flow_rate_fraction: nil,
fan_curve: nil,
system_availability_manager_coupling_mode: nil)
fan_json = JSON.parse(File.read("#{__dir__}/data/fans.json"), symbolize_names: true)
fan_json = JSON.parse(File.read("#{File.dirname(__FILE__)}/data/fans.json"), symbolize_names: true)
fan_data = fan_json[:fans].select { |hash| hash[:name] == typical_fan }

if fan_data.empty?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -882,7 +882,7 @@
{
"standards_building_type": "Outpatient",
"standards_space_type": "DressingRoom",
"new_standards_space_type": "dressing room"
"new_standards_space_type": "exam/treatment"
},
{
"standards_building_type": "Outpatient",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ def model_add_loads(model)
space_type_apply_internal_loads(space_type)

# Schedules
space_type_apply_internal_load_schedules(space_type)
space_type_apply_standard_internal_load_schedules(space_type)

# Thermostat Schedules
space_type_apply_thermostat_schedules(space_type)
Expand Down
162 changes: 162 additions & 0 deletions lib/openstudio-standards/schedules/analyze_schedule_data.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
require 'openstudio'
require 'json'
require 'csv'
require_relative 'create'
require_relative 'add_schedule_parametric'

def load_schedule_data(path)
file_content = File.read(path)
return JSON.parse(file_content, symbolize_names: true)
end

def select_schedules_by_category(schedules, selection_key)
return schedules.select { |obj| obj[:category] == selection_key }
end

# translates standards occupancy schedule data to parametric form
def schedule_data_to_parametric(data)
all_parametric_data = []

data.each do |obj|
param_data = {}
param_data[:name] = obj[:name]
param_data[:category] = obj[:category]
param_data[:units] = obj[:units]
param_data[:day_types] = obj[:day_types]
param_data[:start_date] = obj[:start_date]
param_data[:end_date] = obj[:end_date]
param_data[:type] = 'parametric'

hr_data = obj.select { |k, v| k.to_s.include?('hr_') }
hr_vals = hr_data.values
base = hr_vals.min
peak = hr_vals.max

max_pos_chg = 0
max_neg_chg = 0
st = 0
et = 0
hr_vals.each_with_index do |val, i|
next if i == 0

change = val - hr_vals[i - 1]
if change > max_pos_chg
max_pos_chg = change
st = i + 1
end

if change < max_neg_chg
max_neg_chg = change
et = i + 1
end
end

# diff between st and last time of base val before st
base_st_adj = 0
base_et_adj = 0
peak_st_adj = 0
peak_et_adj = 0

last_base_idx = hr_vals[0...st].rindex(base)
base_st_adj = last_base_idx - (st - 1) unless last_base_idx.nil?
peak_st_adj = [0, hr_vals.index(peak) - (st - 1)].max

last_peak_idx = hr_vals[st...et].rindex(peak)
peak_et_adj = (st + last_peak_idx + 1) - et unless last_peak_idx.nil?

base_et_adj = [0, hr_vals[et..].index(base)].max unless hr_vals[et..].index(base).nil?

param_data[:base_std] = base
param_data[:peak_std] = peak
param_data[:st_std] = st
param_data[:et_std] = et
param_data[:control_points] = [
['st', { '+': base_st_adj }, 'base', {}],
['st', { '+': peak_st_adj }, 'peak', {}],
['et', { '+': peak_et_adj }, 'peak', {}],
['et', { '+': base_et_adj }, 'base', {}]
]
all_parametric_data << param_data
end
return all_parametric_data
end

def write_data_to_csv(data)
names = data.first.keys
c = CSV.generate do |csv|
csv << names
data.each do |o|
csv << o.values
end
end
puts 'writing csv'
File.write('data_summary.csv', c)
end

# translate standard schedule data to a form usable by OpenstudioStandards::Schedules.create_complex_schedule
def schedule_data_to_input_hash(schedule_array)
# schedule_array is an array of hashes with profiles of the same name
options = {}
options['name'] = schedule_array[0][:name]
options['rules'] = []
schedule_array.each do |obj|
hr_data = obj.select { |k, v| k.to_s.include?('hr_') }
tv_pairs = hr_data.keys.map { |k| k.to_s.gsub('hr_', '').to_f }.zip(hr_data.values)
# only keep last time-value pair with unique value
tv_pairs_reduced = tv_pairs.reject.with_index { |e, i| e[1] == tv_pairs[i + 1][1] unless i == 23 }
day_types = obj[:day_types].split('|')
day_types.each do |day_type|
case day_type
when 'Default'
options['default_day'] = ['default'] + tv_pairs_reduced
when 'WntrDsn'
options['winter_design_day'] = tv_pairs_reduced
when 'SmrDsn'
options['summer_design_day'] = tv_pairs_reduced
when 'Hol'
# do nothing
else
start_date = DateTime.strptime(obj[:start_date], '%m/%d/%Y').strftime('%m/%d')
end_date = DateTime.strptime(obj[:end_date], '%m/%d/%Y').strftime('%m/%d')
rule_a = [day_type]
rule_a << "#{start_date}-#{end_date}"
rule_a << day_type
rule_a += tv_pairs_reduced
options['rules'] << rule_a
end
end
end
return options
end

def convert_all_schedules
data_path = 'C:/Repos/PNNL/building-energy-standards-data/building_energy_standards_data/database_files/support_schedules.json'
data = load_schedule_data(data_path)

occ_sch_data = select_schedules_by_category(data, 'Occupancy')
# occ_sch_data = find_values(occ_sch_data)
# write_data_to_csv(occ_sch_data)

names = occ_sch_data.map { |obj| obj[:name] }.uniq

# puts names

model = OpenStudio::Model::Model.new
model.getTimestep.setNumberOfTimestepsPerHour(4)
names.each do |name|
print "Processing #{name}"
sch_data = occ_sch_data.select { |e| e[:name] == name }

parametric_form = schedule_data_to_parametric(sch_data)

OpenstudioStandards::Schedules.create_parametric_schedule_full(model, parametric_form, name, {})

# create equivalent schedule from standard data
standard_sch_inputs = schedule_data_to_input_hash(sch_data)
OpenstudioStandards::Schedules.create_complex_schedule(model, standard_sch_inputs)
end

model.save('test_compare_all.osm', true)
end

convert_all_schedules
4 changes: 3 additions & 1 deletion lib/openstudio-standards/schedules/create.rb
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ def self.create_complex_schedule(model, options = {})
default_data_array.delete_at(0)
default_data_array.each do |data_pair|
hour = data_pair[0].truncate
min = ((data_pair[0] - hour) * 60).to_i
min = ((data_pair[0] - hour) * 60).round
default_day.addValue(OpenStudio::Time.new(0, hour, min, 0), data_pair[1])
end

Expand All @@ -273,6 +273,8 @@ def self.create_complex_schedule(model, options = {})
rule.setApplyThursday(true) if days.include? 'Thu'
rule.setApplyFriday(true) if days.include? 'Fri'
rule.setApplySaturday(true) if days.include? 'Sat'
rule.setApplyWeekdays(true) if days.include? 'Wkdy'
rule.setApplyWeekends(true) if days.include? 'Wknd'
day_schedule = rule.daySchedule
day_schedule.setName("#{sch_ruleset.name} #{data_array[0]}")
data_array.delete_at(0)
Expand Down
Loading