lib/openstudio-standards/schedules/modify.rb in openstudio-standards-0.5.0 vs lib/openstudio-standards/schedules/modify.rb in openstudio-standards-0.6.0.rc1

- old
+ new

@@ -1,22 +1,173 @@ -# Methods to modify existing Schedule objects module OpenstudioStandards + # The Schedules module provides methods to create, modify, and get information about Schedule objects module Schedules - # @!group Modify + # Methods to modify existing Schedule objects - # Increase/decrease by percentage or static value + # @!group Modify:ScheduleDay + + # Method to multiply the values in a day schedule by a specified value + # The method can optionally apply the multiplier to only values above a lower limit. + # This limit prevents multipliers for things like occupancy sensors from affecting unoccupied hours. # + # @param schedule_day [OpenStudio::Model::ScheduleDay] OpenStudio ScheduleDay object + # @param multiplier [Double] value to multiply schedule values by + # @param lower_apply_limit [Double] apply the multiplier to only values above this value + # @return [OpenStudio::Model::ScheduleDay] OpenStudio ScheduleDay object + def self.schedule_day_multiply_by_value(schedule_day, multiplier, lower_apply_limit: nil) + # Record the original times and values + times = schedule_day.times + values = schedule_day.values + + # Remove the original times and values + schedule_day.clearValues + + # Create new values by using the multiplier on the original values + new_values = [] + values.each do |value| + if lower_apply_limit.nil? + new_values << value * multiplier + else + if value > lower_apply_limit + new_values << value * multiplier + else + new_values << value + end + end + end + + # Add the revised time/value pairs to the schedule + new_values.each_with_index do |new_value, i| + schedule_day.addValue(times[i], new_value) + end + + return schedule_day + end + + # 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 self.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 + + # Sets the values of a day schedule from an array of values + # Clears out existing time value pairs and sets to supplied values + # + # @param schedule_day [OpenStudio::Model::ScheduleDay] The day schedule to set. + # @param value_array [Array] Array of 24 values. Schedule times set based on value index. Identical values will be skipped. + # @return [OpenStudio::Model::ScheduleDay] + def self.schedule_day_populate_from_array_of_values(schedule_day, value_array) + schedule_day.clearValues + if value_array.size != 24 + OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Schedules.Modify', "#{__method__} expects value_array to contain 24 values, instead #{value_array.size} values were given. Resulting schedule will use first #{[24, value_array.size].min} values") + end + + value_array[0..23].each_with_index do |value, h| + next if value == value_array[h + 1] + + time = OpenStudio::Time.new(0, h + 1, 0, 0) + schedule_day.addValue(time, value) + end + return schedule_day + end + + # @!endgroup Modify:ScheduleDay + + # @!group Modify:ScheduleRuleset + + # Add a ScheduleRule to a ScheduleRuleset object from an array of hourly values + # # @param schedule_ruleset [OpenStudio::Model::ScheduleRuleset] OpenStudio ScheduleRuleset object + # @param start_date [OpenStudio::Date] start date of week period + # @param end_date [OpenStudio::Date] end date of week period + # @param day_names [Array<String>] list of days of week for which this day type is applicable + # @param values [Array<Double>] array of 24 hourly values for a day + # @param rule_name [String] rule ScheduleDay object name + # @return [OpenStudio::Model::ScheduleRule] OpenStudio ScheduleRule object + def self.schedule_ruleset_add_rule(schedule_ruleset, values, + start_date: nil, + end_date: nil, + day_names: nil, + rule_name: nil) + # create new schedule rule + sch_rule = OpenStudio::Model::ScheduleRule.new(schedule_ruleset) + day_sch = sch_rule.daySchedule + day_sch.setName(rule_name) unless rule_name.nil? + + # set the dates when the rule applies + sch_rule.setStartDate(start_date) unless start_date.nil? + sch_rule.setEndDate(end_date) unless end_date.nil? + + # set the days for which the rule applies + unless day_names.nil? + day_names.each do |day_of_week| + sch_rule.setApplySunday(true) if day_of_week == 'Sunday' + sch_rule.setApplyMonday(true) if day_of_week == 'Monday' + sch_rule.setApplyTuesday(true) if day_of_week == 'Tuesday' + sch_rule.setApplyWednesday(true) if day_of_week == 'Wednesday' + sch_rule.setApplyThursday(true) if day_of_week == 'Thursday' + sch_rule.setApplyFriday(true) if day_of_week == 'Friday' + sch_rule.setApplySaturday(true) if day_of_week == 'Saturday' + end + end + + # Create the day schedule and add hourly values + (0..23).each do |ihr| + next if values[ihr] == values[ihr + 1] + + day_sch.addValue(OpenStudio::Time.new(0, ihr + 1, 0, 0), values[ihr]) + end + + return sch_rule + end + + # Increase/decrease by percentage or static value. + # If the schedule has a scheduleTypeLimits object, the adjusted values will subject to the lower and upper bounds of the schedule type limits object. + # + # @param schedule_ruleset [OpenStudio::Model::ScheduleRuleset] OpenStudio ScheduleRuleset object # @param value [Double] Hash of name and time value pairs # @param modification_type [String] Options are 'Multiplier', which multiples by the value, # and 'Sum' which adds by the value # @return [OpenStudio::Model::ScheduleRuleset] OpenStudio ScheduleRuleset object # @todo add in design day adjustments, maybe as an optional argument # @todo provide option to clone existing schedule def self.schedule_ruleset_simple_value_adjust(schedule_ruleset, value, modification_type = 'Multiplier') # gather profiles profiles = [] + # positive infinity + upper_bound = Float::INFINITY + # negative infinity + lower_bound = -upper_bound + if schedule_ruleset.scheduleTypeLimits.is_initialized + schedule_type_limits = schedule_ruleset.scheduleTypeLimits.get + if schedule_type_limits.lowerLimitValue.is_initialized + lower_bound = schedule_type_limits.lowerLimitValue.get + end + if schedule_type_limits.upperLimitValue.is_initialized + upper_bound = schedule_type_limits.upperLimitValue.get + end + end default_profile = schedule_ruleset.to_ScheduleRuleset.get.defaultDaySchedule profiles << default_profile rules = schedule_ruleset.scheduleRules rules.each do |rule| profiles << rule.daySchedule @@ -28,14 +179,16 @@ i = 0 profile.values.each do |sch_value| case modification_type when 'Multiplier', 'Percentage' # percentage was used early on but Multiplier is preferable - profile.addValue(times[i], sch_value * value) + new_value = [lower_bound, [upper_bound, sch_value * value].min].max + profile.addValue(times[i], new_value) when 'Sum', 'Value' # value was used early on but Sum is preferable - profile.addValue(times[i], sch_value + value) + new_value = [lower_bound, [upper_bound, sch_value + value].min].max + profile.addValue(times[i], new_value) end i += 1 end end @@ -226,17 +379,15 @@ # array of all profiles to change profiles = [] # push default profiles to array if options['default'] - default_rule = schedule.defaultDaySchedule - profiles << default_rule + profiles << schedule.defaultDaySchedule end # push profiles to array - rules = schedule.scheduleRules - rules.each do |rule| + schedule.scheduleRules.each do |rule| day_sch = rule.daySchedule # if any day requested also exists in the rule, then it will be altered alter_rule = false if rule.applyMonday && rule.applyMonday == options['mon'] then alter_rule = true end @@ -254,16 +405,14 @@ end end # add design days to array if options['summer'] - summer_design = schedule.summerDesignDaySchedule - profiles << summer_design + profiles << schedule.summerDesignDaySchedule end if options['winter'] - winter_design = schedule.winterDesignDaySchedule - profiles << winter_design + profiles << schedule.winterDesignDaySchedule end # give info messages as I change specific profiles OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Schedules.Modify', "Adjusting #{schedule.name}") @@ -439,7 +588,153 @@ end end return schedule end + + # Remove unused profiles and set most prevalent profile as default. + # This method expands on the functionality of the RemoveUnusedDefaultProfiles measure. + # + # @author David Goldwasser + # @param schedule_ruleset [OpenStudio::Model::ScheduleRuleset] OpenStudio ScheduleRuleset object + # @return [OpenStudio::Model::ScheduleRuleset] OpenStudio ScheduleRuleset object + # @todo There are potential issues with overlapping rule dates or days of week when setting a profile that isn't the lowest priority as the default day. + def self.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.Schedules.Modify', "#{schedule_ruleset.name} does not use 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_day_schedule = rule_vector[new_default_index].daySchedule + new_default_day_schedule_values = new_default_day_schedule.values + new_default_day_schedule_times = new_default_day_schedule.times + + # update values and times for default profile + for i in 0..(new_default_day_schedule_values.size - 1) + old_default_schedule_day.addValue(new_default_day_schedule_times[i], new_default_day_schedule_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 + + # creates a minimal set of ScheduleRules that applies to all days in a given array of day of year indices + # + # @param schedule_ruleset [OpenStudio::Model::ScheduleRuleset] + # @param days_used [Array] array of day of year integers + # @param schedule_day [OpenStudio::Model::ScheduleDay] optional day schedule to apply to new rule. A new default schedule will be created for each rule if nil + # @return [Array] + def self.schedule_ruleset_create_rules_from_day_list(schedule_ruleset, days_used, schedule_day: nil) + # get year from schedule_ruleset + year = schedule_ruleset.model.getYearDescription.assumedYear + + # split day_used into sub arrays of consecutive days + consec_days = days_used.chunk_while { |i, j| i + 1 == j }.to_a + + # split consec_days into sub arrays of consecutive weeks by checking that any value in next array differs by seven from a value in this array + consec_weeks = consec_days.chunk_while { |i, j| i.product(j).any? { |x, y| (x - y).abs == 7 } }.to_a + + # make new rule for blocks of consectutive weeks + rules = [] + consec_weeks.each do |week_group| + if schedule_day.nil? + OpenStudio.logFree(OpenStudio::Debug, 'openstudio.standards.Parametric.ScheduleRuleset', 'Creating new Rule Schedule from days_used vector with new Day Schedule') + rule = OpenStudio::Model::ScheduleRule.new(schedule_ruleset) + else + OpenStudio.logFree(OpenStudio::Debug, 'openstudio.standards.Parametric.ScheduleRuleset', "Creating new Rule Schedule from days_used vector with clone of Day Schedule: #{schedule_day.name.get}") + rule = OpenStudio::Model::ScheduleRule.new(schedule_ruleset, schedule_day) + end + + # set day types and dates + dates = week_group.flatten.map { |d| OpenStudio::Date.fromDayOfYear(d, year) } + day_types = dates.map { |date| date.dayOfWeek.valueName }.uniq + day_types.each { |type| rule.send("setApply#{type}", true) } + rule.setStartDate(dates.min) + rule.setEndDate(dates.max) + + rules << rule + end + + return rules + end + + # @!endgroup Modify:ScheduleRuleset end end