class Standard # @!group ScheduleRuleset # Returns the min and max value for this schedule_day object # # @param day_sch [OpenStudio::Model::ScheduleDay] schedule day object # @return [Double] day full load hours def day_schedule_equivalent_full_load_hrs(day_sch) # Determine the full load hours for just one day daily_flh = 0 values = day_sch.values times = day_sch.times previous_time_decimal = 0 times.each_with_index do |time, i| time_decimal = (time.days * 24.0) + time.hours + (time.minutes / 60.0) + (time.seconds / 3600.0) duration_of_value = time_decimal - previous_time_decimal # OpenStudio::logFree(OpenStudio::Debug, "openstudio.standards.ScheduleRuleset", " Value of #{values[i]} for #{duration_of_value} hours") daily_flh += values[i] * duration_of_value previous_time_decimal = time_decimal end # OpenStudio::logFree(OpenStudio::Debug, "openstudio.standards.ScheduleRuleset", " #{daily_flh.round(2)} EFLH per day") # OpenStudio::logFree(OpenStudio::Debug, "openstudio.standards.ScheduleRuleset", " Used #{number_of_days_sch_used} days per year") return daily_flh end # Returns the equivalent full load hours (EFLH) for this schedule. # For example, an always-on fractional schedule # (always 1.0, 24/7, 365) would return a value of 8760. # # @author Andrew Parker, NREL. Matt Leach, NORESCO. # @param schedule_ruleset [OpenStudio::Model::ScheduleRuleset] schedule ruleset object # @return [Double] The total number of full load hours for this schedule. def schedule_ruleset_annual_equivalent_full_load_hrs(schedule_ruleset) # OpenStudio::logFree(OpenStudio::Debug, "openstudio.standards.ScheduleRuleset", "Calculating total annual EFLH for schedule: #{self.name}") # Define the start and end date year_start_date = nil year_end_date = nil if schedule_ruleset.model.yearDescription.is_initialized year_description = schedule_ruleset.model.yearDescription.get year = year_description.assumedYear year_start_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('January'), 1, year) year_end_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('December'), 31, year) else OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.ScheduleRuleset', 'Year description is not specified. Full load hours calculation will assume 2009, the default year OS uses.') year_start_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('January'), 1, 2009) year_end_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('December'), 31, 2009) end # Get the ordered list of all the day schedules # that are used by this schedule ruleset day_schs = schedule_ruleset.getDaySchedules(year_start_date, year_end_date) # OpenStudio::logFree(OpenStudio::Debug, "openstudio.standards.ScheduleRuleset", "***Day Schedules Used***") day_schs.uniq.each do |day_sch| # OpenStudio::logFree(OpenStudio::Debug, "openstudio.standards.ScheduleRuleset", " #{day_sch.name.get}") end # Get a 365-value array of which schedule is used on each day of the year, day_schs_used_each_day = schedule_ruleset.getActiveRuleIndices(year_start_date, year_end_date) if !day_schs_used_each_day.length == 365 OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.ScheduleRuleset', "#{schedule_ruleset.name} does not have 365 daily schedules accounted for, cannot accurately calculate annual EFLH.") return 0 end # Create a map that shows how many days each schedule is used day_sch_freq = day_schs_used_each_day.group_by { |n| n } # Build a hash that maps schedule day index to schedule day schedule_index_to_day = {} day_schs.each_with_index do |day_sch, i| schedule_index_to_day[day_schs_used_each_day[i]] = day_sch end # Loop through each of the schedules that is used, figure out the # full load hours for that day, then multiply this by the number # of days that day schedule applies and add this to the total. annual_flh = 0 max_daily_flh = 0 default_day_sch = schedule_ruleset.defaultDaySchedule day_sch_freq.each do |freq| # OpenStudio::logFree(OpenStudio::Debug, "openstudio.standards.ScheduleRuleset", freq.inspect # exit # OpenStudio::logFree(OpenStudio::Debug, "openstudio.standards.ScheduleRuleset", "Schedule Index = #{freq[0]}" sch_index = freq[0] number_of_days_sch_used = freq[1].size # Get the day schedule at this index day_sch = nil day_sch = if sch_index == -1 # If index = -1, this day uses the default day schedule (not a rule) default_day_sch else schedule_index_to_day[sch_index] end # OpenStudio::logFree(OpenStudio::Debug, "openstudio.standards.ScheduleRuleset", "Calculating EFLH for: #{day_sch.name}") daily_flh = day_schedule_equivalent_full_load_hrs(day_sch) # Multiply the daily EFLH by the number # of days this schedule is used per year # and add this to the overall total annual_flh += daily_flh * number_of_days_sch_used end # Warn if the max daily EFLH is more than 24, # which would indicate that this isn't a # fractional schedule. if max_daily_flh > 24 OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.ScheduleRuleset', "#{schedule_ruleset.name} has more than 24 EFLH in one day schedule, indicating that it is not a fractional schedule.") end return annual_flh end # Returns the min and max value for this schedule. # It doesn't evaluate design days only run-period conditions # # @author David Goldwasser, NREL. # @param schedule_ruleset [OpenStudio::Model::ScheduleRuleset] schedule ruleset object # @return [Hash] Hash has two keys, min and max. def schedule_ruleset_annual_min_max_value(schedule_ruleset) # gather profiles profiles = [] profiles << schedule_ruleset.defaultDaySchedule rules = schedule_ruleset.scheduleRules rules.each do |rule| profiles << rule.daySchedule end # test profiles min = nil max = nil profiles.each do |profile| profile.values.each do |value| if min.nil? min = value else min = value if min > value end if max.nil? max = value else max = value if max < value end end end result = { 'min' => min, 'max' => max } return result end # Returns the total number of hours where the schedule is greater than the specified value. # # @author Andrew Parker, NREL. # @param schedule_ruleset [OpenStudio::Model::ScheduleRuleset] schedule ruleset object # @param lower_limit [Double] the lower limit. Values equal to the limit will not be counted. # @return [Double] The total number of hours this schedule is above the specified value. def schedule_ruleset_annual_hours_above_value(schedule_ruleset, lower_limit) # OpenStudio::logFree(OpenStudio::Debug, "openstudio.standards.ScheduleRuleset", "Calculating total annual hours above #{lower_limit} for schedule: #{self.name}") # Define the start and end date year_start_date = nil year_end_date = nil if schedule_ruleset.model.yearDescription.is_initialized year_description = schedule_ruleset.model.yearDescription.get year = year_description.assumedYear year_start_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('January'), 1, year) year_end_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('December'), 31, year) else OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.ScheduleRuleset', 'Year description is not specified. Annual hours above value calculation will assume 2009, the default year OS uses.') year_start_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('January'), 1, 2009) year_end_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('December'), 31, 2009) end # Get the ordered list of all the day schedules # that are used by this schedule ruleset day_schs = schedule_ruleset.getDaySchedules(year_start_date, year_end_date) # OpenStudio::logFree(OpenStudio::Debug, "openstudio.standards.ScheduleRuleset", "***Day Schedules Used***") day_schs.uniq.each do |day_sch| # OpenStudio::logFree(OpenStudio::Debug, "openstudio.standards.ScheduleRuleset", " #{day_sch.name.get}") end # Get a 365-value array of which schedule is used on each day of the year, day_schs_used_each_day = schedule_ruleset.getActiveRuleIndices(year_start_date, year_end_date) if !day_schs_used_each_day.length == 365 OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.ScheduleRuleset', "#{schedule_ruleset.name} does not have 365 daily schedules accounted for, cannot accurately calculate annual EFLH.") return 0 end # Create a map that shows how many days each schedule is used day_sch_freq = day_schs_used_each_day.group_by { |n| n } # Build a hash that maps schedule day index to schedule day schedule_index_to_day = {} day_schs.each_with_index do |day_sch, i| schedule_index_to_day[day_schs_used_each_day[i]] = day_sch end # Loop through each of the schedules that is used, figure out the # hours for that day, then multiply this by the number # of days that day schedule applies and add this to the total. annual_hrs = 0 default_day_sch = schedule_ruleset.defaultDaySchedule day_sch_freq.each do |freq| # OpenStudio::logFree(OpenStudio::Debug, "openstudio.standards.ScheduleRuleset", freq.inspect # exit # OpenStudio::logFree(OpenStudio::Debug, "openstudio.standards.ScheduleRuleset", "Schedule Index = #{freq[0]}" sch_index = freq[0] number_of_days_sch_used = freq[1].size # Get the day schedule at this index day_sch = nil day_sch = if sch_index == -1 # If index = -1, this day uses the default day schedule (not a rule) default_day_sch else schedule_index_to_day[sch_index] end # OpenStudio::logFree(OpenStudio::Debug, "openstudio.standards.ScheduleRuleset", "Calculating hours above #{lower_limit} for: #{day_sch.name}") # Determine the hours for just one day daily_hrs = 0 values = day_sch.values times = day_sch.times previous_time_decimal = 0 times.each_with_index do |time, i| time_decimal = (time.days * 24.0) + time.hours + (time.minutes / 60.0) + (time.seconds / 3600.0) duration_of_value = time_decimal - previous_time_decimal # OpenStudio::logFree(OpenStudio::Debug, "openstudio.standards.ScheduleRuleset", " Value of #{values[i]} for #{duration_of_value} hours") daily_hrs += values[i] * duration_of_value previous_time_decimal = time_decimal end # OpenStudio::logFree(OpenStudio::Debug, "openstudio.standards.ScheduleRuleset", " #{daily_hrs.round(2)} hours above #{lower_limit} per day") # OpenStudio::logFree(OpenStudio::Debug, "openstudio.standards.ScheduleRuleset", " Used #{number_of_days_sch_used} days per year") # Multiply the daily hours by the number # of days this schedule is used per year # and add this to the overall total annual_hrs += daily_hrs * number_of_days_sch_used end return annual_hrs end # Returns the averaged hourly values of the ruleset schedule for all hours of the year # # @param schedule_ruleset [OpenStudio::Model::ScheduleRuleset] schedule ruleset object # @return [Array] An array of hourly values over the whole year def schedule_ruleset_annual_hourly_values(schedule_ruleset) schedule_values = [] year_description = schedule_ruleset.model.getYearDescription (1..365).each do |i| date = year_description.makeDate(i) day_sch = schedule_ruleset.getDaySchedules(date, date)[0] (0..23).each do |j| # take average value over the hour value_15 = day_sch.getValue(OpenStudio::Time.new(0, j, 15, 0)) value_30 = day_sch.getValue(OpenStudio::Time.new(0, j, 30, 0)) value_45 = day_sch.getValue(OpenStudio::Time.new(0, j, 45, 0)) avg = (value_15 + value_30 + value_45).to_f / 3.0 schedule_values << avg.round(5) end end return schedule_values end # Remove unused profiles and set most prevalent profile as default # When moving profile that isn't lowest priority to default need to address possible issues with overlapping rules dates or days of week # method expands on functionality of RemoveUnusedDefaultProfiles measure # # @author David Goldwasser # @param schedule_ruleset [OpenStudio::Model::ScheduleRuleset] schedule ruleset object # @return [OpenStudio::Model::ScheduleRuleset] schedule ruleset object def schedule_ruleset_cleanup_profiles(schedule_ruleset) # set start and end dates year_description = schedule_ruleset.model.yearDescription.get year = year_description.assumedYear year_start_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('January'), 1, year) year_end_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('December'), 31, year) indices_vector = schedule_ruleset.getActiveRuleIndices(year_start_date, year_end_date) most_frequent_item = indices_vector.uniq.max_by { |i| indices_vector.count(i) } rule_vector = schedule_ruleset.scheduleRules replace_existing_default = false if indices_vector.include? -1 && most_frequent_item != -1 # clean up if default isn't most common (e.g. sunday vs. weekday) # if no existing rules cover specific days of week, make new rule from default covering those days of week possible_days_of_week = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] used_days_of_week = [] rule_vector.each do |rule| if rule.applyMonday then used_days_of_week << 'Monday' end if rule.applyTuesday then used_days_of_week << 'Tuesday' end if rule.applyWednesday then used_days_of_week << 'Wednesday' end if rule.applyThursday then used_days_of_week << 'Thursday' end if rule.applyFriday then used_days_of_week << 'Friday' end if rule.applySaturday then used_days_of_week << 'Saturday' end if rule.applySunday then used_days_of_week << 'Sunday' end end if used_days_of_week.uniq.size < possible_days_of_week.size replace_existing_default = true schedule_rule_new = OpenStudio::Model::ScheduleRule.new(schedule_ruleset, schedule_ruleset.defaultDaySchedule) if !used_days_of_week.include?('Monday') then schedule_rule_new.setApplyMonday(true) end if !used_days_of_week.include?('Tuesday') then schedule_rule_new.setApplyTuesday(true) end if !used_days_of_week.include?('Wednesday') then schedule_rule_new.setApplyWednesday(true) end if !used_days_of_week.include?('Thursday') then schedule_rule_new.setApplyThursday(true) end if !used_days_of_week.include?('Friday') then schedule_rule_new.setApplyFriday(true) end if !used_days_of_week.include?('Saturday') then schedule_rule_new.setApplySaturday(true) end if !used_days_of_week.include?('Sunday') then schedule_rule_new.setApplySunday(true) end end end if !indices_vector.include?(-1) || replace_existing_default OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.ScheduleRuleset', "#{schedule_ruleset.name} does not used the default profile, it will be replaced.") # reset values in default ScheduleDay old_default_schedule_day = schedule_ruleset.defaultDaySchedule old_default_schedule_day.clearValues # update selection to the most commonly used profile vs. the lowest priority, if it can be done without any conflicts # safe test is to see if any other rules use same days of week as most common, # if doesn't pass then make highest rule the new default to avoid any problems. School may not pass this test, woudl use last rule days_of_week_most_frequent_item = [] schedule_rule_most_frequent = rule_vector[most_frequent_item] if schedule_rule_most_frequent.applyMonday then days_of_week_most_frequent_item << 'Monday' end if schedule_rule_most_frequent.applyTuesday then days_of_week_most_frequent_item << 'Tuesday' end if schedule_rule_most_frequent.applyWednesday then days_of_week_most_frequent_item << 'Wednesday' end if schedule_rule_most_frequent.applyThursday then days_of_week_most_frequent_item << 'Thursday' end if schedule_rule_most_frequent.applyFriday then days_of_week_most_frequent_item << 'Friday' end if schedule_rule_most_frequent.applySaturday then days_of_week_most_frequent_item << 'Saturday' end if schedule_rule_most_frequent.applySunday then days_of_week_most_frequent_item << 'Sunday' end # loop through rules conflict_found = false rule_vector.each do |rule| next if rule == schedule_rule_most_frequent days_of_week_most_frequent_item.each do |day_of_week| if (day_of_week == 'Monday') && rule.applyMonday then conflict_found == true end if (day_of_week == 'Tuesday') && rule.applyTuesday then conflict_found == true end if (day_of_week == 'Wednesday') && rule.applyWednesday then conflict_found == true end if (day_of_week == 'Thursday') && rule.applyThursday then conflict_found == true end if (day_of_week == 'Friday') && rule.applyFriday then conflict_found == true end if (day_of_week == 'Saturday') && rule.applySaturday then conflict_found == true end if (day_of_week == 'Sunday') && rule.applySunday then conflict_found == true end end end if conflict_found new_default_index = indices_vector.max else new_default_index = most_frequent_item end # get values for new default profile new_default_daySchedule = rule_vector[new_default_index].daySchedule new_default_daySchedule_values = new_default_daySchedule.values new_default_daySchedule_times = new_default_daySchedule.times # update values and times for default profile for i in 0..(new_default_daySchedule_values.size - 1) old_default_schedule_day.addValue(new_default_daySchedule_times[i], new_default_daySchedule_values[i]) end # remove rule object that has become the default. Also try to remove the ScheduleDay rule_vector[new_default_index].remove # this seems to also remove the ScheduleDay associated with the rule end return schedule_ruleset end # this will use parametric inputs contained in schedule and profiles along with inferred hours of operation to generate updated ruleset schedule profiles # # @author David Goldwasser # @param schedule [OpenStudio::Model::ScheduleRuleset] schedule ruleset object # @param ramp_frequency [Double] ramp frequency in minutes # @param infer_hoo_for_non_assigned_objects [Bool] attempt to get hoo for objects like swh with and exterior lighting # @param error_on_out_of_order [Bool] true will error if applying formula creates out of order values # @return [OpenStudio::Model::ScheduleRuleset] schedule ruleset object def schedule_apply_parametric_inputs(schedule, ramp_frequency, infer_hoo_for_non_assigned_objects, error_on_out_of_order, parametric_inputs = nil) # Check if parametric inputs were supplied and generate them if not if parametric_inputs.nil? OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.ScheduleRuleset', "For #{schedule.name}, no parametric inputs were not supplied so they will be generated now.") parametric_inputs = model_setup_parametric_schedules(schedule.model, gather_data_only: true) end # Check that parametric inputs exist for this schedule after generation if parametric_inputs[schedule].nil? OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.ScheduleRuleset', "For #{schedule.name}, no parametric inputs exists so schedule will not be changed.") return schedule end # Check that an hours of operation schedule is associated with this schedule if parametric_inputs[schedule][:hoo_inputs].nil? OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.ScheduleRuleset', "For #{schedule.name}, no associated hours of operation schedule was found so schedule will not be changed.") return schedule end # Get the hours of operation schedule hours_of_operation = parametric_inputs[schedule][:hoo_inputs] # OpenStudio.logFree(OpenStudio::Debug, 'openstudio.standards.ScheduleRuleset', "For #{schedule.name} hours_of_operation = #{hours_of_operation.name}.") starting_aeflh = schedule_ruleset_annual_equivalent_full_load_hrs(schedule) # store floor and ceiling value val_flr = nil if schedule.hasAdditionalProperties && schedule.additionalProperties.hasFeature('param_sch_floor') val_flr = schedule.additionalProperties.getFeatureAsDouble('param_sch_floor').get end val_clg = nil if schedule.hasAdditionalProperties && schedule.additionalProperties.hasFeature('param_sch_ceiling') val_clg = schedule.additionalProperties.getFeatureAsDouble('param_sch_ceiling').get end # loop through schedule days from highest to lowest priority (with default as lowest priority) # if rule needs to be split to address hours of operation rules add new rule next to relevant existing rule profiles = {} schedule.scheduleRules.each do |rule| # remove any use manually generated non parametric rules or any auto-generated rules from prior application of formulas and hoo sch_day = rule.daySchedule if !sch_day.hasAdditionalProperties || !sch_day.additionalProperties.hasFeature('param_day_tag') || (sch_day.additionalProperties.getFeatureAsString('param_day_tag').get == 'autogen') sch_day.remove # remove day schedule for this rule rule.remove # remove the rule elsif !sch_day.additionalProperties.hasFeature('param_day_profile') OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.ScheduleRuleset', "#{schedule.name} doesn't have a parametric formula for #{rule.name} This profile will not be altered.") next else profiles[sch_day] = rule end end profiles[schedule.defaultDaySchedule] = nil # get indices for current schedule year_description = schedule.model.yearDescription.get year = year_description.assumedYear year_start_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('January'), 1, year) year_end_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('December'), 31, year) indices_vector = schedule.getActiveRuleIndices(year_start_date, year_end_date) # process profiles profiles.each do |sch_day, rule| # for current profile index identify hours of operation index that contains all days if rule.nil? current_rule_index = -1 else current_rule_index = rule.ruleIndex end # loop through indices looking of rule in hoo that contains days in the rule hoo_target_index = nil days_used = [] indices_vector.each_with_index do |profile_index, i| if profile_index == current_rule_index then days_used << i + 1 end end # find days_used in hoo profiles that contains all days used from this profile hoo_profile_match_hash = {} best_fit_check = {} hours_of_operation.each do |profile_index, value| days_for_rule_not_in_hoo_profile = days_used - value[:days_used] hoo_profile_match_hash[profile_index] = days_for_rule_not_in_hoo_profile best_fit_check[profile_index] = days_for_rule_not_in_hoo_profile.size if days_for_rule_not_in_hoo_profile.empty? hoo_target_index = profile_index end end clone_needed = false hoo_target_index = best_fit_check.key(best_fit_check.values.min) if best_fit_check[hoo_target_index] > 0 clone_needed = true end # get hours of operation for this specific profile hoo_start = hours_of_operation[hoo_target_index][:hoo_start] hoo_end = hours_of_operation[hoo_target_index][:hoo_end] # update scheduleDay process_hrs_of_operation_hash(sch_day, hoo_start, hoo_end, val_flr, val_clg, ramp_frequency, infer_hoo_for_non_assigned_objects, error_on_out_of_order) # clone new rule if needed if clone_needed # make list of new rules needed as has or array autogen_rules = {} days_to_fill = hoo_profile_match_hash[hoo_target_index] hours_of_operation.each do |profile_index, value| remainder = days_to_fill - value[:days_used] day_for_rule = days_to_fill - remainder if remainder.size < days_to_fill.size autogen_rules[profile_index] = { days_to_fill: day_for_rule, hoo_start: hoo_start, hoo_end: hoo_end } end days_to_fill = remainder end # loop through new rules to make and process autogen_rules.each do |autogen_rule, hash| # generate new rule sch_rule_autogen = OpenStudio::Model::ScheduleRule.new(schedule) if current_rule_index target_index = schedule.scheduleRules.size - 1 # just above default else target_index = current_rule_index - 1 # confirm just above orig rule end current_rule_index = target_index if rule.nil? sch_rule_autogen.setName("autogen #{schedule.name} #{target_index}") else sch_rule_autogen.setName("autogen #{rule.name} #{target_index}") end schedule.setScheduleRuleIndex(sch_rule_autogen, target_index) # @todo confirm this is higher priority than the non-auto-generated rule hash[:days_to_fill].each do |day| date = OpenStudio::Date.fromDayOfYear(day, year) sch_rule_autogen.addSpecificDate(date) end sch_rule_autogen.setApplySunday(true) sch_rule_autogen.setApplyMonday(true) sch_rule_autogen.setApplyTuesday(true) sch_rule_autogen.setApplyWednesday(true) sch_rule_autogen.setApplyThursday(true) sch_rule_autogen.setApplyFriday(true) sch_rule_autogen.setApplySaturday(true) # match profile from source rule (don't add time/values need a formula to process) sch_day_auto_gen = sch_rule_autogen.daySchedule sch_day_auto_gen.setName("#{sch_rule_autogen.name}_day_sch") sch_day_auto_gen.additionalProperties.setFeature('param_day_tag', 'autogen') val = sch_day.additionalProperties.getFeatureAsString('param_day_profile').get sch_day_auto_gen.additionalProperties.setFeature('param_day_profile', val) val = sch_day.additionalProperties.getFeatureAsString('param_day_secondary_logic').get sch_day_auto_gen.additionalProperties.setFeature('param_day_secondary_logic', val) val = sch_day.additionalProperties.getFeatureAsString('param_day_secondary_logic_arg_val').get sch_day_auto_gen.additionalProperties.setFeature('param_day_secondary_logic_arg_val', val) # get hours of operation for this specific profile hoo_start = hash[:hoo_start] hoo_end = hash[:hoo_end] # process new rule process_hrs_of_operation_hash(sch_day_auto_gen, hoo_start, hoo_end, val_flr, val_clg, ramp_frequency, infer_hoo_for_non_assigned_objects, error_on_out_of_order) end end end # @todo create summer and winter design day profiles (make sure scheduleDay objects parametric) # @todo should they have their own formula, or should this be hard coded logic by schedule type # check orig vs. updated aeflh final_aeflh = schedule_ruleset_annual_equivalent_full_load_hrs(schedule) percent_change = ((starting_aeflh - final_aeflh) / starting_aeflh) * 100.0 if percent_change.abs > 0.05 OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.ScheduleRuleset', "For #{schedule.name}, applying parametric schedules made a #{percent_change.round(1)}% change in annual equivalent full load hours. (from #{starting_aeflh.round(2)} to #{final_aeflh.round(2)})") end return schedule end # Apply specified hours of operation values to rules in this schedule. # Weekday values will be applied to the default profile. # Weekday values will be applied to any rules that are used on a weekday. # Saturday values will be applied to any rules that are used on a Saturday. # Sunday values will be applied to any rules that are used on a Sunday. # If a rule applies to Weekdays, Saturdays, and/or Sundays, values will be applied in that order of precedence. # If a rule does not apply to any of these days, it is unused and will not be modified. # # @param schedule_ruleset [OpenStudio::Model::ScheduleRuleset] schedule ruleset object # @param wkdy_start_time [OpenStudio::Time] Weekday start time. If nil, no change will be made to this day. # @param wkdy_end_time [OpenStudio::Time] Weekday end time. If greater than 24:00, hours of operation will wrap over midnight. # @param sat_start_time [OpenStudio::Time] Saturday start time. If nil, no change will be made to this day. # @param sat_end_time [OpenStudio::Time] Saturday end time. If greater than 24:00, hours of operation will wrap over midnight. # @param sun_start_time [OpenStudio::Time] Sunday start time. If nil, no change will be made to this day. # @param sun_end_time [OpenStudio::Time] Sunday end time. If greater than 24:00, hours of operation will wrap over midnight. # @return [Bool] returns true if successful, false if not def schedule_ruleset_set_hours_of_operation(schedule_ruleset, wkdy_start_time: nil, wkdy_end_time: nil, sat_start_time: nil, sat_end_time: nil, sun_start_time: nil, sun_end_time: nil) # Default day is assumed to represent weekdays if wkdy_start_time && wkdy_end_time schedule_day_set_hours_of_operation(schedule_ruleset.defaultDaySchedule, wkdy_start_time, wkdy_end_time) # OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.ScheduleRuleset', "For #{schedule_ruleset.name}, set default operating hours to #{wkdy_start_time}-#{wkdy_end_time}.") end # Modify each rule schedule_ruleset.scheduleRules.each do |rule| if rule.applyMonday || rule.applyTuesday || rule.applyWednesday || rule.applyThursday || rule.applyFriday if wkdy_start_time && wkdy_end_time schedule_day_set_hours_of_operation(rule.daySchedule, wkdy_start_time, wkdy_end_time) # OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.ScheduleRuleset', "For #{schedule_ruleset.name}, set Saturday rule operating hours to #{wkdy_start_time}-#{wkdy_end_time}.") end elsif rule.applySaturday if sat_start_time && sat_end_time schedule_day_set_hours_of_operation(rule.daySchedule, sat_start_time, sat_end_time) # OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.ScheduleRuleset', "For #{schedule_ruleset.name}, set Saturday rule operating hours to #{sat_start_time}-#{sat_end_time}.") end elsif rule.applySunday if sun_start_time && sun_end_time schedule_day_set_hours_of_operation(rule.daySchedule, sun_start_time, sun_end_time) # OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.ScheduleRuleset', "For #{schedule_ruleset.name}, set Sunday rule operating hours to #{sun_start_time}-#{sun_end_time}.") end end end return true end private # Set the hours of operation (0 or 1) for a ScheduleDay. # Clears out existing time/value pairs and sets to supplied values. # # @author Andrew Parker # @param schedule_day [OpenStudio::Model::ScheduleDay] The day schedule to set. # @param start_time [OpenStudio::Time] Start time. # @param end_time [OpenStudio::Time] End time. If greater than 24:00, hours of operation will wrap over midnight. # # @return [Void] # @api private def schedule_day_set_hours_of_operation(schedule_day, start_time, end_time) schedule_day.clearValues twenty_four_hours = OpenStudio::Time.new(0, 24, 0, 0) if end_time < twenty_four_hours # Operating hours don't wrap over midnight schedule_day.addValue(start_time, 0) # 0 until start time schedule_day.addValue(end_time, 1) # 1 from start time until end time schedule_day.addValue(twenty_four_hours, 0) # 0 after end time else # Operating hours start on previous day schedule_day.addValue(end_time - twenty_four_hours, 1) # 1 for hours started on the previous day schedule_day.addValue(start_time, 0) # 0 from end of previous days hours until start of today's schedule_day.addValue(twenty_four_hours, 1) # 1 from start of today's hours until midnight end end # process individual schedule profiles # # @author David Goldwasser # @param sch_day [OpenStudio::Model::ScheduleDay] schedule day object # @param hoo_start [Double] hours of operation start # @param hoo_end [Double] hours of operation end # @param val_flr [Double] value floor # @param val_clg [Double] value ceiling # @param ramp_frequency [Double] ramp frequency in minutes # @param infer_hoo_for_non_assigned_objects [Bool] attempt to get hoo for objects like swh with and exterior lighting # @param error_on_out_of_order [Bool] true will error if applying formula creates out of order values # @return [OpenStudio::Model::ScheduleDay] schedule day # @api private def process_hrs_of_operation_hash(sch_day, hoo_start, hoo_end, val_flr, val_clg, ramp_frequency, infer_hoo_for_non_assigned_objects, error_on_out_of_order) # process hoo and floor/ceiling vars to develop formulas without variables formula_string = sch_day.additionalProperties.getFeatureAsString('param_day_profile').get formula_hash = {} formula_string.split('|').each do |time_val_valopt| a1 = time_val_valopt.to_s.split('~') time = a1[0] value_array = a1.drop(1) formula_hash[time] = value_array end # setup additional variables if hoo_end >= hoo_start occ = hoo_end - hoo_start else occ = 24.0 + hoo_end - hoo_start end vac = 24.0 - occ range = val_clg - val_flr # apply variables and create updated hash with only numbers formula_hash_var_free = {} formula_hash.each do |time, val_in_out| # replace time variables with value time = time.gsub('hoo_start', hoo_start.to_s) time = time.gsub('hoo_end', hoo_end.to_s) time = time.gsub('occ', occ.to_s) # can save special variables like lunch or break using this logic time = time.gsub('mid', (hoo_start + occ * 0.5).to_s) time = time.gsub('vac', vac.to_s) begin time_float = eval(time) if time_float.to_i.to_s == time_float.to_s || time_float.to_f.to_s == time_float.to_s # check to see if numeric time_float = time_float.to_f else OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.ScheduleRuleset', "Time formula #{time} for #{sch_day.name} is invalid. It can't be converted to a float.") end rescue SyntaxError => e OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.ScheduleRuleset', "Time formula #{time} for #{sch_day.name} is invalid. It can't be evaluated.") end # replace variables in array of values val_in_out_float = [] val_in_out.each do |val| # replace variables for values val = val.gsub('val_flr', val_flr.to_s) val = val.gsub('val_clg', val_clg.to_s) val = val.gsub('val_range', range.to_s) # will expect a fractional value and will scale within ceiling and floor begin val_float = eval(val) if val_float.to_i.to_s == val_float.to_s || val_float.to_f.to_s == val_float.to_s # check to see if numeric val_float = val_float.to_f else OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.ScheduleRuleset', "Value formula #{val_float} for #{sch_day.name} is invalid. It can't be converted to a float.") end rescue SyntaxError => e OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.ScheduleRuleset', "Time formula #{val_float} for #{sch_day.name} is invalid. It can't be evaluated.") end val_in_out_float << val_float end # update hash formula_hash_var_free[time_float] = val_in_out_float end # this is old variable used in loop, just combining for now to avoid refactor, may change this later time_value_pairs = [] formula_hash_var_free.each do |time, val_in_out| val_in_out.each do |val| time_value_pairs << [time, val] end end # re-order so first value is lowest, and last is highest (need to adjust so no negative or > 24 values first) neg_time_hash = {} temp_min_time_hash = {} time_value_pairs.each_with_index do |pair, i| # if value 24 add it to 24 so it will go on tail end of profile # case when value is greater than 24 can be left alone for now, will be addressed if pair[0] < 0.0 neg_time_hash[i] = pair[0] time = pair[0] + 24.0 time_value_pairs[i][0] = time else time = pair[0] end temp_min_time_hash[i] = pair[0] end time_value_pairs.rotate!(temp_min_time_hash.key(temp_min_time_hash.values.min)) # validate order, issue warning and correct if out of order last_time = nil throw_order_warning = false pre_fix_time_value_pairs = time_value_pairs.to_s time_value_pairs.each_with_index do |time_value_pair, i| if last_time.nil? last_time = time_value_pair[0] elsif time_value_pair[0] < last_time || neg_time_hash.key?(i) # @todo it doesn't actually stop here now if error_on_out_of_order OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.ScheduleRuleset', "Pre-interpolated processed hash for #{sch_day.name} has one or more out of order conflicts: #{pre_fix_time_value_pairs}. Method will stop because Error on Out of Order was set to true.") end if neg_time_hash.key?(i) orig_current_time = time_value_pair[0] updated_time = 0.0 last_buffer = 'NA' else # pick midpoint and put each time there. e.g. times of (2,7,9,8,11) would be changed to (2,7,8.5,8.5,11) delta = last_time - time_value_pair[0] # determine much space last item can move if i < 2 last_buffer = time_value_pairs[i - 1][0] # can move down to 0 without any issues else last_buffer = time_value_pairs[i - 1][0] - time_value_pairs[i - 2][0] end # center if possible but don't exceed available buffer updated_time = time_value_pairs[i - 1][0] - [delta / 2.0, last_buffer].min end # update values in array orig_current_time = time_value_pair[0] time_value_pairs[i - 1][0] = updated_time time_value_pairs[i][0] = updated_time # reporting mostly for diagnostic purposes OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.ScheduleRuleset', "For #{sch_day.name} profile item #{i} time was #{last_time} and item #{i + 1} time was #{orig_current_time}. Last buffer is #{last_buffer}. Changing both times to #{updated_time}.") last_time = updated_time throw_order_warning = true else last_time = time_value_pair[0] end end # issue warning if order was changed if throw_order_warning OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.ScheduleRuleset', "Pre-interpolated processed hash for #{sch_day.name} has one or more out of order conflicts: #{pre_fix_time_value_pairs}. Time values were adjusted as shown to crate a valid profile: #{time_value_pairs}") end # add interpolated values at ramp_frequency time_value_pairs.each_with_index do |time_value_pair, i| # store current and next time and value current_time = time_value_pair[0] current_value = time_value_pair[1] if i + 1 < time_value_pairs.size next_time = time_value_pairs[i + 1][0] next_value = time_value_pairs[i + 1][1] else # use time and value of first item next_time = time_value_pairs[0][0] + 24 # need to adjust values for beginning of array next_value = time_value_pairs[0][1] end step_delta = next_time - current_time # skip if time between values is 0 or less than ramp frequency next if step_delta <= ramp_frequency # skip if next value is same next if current_value == next_value # add interpolated value to array interpolated_time = current_time + ramp_frequency interpolated_value = next_value * (interpolated_time - current_time) / step_delta + current_value * (next_time - interpolated_time) / step_delta time_value_pairs.insert(i + 1, [interpolated_time, interpolated_value]) end # remove second instance of time when there are two time_values_used = [] items_to_remove = [] time_value_pairs.each_with_index do |time_value_pair, i| if time_values_used.include? time_value_pair[0] items_to_remove << i else time_values_used << time_value_pair[0] end end items_to_remove.reverse.each do |i| time_value_pairs.delete_at(i) end # if time is > 24 shift to front of array and adjust value rotate_steps = 0 time_value_pairs.reverse.each_with_index do |time_value_pair, i| if time_value_pair[0] > 24 rotate_steps -= 1 time_value_pair[0] -= 24 else next end end time_value_pairs.rotate!(rotate_steps) # add a 24 on the end of array that matches the first value if time_value_pairs.last[0] != 24.0 time_value_pairs << [24.0, time_value_pairs.first[1]] end # reset scheduleDay values based on interpolated values sch_day.clearValues time_value_pairs.each do |time_val| hour = time_val.first.floor min = ((time_val.first - hour) * 60.0).floor os_time = OpenStudio::Time.new(0, hour, min, 0) value = time_val.last sch_day.addValue(os_time, value) end # @todo apply secondary logic # Tell EnergyPlus to interpolate schedules to timestep so that it doesn't have to be done in this code sch_day.setInterpolatetoTimestep(true) return sch_day end end