example_files/resources/hpxml-measures/HPXMLtoOpenStudio/resources/schedules.rb in urbanopt-cli-0.7.1 vs example_files/resources/hpxml-measures/HPXMLtoOpenStudio/resources/schedules.rb in urbanopt-cli-0.8.0

- old
+ new

@@ -1091,83 +1091,161 @@ fail "Invalid date format specified for '#{date_range}'." end return begin_month, begin_day, end_month, end_day end + + def self.schedules_file_includes_col_name(schedules_file, col_name) + schedules_file_includes_col_name = false + if not schedules_file.nil? + if schedules_file.schedules.keys.include?(col_name) + schedules_file_includes_col_name = true + end + end + return schedules_file_includes_col_name + end end class SchedulesFile + # Constants + ColumnOccupants = 'occupants' + ColumnLightingInterior = 'lighting_interior' + ColumnLightingExterior = 'lighting_exterior' + ColumnLightingGarage = 'lighting_garage' + ColumnLightingExteriorHoliday = 'lighting_exterior_holiday' + ColumnCookingRange = 'cooking_range' + ColumnRefrigerator = 'refrigerator' + ColumnExtraRefrigerator = 'extra_refrigerator' + ColumnFreezer = 'freezer' + ColumnDishwasher = 'dishwasher' + ColumnClothesWasher = 'clothes_washer' + ColumnClothesDryer = 'clothes_dryer' + ColumnCeilingFan = 'ceiling_fan' + ColumnPlugLoadsOther = 'plug_loads_other' + ColumnPlugLoadsTV = 'plug_loads_tv' + ColumnPlugLoadsVehicle = 'plug_loads_vehicle' + ColumnPlugLoadsWellPump = 'plug_loads_well_pump' + ColumnFuelLoadsGrill = 'fuel_loads_grill' + ColumnFuelLoadsLighting = 'fuel_loads_lighting' + ColumnFuelLoadsFireplace = 'fuel_loads_fireplace' + ColumnPoolPump = 'pool_pump' + ColumnPoolHeater = 'pool_heater' + ColumnHotTubPump = 'hot_tub_pump' + ColumnHotTubHeater = 'hot_tub_heater' + ColumnHotWaterDishwasher = 'hot_water_dishwasher' + ColumnHotWaterClothesWasher = 'hot_water_clothes_washer' + ColumnHotWaterFixtures = 'hot_water_fixtures' + ColumnVacancy = 'vacancy' + ColumnHeatingSetpoint = 'heating_setpoint' + ColumnCoolingSetpoint = 'cooling_setpoint' + ColumnWaterHeaterSetpoint = 'water_heater_setpoint' + ColumnWaterHeaterOperatingMode = 'water_heater_operating_mode' + def initialize(runner: nil, model: nil, - year: nil, - schedules_path:, - **remainder) + schedules_paths:, + col_names:) + return if schedules_paths.empty? @runner = runner @model = model - @year = year - @schedules_path = schedules_path + @schedules_paths = schedules_paths - import(col_names: Constants.ScheduleColNames.keys) + import(col_names: col_names) @tmp_schedules = Marshal.load(Marshal.dump(@schedules)) set_vacancy + convert_setpoints - tmpfile = Tempfile.new(['schedules', '.csv']) - @tmp_schedules_path = tmpfile.path.to_s - export + tmpdir = Dir.tmpdir + tmpdir = ENV['LOCAL_SCRATCH'] if ENV.keys.include?('LOCAL_SCRATCH') + tmpfile = Tempfile.new(['schedules', '.csv'], tmpdir) + tmp_schedules_path = tmpfile.path.to_s - get_external_file + export(tmp_schedules_path) + + get_external_file(tmp_schedules_path) end + def nil? + if @schedules.nil? + return true + end + + return false + end + def import(col_names:) @schedules = {} - columns = CSV.read(@schedules_path).transpose - columns.each do |col| - unless col_names.include? col[0] - fail "Schedule column name '#{col[0]}' is invalid. [context: #{@schedules_path}]" - end + @schedules_paths.each do |schedules_path| + columns = CSV.read(schedules_path).transpose + columns.each do |col| + col_name = col[0] + unless col_names.include? col_name + fail "Schedule column name '#{col_name}' is invalid. [context: #{schedules_path}]" unless [SchedulesFile::ColumnVacancy].include?(col_name) + end - values = col[1..-1].reject { |v| v.nil? } - values = validate_schedule(col_name: col[0], values: values) - @schedules[col[0]] = values + values = col[1..-1].reject { |v| v.nil? } + + begin + values = values.map { |v| Float(v) } + rescue ArgumentError + fail "Schedule value must be numeric for column '#{col_name}'. [context: #{schedules_path}]" + end + + if @schedules.keys.include? col_name + fail "Schedule column name '#{col_name}' is duplicated. [context: #{schedules_path}]" + end + + @schedules[col_name] = values + end end end - def validate_schedule(col_name:, - values:) - + def validate_schedules(year:) + @year = year num_hrs_in_year = Constants.NumHoursInYear(@year) - schedule_length = values.length - begin - values = values.map { |v| Float(v) } - rescue ArgumentError - fail "Schedule value must be numeric for column '#{col_name}'. [context: #{@schedules_path}]" - end + @schedules_paths.each do |schedules_path| + columns = CSV.read(schedules_path).transpose + columns.each do |col| + col_name = col[0] + values = col[1..-1].reject { |v| v.nil? } + values = values.map { |v| Float(v) } + schedule_length = values.length - if (1.0 - values.max).abs > 0.01 - fail "Schedule max value for column '#{col_name}' must be 1. [context: #{@schedules_path}]" - end + if max_value_one[col_name] + if values.max > 1 + fail "Schedule max value for column '#{col_name}' must be 1. [context: #{schedules_path}]" + end + end - if values.min < 0 - fail "Schedule min value for column '#{col_name}' must be non-negative. [context: #{@schedules_path}]" - end + if min_value_zero[col_name] + if values.min < 0 + fail "Schedule min value for column '#{col_name}' must be non-negative. [context: #{schedules_path}]" + end + end - valid_minutes_per_item = [1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60] - valid_num_rows = valid_minutes_per_item.map { |min_per_item| (60.0 * num_hrs_in_year / min_per_item).to_i } - unless valid_num_rows.include? schedule_length - fail "Schedule has invalid number of rows (#{schedule_length}) for column '#{col_name}'. Must be one of: #{valid_num_rows.reverse.join(', ')}. [context: #{@schedules_path}]" - end + if only_zeros_and_ones[col_name] + if values.any? { |v| v != 0 && v != 1 } + fail "Schedule value for column '#{col_name}' must be either 0 or 1. [context: #{schedules_path}]" + end + end - return values + valid_minutes_per_item = [1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60] + valid_num_rows = valid_minutes_per_item.map { |min_per_item| (60.0 * num_hrs_in_year / min_per_item).to_i } + unless valid_num_rows.include? schedule_length + fail "Schedule has invalid number of rows (#{schedule_length}) for column '#{col_name}'. Must be one of: #{valid_num_rows.reverse.join(', ')}. [context: #{@schedules_path}]" + end + end + end end - def export - return false if @tmp_schedules_path.nil? + def export(tmp_schedules_path) + return false if tmp_schedules_path.nil? - CSV.open(@tmp_schedules_path, 'wb') do |csv| + CSV.open(tmp_schedules_path, 'wb') do |csv| csv << @tmp_schedules.keys rows = @tmp_schedules.values.transpose rows.each do |row| csv << row end @@ -1187,11 +1265,16 @@ def external_file return @external_file end def get_col_index(col_name:) - headers = CSV.open(@schedules_path, 'r') { |csv| csv.first } + headers = [] + @schedules_paths.each do |schedules_path| + next if schedules_path.nil? + + headers += CSV.open(schedules_path, 'r') { |csv| csv.first } + end col_num = headers.index(col_name) return col_num end def create_schedule_file(col_name:, @@ -1201,12 +1284,11 @@ return schedule_file end if @schedules[col_name].nil? - @runner.registerError("Could not find the '#{col_name}' schedule.") - return false + return end col_index = get_col_index(col_name: col_name) num_hrs_in_year = Constants.NumHoursInYear(@year) schedule_length = @schedules[col_name].length @@ -1223,10 +1305,14 @@ end # the equivalent number of hours in the year, if the schedule was at full load (1.0) def annual_equivalent_full_load_hrs(col_name:, schedules: nil) + if @schedules[col_name].nil? + return + end + if schedules.nil? schedules = @schedules # the schedules before vacancy is applied end num_hrs_in_year = Constants.NumHoursInYear(@year) @@ -1240,20 +1326,26 @@ # the power in watts the equipment needs to consume so that, if it were to run annual_equivalent_full_load_hrs hours, # it would consume the annual_kwh energy in the year. Essentially, returns the watts for the equipment when schedule # is at 1.0, so that, for the given schedule values, the equipment will consume annual_kwh energy in a year. def calc_design_level_from_annual_kwh(col_name:, annual_kwh:) + if @schedules[col_name].nil? + return + end ann_equiv_full_load_hrs = annual_equivalent_full_load_hrs(col_name: col_name) design_level = annual_kwh * 1000.0 / ann_equiv_full_load_hrs # W return design_level end # Similar to ann_equiv_full_load_hrs, but for thermal energy def calc_design_level_from_annual_therm(col_name:, annual_therm:) + if @schedules[col_name].nil? + return + end annual_kwh = UnitConversions.convert(annual_therm, 'therm', 'kWh') design_level = calc_design_level_from_annual_kwh(col_name: col_name, annual_kwh: annual_kwh) return design_level @@ -1261,49 +1353,194 @@ # similar to the calc_design_level_from_annual_kwh, but use daily_kwh instead of annual_kwh to calculate the design # level def calc_design_level_from_daily_kwh(col_name:, daily_kwh:) + if @schedules[col_name].nil? + return + end + full_load_hrs = annual_equivalent_full_load_hrs(col_name: col_name) num_days_in_year = Constants.NumDaysInYear(@year) daily_full_load_hrs = full_load_hrs / num_days_in_year design_level = UnitConversions.convert(daily_kwh / daily_full_load_hrs, 'kW', 'W') return design_level end # similar to calc_design_level_from_daily_kwh but for water usage - def calc_peak_flow_from_daily_gpm(col_name:, daily_water:) + def calc_peak_flow_from_daily_gpm(col_name:, + daily_water:) + if @schedules[col_name].nil? + return + end + ann_equiv_full_load_hrs = annual_equivalent_full_load_hrs(col_name: col_name) num_days_in_year = Constants.NumDaysInYear(@year) daily_full_load_hrs = ann_equiv_full_load_hrs / num_days_in_year peak_flow = daily_water / daily_full_load_hrs # gallons_per_hour peak_flow /= 60 # convert to gallons per minute peak_flow = UnitConversions.convert(peak_flow, 'gal/min', 'm^3/s') # convert to m^3/s return peak_flow end - def get_external_file - if File.exist? @tmp_schedules_path - @external_file = OpenStudio::Model::ExternalFile::getExternalFile(@model, @tmp_schedules_path) + def get_external_file(tmp_schedules_path) + if File.exist? tmp_schedules_path + @external_file = OpenStudio::Model::ExternalFile::getExternalFile(@model, tmp_schedules_path) if @external_file.is_initialized @external_file = @external_file.get + # ExternalFile creates a new file, so delete our temporary one immediately if we can + begin + File.delete(tmp_schedules_path) + rescue + end end end end def set_vacancy - return unless @tmp_schedules.keys.include? 'vacancy' - return if @tmp_schedules['vacancy'].all? { |i| i == 0 } + return unless @tmp_schedules.keys.include? ColumnVacancy + return if @tmp_schedules[ColumnVacancy].all? { |i| i == 0 } - col_names = Constants.ScheduleColNames + col_names = SchedulesFile.ColumnNames - @tmp_schedules[col_names.keys[0]].each_with_index do |ts, i| - col_names.keys.each do |col_name| - next if col_names[col_name].nil? - next unless col_names[col_name] # skip those unaffected by vacancy + @tmp_schedules[col_names[0]].each_with_index do |ts, i| + col_names.each do |col_name| + next unless affected_by_vacancy[col_name] # skip those unaffected by vacancy - @tmp_schedules[col_name][i] *= (1.0 - @tmp_schedules['vacancy'][i]) + @tmp_schedules[col_name][i] *= (1.0 - @tmp_schedules[ColumnVacancy][i]) end end + end + + def convert_setpoints + return if @tmp_schedules.keys.none? { |k| SchedulesFile.SetpointColumnNames.include?(k) } + + col_names = @tmp_schedules.keys + + @tmp_schedules[col_names[0]].each_with_index do |ts, i| + SchedulesFile.SetpointColumnNames.each do |setpoint_col_name| + next unless col_names.include?(setpoint_col_name) + + @tmp_schedules[setpoint_col_name][i] = UnitConversions.convert(@tmp_schedules[setpoint_col_name][i], 'f', 'c') + end + end + end + + def self.ColumnNames + return SchedulesFile.OccupancyColumnNames + SchedulesFile.HVACSetpointColumnNames + SchedulesFile.WaterHeaterColumnNames + end + + def self.OccupancyColumnNames + return [ + ColumnOccupants, + ColumnLightingInterior, + ColumnLightingExterior, + ColumnLightingGarage, + ColumnLightingExteriorHoliday, + ColumnCookingRange, + ColumnRefrigerator, + ColumnExtraRefrigerator, + ColumnFreezer, + ColumnDishwasher, + ColumnClothesWasher, + ColumnClothesDryer, + ColumnCeilingFan, + ColumnPlugLoadsOther, + ColumnPlugLoadsTV, + ColumnPlugLoadsVehicle, + ColumnPlugLoadsWellPump, + ColumnFuelLoadsGrill, + ColumnFuelLoadsLighting, + ColumnFuelLoadsFireplace, + ColumnPoolPump, + ColumnPoolHeater, + ColumnHotTubPump, + ColumnHotTubHeater, + ColumnHotWaterDishwasher, + ColumnHotWaterClothesWasher, + ColumnHotWaterFixtures + ] + end + + def self.HVACSetpointColumnNames + return [ + ColumnHeatingSetpoint, + ColumnCoolingSetpoint + ] + end + + def self.WaterHeaterColumnNames + return [ + ColumnWaterHeaterSetpoint, + ColumnWaterHeaterOperatingMode + ] + end + + def self.SetpointColumnNames + return [ + ColumnHeatingSetpoint, + ColumnCoolingSetpoint, + ColumnWaterHeaterSetpoint + ] + end + + def self.OperatingModeColumnNames + return [ + ColumnWaterHeaterOperatingMode + ] + end + + def affected_by_vacancy + affected_by_vacancy = {} + column_names = SchedulesFile.ColumnNames + column_names.each do |column_name| + affected_by_vacancy[column_name] = true + next unless ([ColumnRefrigerator, + ColumnExtraRefrigerator, + ColumnFreezer, + ColumnPoolPump, + ColumnPoolHeater, + ColumnHotTubPump, + ColumnHotTubHeater] + SchedulesFile.HVACSetpointColumnNames + SchedulesFile.WaterHeaterColumnNames).include? column_name + + affected_by_vacancy[column_name] = false + end + return affected_by_vacancy + end + + def max_value_one + max_value_one = {} + column_names = SchedulesFile.ColumnNames + column_names.each do |column_name| + max_value_one[column_name] = true + if SchedulesFile.SetpointColumnNames.include?(column_name) || SchedulesFile.OperatingModeColumnNames.include?(column_name) + max_value_one[column_name] = false + end + end + return max_value_one + end + + def min_value_zero + min_value_zero = {} + column_names = SchedulesFile.ColumnNames + column_names.each do |column_name| + min_value_zero[column_name] = true + if SchedulesFile.SetpointColumnNames.include?(column_name) || SchedulesFile.OperatingModeColumnNames.include?(column_name) + min_value_zero[column_name] = false + end + end + return min_value_zero + end + + def only_zeros_and_ones + only_zeros_and_ones = { SchedulesFile::ColumnVacancy => true } + column_names = SchedulesFile.ColumnNames + column_names.each do |column_name| + only_zeros_and_ones[column_name] = false + if SchedulesFile.OperatingModeColumnNames.include?(column_name) + only_zeros_and_ones[column_name] = true + end + end + return only_zeros_and_ones end end