lib/to_openstudio/hvac/radiant.rb in honeybee-openstudio-2.31.0 vs lib/to_openstudio/hvac/radiant.rb in honeybee-openstudio-2.31.1

- old
+ new

@@ -52,24 +52,32 @@ # get the radiant hot water temperature based on the climate zone case climate_zone when '0', '1' radiant_htg_dsgn_sup_wtr_temp_f = 90.0 + cz_mult = 2 when '2', '2A', '2B' radiant_htg_dsgn_sup_wtr_temp_f = 100.0 + cz_mult = 2 when '3', '3A', '3B', '3C' radiant_htg_dsgn_sup_wtr_temp_f = 100.0 + cz_mult = 3 when '4', '4A', '4B', '4C' radiant_htg_dsgn_sup_wtr_temp_f = 100.0 + cz_mult = 4 when '5', '5A', '5B', '5C' radiant_htg_dsgn_sup_wtr_temp_f = 110.0 + cz_mult = 4 when '6', '6A', '6B' radiant_htg_dsgn_sup_wtr_temp_f = 120.0 + cz_mult = 4 when '7', '8' radiant_htg_dsgn_sup_wtr_temp_f = 120.0 + cz_mult = 5 else # unrecognized climate zone; default to climate zone 4 radiant_htg_dsgn_sup_wtr_temp_f = 100.0 + cz_mult = 4 end # create the hot water loop if system_type.include? 'ASHP' boiler_fuel_type = 'ASHP' @@ -118,15 +126,10 @@ dsgn_sup_wtr_temp: 55.0, dsgn_sup_wtr_temp_delt: 5.0) end # get the various controls for the radiant system - if rad_props[:proportional_gain] - proportional_gain = rad_props[:proportional_gain] - else - proportional_gain = 0.3 - end if rad_props[:minimum_operation_time] minimum_operation = rad_props[:minimum_operation_time] else minimum_operation = 1 end @@ -134,31 +137,402 @@ switch_over_time = rad_props[:switch_over_time] else switch_over_time = 24 end + # get the start and end hour from the input zones + start_hour, end_hour = start_end_hour_from_zones_occupancy(zones) + # add radiant system to the conditioned zones include_carpet = false - if rad_props[:radiant_face_type] - radiant_type = rad_props[:radiant_face_type].downcase + control_strategy = 'proportional_control' + if rad_props[:radiant_type] + radiant_type = rad_props[:radiant_type].downcase if radiant_type == 'floorwithcarpet' radiant_type = 'floor' include_carpet = true + elsif radiant_type == 'ceilingmetalpanel' + control_strategy = 'default' end else radiant_type = 'floor' end - radiant_loops = std.model_add_low_temp_radiant( - self, conditioned_zones, hot_water_loop, chilled_water_loop, + + radiant_loops = model_add_low_temp_radiant( + std, conditioned_zones, hot_water_loop, chilled_water_loop, radiant_type: radiant_type, + control_strategy: control_strategy, include_carpet: include_carpet, - proportional_gain: proportional_gain, + model_occ_hr_start: start_hour, + model_occ_hr_end: end_hour, minimum_operation: minimum_operation, - switch_over_time: switch_over_time) + switch_over_time: switch_over_time, + cz_mult: cz_mult) # if the equipment includes a DOAS, then add it if system_type.include? 'DOAS_' std.model_add_doas(self, conditioned_zones) end end + + # get the start and end hour from the occupancy schedules of thermal zones + def start_end_hour_from_zones_occupancy(thermal_zones, threshold: 0.1) + # set the default start and end hour in the event there's no occupancy + start_hour = 12 + end_hour = 12 + # loop through the occupancy schedules and get the lowest start hour; highest end hour + thermal_zones.each do |zone| + zone.spaces.each do |space| + # gather all of the people objects assigned to the sapce + peoples = [] + unless space.spaceType.empty? + space_type = space.spaceType.get + unless space_type.people.empty? + space_type.people.each do |ppl| + peoples << ppl + end + end + end + space.people.each do |ppl| + peoples << ppl + end + # loop through the pople and gather all occupancy schedules + peoples.each do |people| + occupancy_sch_opt = people.numberofPeopleSchedule + unless occupancy_sch_opt.empty? + occupancy_sch = occupancy_sch_opt.get + if occupancy_sch.to_ScheduleRuleset.is_initialized + occupancy_sch = occupancy_sch.to_ScheduleRuleset.get + # gather all of the day schedules across the schedule ruleset + schedule_days, day_ids = [], [] + required_days = [ + occupancy_sch.defaultDaySchedule, + occupancy_sch.summerDesignDaySchedule, + occupancy_sch.winterDesignDaySchedule, + occupancy_sch.holidaySchedule + ] + required_days.each do |day_sch| + unless day_ids.include? day_sch.nameString + schedule_days << day_sch + day_ids << day_sch.nameString + end + end + occupancy_sch.scheduleRules.each do |schedule_rule| + day_sch = schedule_rule.daySchedule + unless day_ids.include? day_sch.nameString + schedule_days << day_sch + day_ids << day_sch.nameString + end + end + # loop through the day schedules and see if the start and end hours should be changed + schedule_days.each do |day_sch| + time_until = [1] + day_sch.times.each do |time| + time_until << time.hours + end + final_time = time_until[-2] + day_sch.values.zip(time_until).each do |value, time| + if value > threshold + if time < start_hour + start_hour = time + end + if time > end_hour + end_hour = time + end + if time == final_time + start_hour = 1 + end_hour = 24 + end + end + end + end + end + end + end + end + end + # if no values were set, just set the system to be on all of the time + if start_hour == 12 or start_hour == 0 + start_hour = 1 + end + if end_hour == 12 + end_hour = 24 + end + return start_hour, end_hour + end + + def model_add_low_temp_radiant(std, + thermal_zones, + hot_water_loop, + chilled_water_loop, + radiant_type: 'floor', + include_carpet: true, + carpet_thickness_in: 0.25, + model_occ_hr_start: 1.0, + model_occ_hr_end: 24.0, + control_strategy: 'proportional_control', + proportional_gain: 0.3, + minimum_operation: 1, + weekend_temperature_reset: 2, + early_reset_out_arg: 20, + switch_over_time: 24.0, + cz_mult: 4) + + # create internal source constructions for surfaces + OpenStudio.logFree(OpenStudio::Warn, 'openstudio.Model.Model', "Replacing constructions with new radiant slab constructions.") + + # create materials + mat_concrete_3_5in = OpenStudio::Model::StandardOpaqueMaterial.new(self, 'MediumRough', 0.0889, 2.31, 2322, 832) + mat_concrete_3_5in.setName('Radiant Slab Concrete - 3.5 in.') + mat_concrete_1_5in = OpenStudio::Model::StandardOpaqueMaterial.new(self, 'MediumRough', 0.0381, 2.31, 2322, 832) + mat_concrete_1_5in.setName('Radiant Slab Concrete - 1.5 in') + + metal_mat = OpenStudio::Model::StandardOpaqueMaterial.new(self, 'MediumSmooth', 0.003175, 30, 7680, 418) + metal_mat.setName('Radiant Metal Layer - 0.125 in') + air_gap_map = OpenStudio::Model::MasslessOpaqueMaterial.new(self, 'Smooth', 0.004572) + air_gap_map.setName('Generic Ceiling Air Gap - R 0.025') + + mat_refl_roof_membrane = self.getStandardOpaqueMaterialByName('Roof Membrane - Highly Reflective') + if mat_refl_roof_membrane.is_initialized + mat_refl_roof_membrane = self.getStandardOpaqueMaterialByName('Roof Membrane - Highly Reflective').get + else + mat_refl_roof_membrane = OpenStudio::Model::StandardOpaqueMaterial.new(self, 'VeryRough', 0.0095, 0.16, 1121.29, 1460) + mat_refl_roof_membrane.setThermalAbsorptance(0.75) + mat_refl_roof_membrane.setSolarAbsorptance(0.45) + mat_refl_roof_membrane.setVisibleAbsorptance(0.7) + mat_refl_roof_membrane.setName('Roof Membrane - Highly Reflective') + end + + if include_carpet + carpet_thickness_m = OpenStudio.convert(carpet_thickness_in / 12.0, 'ft', 'm').get + conductivity_si = 0.06 + conductivity_ip = OpenStudio.convert(conductivity_si, 'W/m*K', 'Btu*in/hr*ft^2*R').get + r_value_ip = carpet_thickness_in * (1 / conductivity_ip) + mat_thin_carpet_tile = OpenStudio::Model::StandardOpaqueMaterial.new(self, 'MediumRough', carpet_thickness_m, conductivity_si, 288, 1380) + mat_thin_carpet_tile.setThermalAbsorptance(0.9) + mat_thin_carpet_tile.setSolarAbsorptance(0.7) + mat_thin_carpet_tile.setVisibleAbsorptance(0.8) + mat_thin_carpet_tile.setName("Radiant Slab Thin Carpet Tile R-#{r_value_ip.round(2)}") + end + + # set exterior slab insulation thickness based on climate zone + slab_insulation_thickness_m = 0.0254 * cz_mult + mat_slab_insulation = OpenStudio::Model::StandardOpaqueMaterial.new(self, 'Rough', slab_insulation_thickness_m, 0.02, 56.06, 1210) + mat_slab_insulation.setName("Radiant Ground Slab Insulation - #{cz_mult} in.") + + ext_insulation_thickness_m = 0.0254 * (cz_mult + 1) + mat_ext_insulation = OpenStudio::Model::StandardOpaqueMaterial.new(self, 'Rough', ext_insulation_thickness_m, 0.02, 56.06, 1210) + mat_ext_insulation.setName("Radiant Exterior Slab Insulation - #{cz_mult + 1} in.") + + roof_insulation_thickness_m = 0.0254 * (cz_mult + 1) * 2 + mat_roof_insulation = OpenStudio::Model::StandardOpaqueMaterial.new(self, 'Rough', roof_insulation_thickness_m, 0.02, 56.06, 1210) + mat_roof_insulation.setName("Radiant Exterior Ceiling Insulation - #{(cz_mult + 1) * 2} in.") + + # create radiant internal source constructions + OpenStudio.logFree(OpenStudio::Warn, 'openstudio.Model.Model', 'New constructions exclude the metal deck, as high thermal diffusivity materials cause errors in EnergyPlus internal source construction calculations.') + + layers = [] + layers << mat_slab_insulation + layers << mat_concrete_3_5in + layers << mat_concrete_1_5in + layers << mat_thin_carpet_tile if include_carpet + radiant_ground_slab_construction = OpenStudio::Model::ConstructionWithInternalSource.new(layers) + radiant_ground_slab_construction.setName('Radiant Ground Slab Construction') + radiant_ground_slab_construction.setSourcePresentAfterLayerNumber(2) + radiant_ground_slab_construction.setTemperatureCalculationRequestedAfterLayerNumber(3) + radiant_ground_slab_construction.setTubeSpacing(0.2286) # 9 inches + + layers = [] + layers << mat_ext_insulation + layers << mat_concrete_3_5in + layers << mat_concrete_1_5in + layers << mat_thin_carpet_tile if include_carpet + radiant_exterior_slab_construction = OpenStudio::Model::ConstructionWithInternalSource.new(layers) + radiant_exterior_slab_construction.setName('Radiant Exterior Slab Construction') + radiant_exterior_slab_construction.setSourcePresentAfterLayerNumber(2) + radiant_exterior_slab_construction.setTemperatureCalculationRequestedAfterLayerNumber(3) + radiant_exterior_slab_construction.setTubeSpacing(0.2286) # 9 inches + + layers = [] + layers << mat_concrete_3_5in + layers << mat_concrete_1_5in + layers << mat_thin_carpet_tile if include_carpet + radiant_interior_floor_slab_construction = OpenStudio::Model::ConstructionWithInternalSource.new(layers) + radiant_interior_floor_slab_construction.setName('Radiant Interior Floor Slab Construction') + radiant_interior_floor_slab_construction.setSourcePresentAfterLayerNumber(1) + radiant_interior_floor_slab_construction.setTemperatureCalculationRequestedAfterLayerNumber(2) + radiant_interior_floor_slab_construction.setTubeSpacing(0.2286) # 9 inches + + layers = [] + layers << mat_thin_carpet_tile if include_carpet + layers << mat_concrete_3_5in + layers << mat_concrete_1_5in + radiant_interior_ceiling_slab_construction = OpenStudio::Model::ConstructionWithInternalSource.new(layers) + radiant_interior_ceiling_slab_construction.setName('Radiant Interior Ceiling Slab Construction') + slab_src_loc = include_carpet ? 2 : 1 + radiant_interior_ceiling_slab_construction.setSourcePresentAfterLayerNumber(slab_src_loc) + radiant_interior_ceiling_slab_construction.setTemperatureCalculationRequestedAfterLayerNumber(slab_src_loc + 1) + radiant_interior_ceiling_slab_construction.setTubeSpacing(0.2286) # 9 inches + + layers = [] + layers << mat_refl_roof_membrane + layers << mat_roof_insulation + layers << mat_concrete_3_5in + layers << mat_concrete_1_5in + radiant_ceiling_slab_construction = OpenStudio::Model::ConstructionWithInternalSource.new(layers) + radiant_ceiling_slab_construction.setName('Radiant Exterior Ceiling Slab Construction') + radiant_ceiling_slab_construction.setSourcePresentAfterLayerNumber(3) + radiant_ceiling_slab_construction.setTemperatureCalculationRequestedAfterLayerNumber(4) + radiant_ceiling_slab_construction.setTubeSpacing(0.2286) # 9 inches + + layers = [] + layers << mat_concrete_3_5in + layers << air_gap_map + layers << metal_mat + layers << metal_mat + radiant_interior_ceiling_metal_construction = OpenStudio::Model::ConstructionWithInternalSource.new(layers) + radiant_interior_ceiling_metal_construction.setName('Radiant Interior Ceiling Metal Construction') + radiant_interior_ceiling_metal_construction.setSourcePresentAfterLayerNumber(3) + radiant_interior_ceiling_metal_construction.setTemperatureCalculationRequestedAfterLayerNumber(4) + radiant_interior_ceiling_metal_construction.setTubeSpacing(0.1524) # 6 inches + + layers = [] + layers << mat_refl_roof_membrane + layers << mat_roof_insulation + layers << mat_concrete_3_5in + layers << air_gap_map + layers << metal_mat + layers << metal_mat + radiant_ceiling_metal_construction = OpenStudio::Model::ConstructionWithInternalSource.new(layers) + radiant_ceiling_metal_construction.setName('Radiant Ceiling Metal Construction') + radiant_ceiling_metal_construction.setSourcePresentAfterLayerNumber(5) + radiant_ceiling_metal_construction.setTemperatureCalculationRequestedAfterLayerNumber(6) + radiant_ceiling_metal_construction.setTubeSpacing(0.1524) # 6 inches + + # default temperature controls for radiant system + zn_radiant_htg_dsgn_temp_f = 68.0 + zn_radiant_htg_dsgn_temp_c = OpenStudio.convert(zn_radiant_htg_dsgn_temp_f, 'F', 'C').get + zn_radiant_clg_dsgn_temp_f = 74.0 + zn_radiant_clg_dsgn_temp_c = OpenStudio.convert(zn_radiant_clg_dsgn_temp_f, 'F', 'C').get + + htg_control_temp_sch = std.model_add_constant_schedule_ruleset( + self, + zn_radiant_htg_dsgn_temp_c, + name = "Zone Radiant Loop Heating Threshold Temperature Schedule - #{zn_radiant_htg_dsgn_temp_f.round(0)}F") + clg_control_temp_sch = std.model_add_constant_schedule_ruleset( + self, + zn_radiant_clg_dsgn_temp_c, + name = "Zone Radiant Loop Cooling Threshold Temperature Schedule - #{zn_radiant_clg_dsgn_temp_f.round(0)}F") + throttling_range_f = 4.0 # 2 degF on either side of control temperature + throttling_range_c = OpenStudio.convert(throttling_range_f, 'F', 'C').get + + # make a low temperature radiant loop for each zone + radiant_loops = [] + thermal_zones.each do |zone| + OpenStudio.logFree(OpenStudio::Info, 'openstudio.Model.Model', "Adding radiant loop for #{zone.name}.") + if zone.name.to_s.include? ':' + OpenStudio.logFree(OpenStudio::Error, 'openstudio.Model.Model', "Thermal zone '#{zone.name}' has a restricted character ':' in the name and will not work with some EMS and output reporting objects. Please rename the zone.") + end + + # create radiant coils + if hot_water_loop + radiant_loop_htg_coil = OpenStudio::Model::CoilHeatingLowTempRadiantVarFlow.new(self, htg_control_temp_sch) + radiant_loop_htg_coil.setName("#{zone.name} Radiant Loop Heating Coil") + radiant_loop_htg_coil.setHeatingControlThrottlingRange(throttling_range_c) + hot_water_loop.addDemandBranchForComponent(radiant_loop_htg_coil) + else + OpenStudio.logFree(OpenStudio::Error, 'openstudio.Model.Model', 'Radiant loops require a hot water loop, but none was provided.') + end + + if chilled_water_loop + radiant_loop_clg_coil = OpenStudio::Model::CoilCoolingLowTempRadiantVarFlow.new(self, clg_control_temp_sch) + radiant_loop_clg_coil.setName("#{zone.name} Radiant Loop Cooling Coil") + radiant_loop_clg_coil.setCoolingControlThrottlingRange(throttling_range_c) + chilled_water_loop.addDemandBranchForComponent(radiant_loop_clg_coil) + else + OpenStudio.logFree(OpenStudio::Error, 'openstudio.Model.Model', 'Radiant loops require a chilled water loop, but none was provided.') + end + + radiant_avail_sch = self.alwaysOnDiscreteSchedule + radiant_loop = OpenStudio::Model::ZoneHVACLowTempRadiantVarFlow.new(self, + radiant_avail_sch, + radiant_loop_htg_coil, + radiant_loop_clg_coil) + + # assign internal source construction to floors in zone + zone.spaces.each do |space| + space.surfaces.each do |surface| + if radiant_type == 'floor' + if surface.surfaceType == 'Floor' + if surface.outsideBoundaryCondition == 'Ground' + surface.setConstruction(radiant_ground_slab_construction) + elsif surface.outsideBoundaryCondition == 'Outdoors' + surface.setConstruction(radiant_exterior_slab_construction) + else # interior floor + surface.setConstruction(radiant_interior_floor_slab_construction) + end + end + elsif radiant_type == 'ceiling' + if surface.surfaceType == 'RoofCeiling' + if surface.outsideBoundaryCondition == 'Outdoors' + surface.setConstruction(radiant_ceiling_slab_construction) + else # interior ceiling + surface.setConstruction(radiant_interior_ceiling_slab_construction) + end + end + elsif radiant_type == 'ceilingmetalpanel' + if surface.surfaceType == 'RoofCeiling' + if surface.outsideBoundaryCondition == 'Outdoors' + surface.setConstruction(radiant_ceiling_metal_construction) + else # interior ceiling + surface.setConstruction(radiant_interior_ceiling_metal_construction) + end + end + end + end + end + + # radiant loop surfaces + radiant_loop.setName("#{zone.name} Radiant Loop") + if radiant_type == 'floor' + radiant_loop.setRadiantSurfaceType('Floors') + elsif radiant_type == 'ceiling' + radiant_loop.setRadiantSurfaceType('Ceilings') + elsif radiant_type == 'ceilingmetalpanel' + radiant_loop.setRadiantSurfaceType('Ceilings') + end + + # radiant loop layout details + radiant_loop.setHydronicTubingInsideDiameter(0.015875) # 5/8 in. ID, 3/4 in. OD + # @todo include a method to determine tubing length in the zone + # loop_length = 7*zone.floorArea + # radiant_loop.setHydronicTubingLength() + radiant_loop.setNumberofCircuits('CalculateFromCircuitLength') + radiant_loop.setCircuitLength(106.7) + + # radiant loop controls + radiant_loop.setTemperatureControlType('MeanAirTemperature') + radiant_loop.addToThermalZone(zone) + radiant_loops << radiant_loop + + # rename nodes before adding EMS code + std.rename_plant_loop_nodes(self) + + # set radiant loop controls + if control_strategy == 'proportional_control' + std.model_add_radiant_proportional_controls(self, zone, radiant_loop, + radiant_type: radiant_type, + model_occ_hr_start: model_occ_hr_start, + model_occ_hr_end: model_occ_hr_end, + proportional_gain: proportional_gain, + minimum_operation: minimum_operation, + weekend_temperature_reset: weekend_temperature_reset, + early_reset_out_arg: early_reset_out_arg, + switch_over_time: switch_over_time) + end + end + + return radiant_loops + end + end \ No newline at end of file