# *******************************************************************************
# Honeybee OpenStudio Gem, Copyright (c) 2020, Alliance for Sustainable
# Energy, LLC, Ladybug Tools LLC and other contributors. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# (1) Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# (2) Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# (3) Neither the name of the copyright holder nor the names of any contributors
# may be used to endorse or promote products derived from this software without
# specific prior written permission from the respective party.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE
# UNITED STATES GOVERNMENT, OR THE UNITED STATES DEPARTMENT OF ENERGY, NOR ANY OF
# THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# *******************************************************************************

# Note: This file is derived from the "Radiant Slab with DOAS" measure
# https://github.com/NREL/openstudio-model-articulation-gem/tree/develop/lib/measures/radiant_slab_with_doas
# It is intended that this file be updated if changes are made to the original measure

class OpenStudio::Model::Model
  # Adds a radiant HVAC system to the model
  def add_radiant_hvac_system(std, system_type, zones, rad_props)
    # the 'zones' argument includes zones that have heating, cooling, or both
    conditioned_zones = zones.select { |zone| std.thermal_zone_heated?(zone) && std.thermal_zone_cooled?(zone) }

    # get the climate zone from the model, which will help set water temerpatures
    climate_zone_obj = self.getClimateZones.getClimateZone('ASHRAE', 2006)
    if climate_zone_obj.empty
      climate_zone_obj = self.getClimateZones.getClimateZone('ASHRAE', 2013)
    end
    if climate_zone_obj.empty || climate_zone_obj.value == ''
        climate_zone = ''
      else
        climate_zone = climate_zone_obj.value
      end
    
    # 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'
    elsif system_type.include? 'Boiler'
      boiler_fuel_type = 'NaturalGas'
    elsif system_type.include? 'DHW'
      boiler_fuel_type = 'DistrictHeating'
    end
    hot_water_loop = std.model_add_hw_loop(
      self,
      boiler_fuel_type,
      dsgn_sup_wtr_temp: radiant_htg_dsgn_sup_wtr_temp_f,
      dsgn_sup_wtr_temp_delt: 10.0)

    # create the chilled water loop
    if system_type.include? 'Radiant_Chiller'  # water-cooled chiller
      # make condenser water loop
      fan_type = std.model_cw_loop_cooling_tower_fan_type(self)
      condenser_water_loop = std.model_add_cw_loop(
        self,
        cooling_tower_type: 'Open Cooling Tower',
        cooling_tower_fan_type: 'Propeller or Axial',
        cooling_tower_capacity_control: fan_type,
        number_of_cells_per_tower: 1,
        number_cooling_towers: 1)
      # make chilled water loop
      chilled_water_loop = std.model_add_chw_loop(
        self,
        chw_pumping_type: 'const_pri_var_sec',
        dsgn_sup_wtr_temp: 55.0,
        dsgn_sup_wtr_temp_delt: 5.0,
        chiller_cooling_type: 'WaterCooled',
        condenser_water_loop: condenser_water_loop)
    elsif system_type.include? 'Radiant_ACChiller'  # air-cooled chiller
      chilled_water_loop = std.model_add_chw_loop(
        self,
        chw_pumping_type: 'const_pri_var_sec',
        dsgn_sup_wtr_temp: 55.0,
        dsgn_sup_wtr_temp_delt: 5.0,
        chiller_cooling_type: 'AirCooled')
    else  # district chilled water cooled
        chilled_water_loop = std.model_add_chw_loop(
          self,
          cooling_fuel: 'DistrictCooling',
          chw_pumping_type: 'const_pri_var_sec',
          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[:minimum_operation_time]
      minimum_operation = rad_props[:minimum_operation_time]
    else
      minimum_operation = 1
    end
    if rad_props[:switch_over_time]
      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
    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' || radiant_type == 'floorwithhardwood'
        control_strategy = 'default'
      end
    else
      radiant_type = 'floor'
    end

    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,
      model_occ_hr_start: start_hour,
      model_occ_hr_end: end_hour,
      minimum_operation: minimum_operation,
      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
    # concrete slab 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 = nil
    air_gap_mat = nil
    wood_mat = nil
    wood_floor_insulation = nil
    gypsum_ceiling_mat = nil
    if radiant_type == 'ceilingmetalpanel'
      metal_mat = OpenStudio::Model::StandardOpaqueMaterial.new(self, 'MediumSmooth', 0.003175, 30, 7680, 418)
      metal_mat.setName('Radiant Metal Layer - 0.125 in')
      air_gap_mat = OpenStudio::Model::MasslessOpaqueMaterial.new(self, 'Smooth', 0.004572)
      air_gap_mat.setName('Generic Ceiling Air Gap - R 0.025')
    elsif radiant_type == 'floorwithhardwood'
      wood_mat = OpenStudio::Model::StandardOpaqueMaterial.new(self, 'MediumSmooth', 0.01905, 0.15, 608, 1629)
      wood_mat.setName('Radiant Hardwood Flooring - 0.75 in')
      wood_floor_insulation = OpenStudio::Model::StandardOpaqueMaterial.new(self, 'Rough', 0.0508, 0.02, 56.06, 1210)
      wood_floor_insulation.setName('Radiant Subfloor Insulation - 4.0 in')
      gypsum_ceiling_mat = OpenStudio::Model::StandardOpaqueMaterial.new(self, 'Smooth', 0.0127, 0.16, 800, 1089)
      gypsum_ceiling_mat.setName('Gypsum Ceiling for Radiant Hardwood Flooring - 0.5 in')
    end

    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
    radiant_ground_slab_construction = nil
    radiant_exterior_slab_construction = nil
    radiant_interior_floor_slab_construction = nil
    radiant_interior_ceiling_slab_construction = nil
    radiant_ceiling_slab_construction = nil
    radiant_interior_ceiling_metal_construction = nil
    radiant_ceiling_metal_construction = nil

    if radiant_type == 'floor'
      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
    elsif radiant_type == 'ceiling'
      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
    elsif radiant_type == 'ceilingmetalpanel'
      layers = []
      layers << mat_concrete_3_5in
      layers << air_gap_mat
      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_mat
      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
    elsif radiant_type == 'floorwithhardwood'
      layers = []
      layers << mat_slab_insulation
      layers << mat_concrete_3_5in
      layers << wood_mat
      layers << mat_thin_carpet_tile if include_carpet
      radiant_ground_wood_construction = OpenStudio::Model::ConstructionWithInternalSource.new(layers)
      radiant_ground_wood_construction.setName('Radiant Ground Slab Wood Floor Construction')
      radiant_ground_wood_construction.setSourcePresentAfterLayerNumber(2)
      radiant_ground_wood_construction.setTemperatureCalculationRequestedAfterLayerNumber(3)
      radiant_ground_wood_construction.setTubeSpacing(0.2286) # 9 inches

      layers = []
      layers << mat_ext_insulation
      layers << wood_mat
      layers << mat_thin_carpet_tile if include_carpet
      radiant_exterior_wood_construction = OpenStudio::Model::ConstructionWithInternalSource.new(layers)
      radiant_exterior_wood_construction.setName('Radiant Exterior Wood Floor Construction')
      radiant_exterior_wood_construction.setSourcePresentAfterLayerNumber(1)
      radiant_exterior_wood_construction.setTemperatureCalculationRequestedAfterLayerNumber(2)
      radiant_exterior_wood_construction.setTubeSpacing(0.2286) # 9 inches

      layers = []
      layers << gypsum_ceiling_mat
      layers << wood_floor_insulation
      layers << wood_mat
      layers << mat_thin_carpet_tile if include_carpet
      radiant_interior_wood_floor_construction = OpenStudio::Model::ConstructionWithInternalSource.new(layers)
      radiant_interior_wood_floor_construction.setName('Radiant Interior Wooden Floor Construction')
      radiant_interior_wood_floor_construction.setSourcePresentAfterLayerNumber(2)
      radiant_interior_wood_floor_construction.setTemperatureCalculationRequestedAfterLayerNumber(3)
      radiant_interior_wood_floor_construction.setTubeSpacing(0.2286) # 9 inches
    end

    # 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
          elsif radiant_type == 'floorwithhardwood'
            if surface.surfaceType == 'Floor'
              if surface.outsideBoundaryCondition == 'Ground'
                surface.setConstruction(radiant_ground_wood_construction)
              elsif surface.outsideBoundaryCondition == 'Outdoors'
                surface.setConstruction(radiant_exterior_wood_construction)
              else # interior floor
                surface.setConstruction(radiant_interior_wood_floor_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')
      elsif radiant_type == 'floorwithhardwood'
        radiant_loop.setRadiantSurfaceType('Floors')
      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