# Reopen the OpenStudio class to add methods to apply standards to this object
class OpenStudio::Model::PlantLoop
  # Apply all standard required controls to the plantloop
  #
  # @param (see #economizer_required?)
  # @return [Bool] returns true if successful, false if not
  def apply_standard_controls(template, climate_zone)
    # Variable flow system
    enable_variable_flow(template) if is_variable_flow_required(template)

    # Supply water temperature reset
    enable_supply_water_temperature_reset if supply_water_temperature_reset_required?(template)
  end

  def enable_variable_flow(template)
  end

  def variable_flow_system?
    variable_flow = false

    # Modify all the primary pumps
    supplyComponents.each do |sc|
      if sc.to_PumpVariableSpeed.is_initialized
        variable_flow = true
      end
    end

    # Modify all the secondary pumps
    demandComponents.each do |sc|
      if sc.to_PumpVariableSpeed.is_initialized
        variable_flow = true
      end
    end

    return variable_flow
  end

  # TODO: I think it makes more sense to sense the motor efficiency right there...
  # But actually it's completely irrelevant... you could set at 0.9 and just calculate the pressurise rise to have your 19 W/GPM or whatever
  def apply_prm_baseline_pump_power(template)
    # Determine the pumping power per
    # flow based on loop type.
    pri_w_per_gpm = nil
    sec_w_per_gpm = nil

    sizing_plant = sizingPlant
    loop_type = sizing_plant.loopType

    case loop_type
    when 'Heating'

      has_district_heating = false
      supplyComponents.each do |sc|
        if sc.to_DistrictHeating.is_initialized
          has_district_heating = true
        end
      end

      pri_w_per_gpm = if has_district_heating # District HW
                        14.0
                      else # HW
                        19.0
                      end

    when 'Cooling'

      has_district_cooling = false
      supplyComponents.each do |sc|
        if sc.to_DistrictCooling.is_initialized
          has_district_cooling = true
        end
      end

      has_secondary_pump = false
      demandComponents.each do |sc|
        if sc.to_PumpConstantSpeed.is_initialized || sc.to_PumpVariableSpeed.is_initialized
          has_secondary_pump = true
        end
      end

      if has_district_cooling # District CHW
        pri_w_per_gpm = 16.0
      elsif has_secondary_pump # Primary/secondary CHW
        pri_w_per_gpm = 9.0
        sec_w_per_gpm = 13.0
      else # Primary only CHW
        pri_w_per_gpm = 22.0
      end

    when 'Condenser'

      # TODO: prm condenser loop pump power
      pri_w_per_gpm = 19.0

    end

    # Modify all the primary pumps
    supplyComponents.each do |sc|
      if sc.to_PumpConstantSpeed.is_initialized
        pump = sc.to_PumpConstantSpeed.get
        pump.apply_prm_pressure_rise_and_motor_efficiency(pri_w_per_gpm, template)
      elsif sc.to_PumpVariableSpeed.is_initialized
        pump = sc.to_PumpVariableSpeed.get
        pump.apply_prm_pressure_rise_and_motor_efficiency(pri_w_per_gpm, template)
      elsif sc.to_HeaderedPumpsConstantSpeed.is_initialized
        pump = sc.to_HeaderedPumpsConstantSpeed.get
        pump.apply_prm_pressure_rise_and_motor_efficiency(pri_w_per_gpm, template)
      elsif sc.to_HeaderedPumpsVariableSpeed.is_initialized
        pump = sc.to_HeaderedPumpsVariableSpeed.get
        pump.apply_prm_pressure_rise_and_motor_efficiency(pri_w_per_gpm, template)
      end
    end

    # Modify all the secondary pumps
    demandComponents.each do |sc|
      if sc.to_PumpConstantSpeed.is_initialized
        pump = sc.to_PumpConstantSpeed.get
        pump.apply_prm_pressure_rise_and_motor_efficiency(sec_w_per_gpm, template)
      elsif sc.to_PumpVariableSpeed.is_initialized
        pump = sc.to_PumpVariableSpeed.get
        pump.apply_prm_pressure_rise_and_motor_efficiency(sec_w_per_gpm, template)
      elsif sc.to_HeaderedPumpsConstantSpeed.is_initialized
        pump = sc.to_HeaderedPumpsConstantSpeed.get
        pump.apply_prm_pressure_rise_and_motor_efficiency(pri_w_per_gpm, template)
      elsif sc.to_HeaderedPumpsVariableSpeed.is_initialized
        pump = sc.to_HeaderedPumpsVariableSpeed.get
        pump.apply_prm_pressure_rise_and_motor_efficiency(pri_w_per_gpm, template)
      end
    end

    return true
  end

  def apply_prm_baseline_temperatures(template)
    sizing_plant = sizingPlant
    loop_type = sizing_plant.loopType
    case loop_type
    when 'Heating'

      # Loop properties
      # G3.1.3.3 - HW Supply at 180F, return at 130F
      hw_temp_f = 180
      hw_delta_t_r = 50
      min_temp_f = 50

      hw_temp_c = OpenStudio.convert(hw_temp_f, 'F', 'C').get
      hw_delta_t_k = OpenStudio.convert(hw_delta_t_r, 'R', 'K').get
      min_temp_c = OpenStudio.convert(min_temp_f, 'F', 'C').get

      sizing_plant.setDesignLoopExitTemperature(hw_temp_c)
      sizing_plant.setLoopDesignTemperatureDifference(hw_delta_t_k)
      setMinimumLoopTemperature(min_temp_c)

      # ASHRAE Appendix G - G3.1.3.4 (for ASHRAE 90.1-2004, 2007 and 2010)
      # HW reset: 180F at 20F and below, 150F at 50F and above
      enable_supply_water_temperature_reset

      # Boiler properties
      supplyComponents.each do |sc|
        if sc.to_BoilerHotWater.is_initialized
          boiler = sc.to_BoilerHotWater.get
          boiler.setDesignWaterOutletTemperature(hw_temp_c)
        end
      end

    when 'Cooling'

      # Loop properties
      # G3.1.3.8 - LWT 44 / EWT 56
      chw_temp_f = 44
      chw_delta_t_r = 12
      min_temp_f = 34
      max_temp_f = 200
      # For water-cooled chillers this is the water temperature entering the condenser (e.g., leaving the cooling tower).
      ref_cond_wtr_temp_f = 85

      chw_temp_c = OpenStudio.convert(chw_temp_f, 'F', 'C').get
      chw_delta_t_k = OpenStudio.convert(chw_delta_t_r, 'R', 'K').get
      min_temp_c = OpenStudio.convert(min_temp_f, 'F', 'C').get
      max_temp_c = OpenStudio.convert(max_temp_f, 'F', 'C').get
      ref_cond_wtr_temp_c = OpenStudio.convert(ref_cond_wtr_temp_f, 'F', 'C').get

      sizing_plant.setDesignLoopExitTemperature(chw_temp_c)
      sizing_plant.setLoopDesignTemperatureDifference(chw_delta_t_k)
      setMinimumLoopTemperature(min_temp_c)
      setMaximumLoopTemperature(max_temp_c)

      # ASHRAE Appendix G - G3.1.3.9 (for ASHRAE 90.1-2004, 2007 and 2010)
      # ChW reset: 44F at 80F and above, 54F at 60F and below
      enable_supply_water_temperature_reset

      # Chiller properties
      supplyComponents.each do |sc|
        if sc.to_ChillerElectricEIR.is_initialized
          chiller = sc.to_ChillerElectricEIR.get
          chiller.setReferenceLeavingChilledWaterTemperature(chw_temp_c)
          chiller.setReferenceEnteringCondenserFluidTemperature(ref_cond_wtr_temp_c)
        end
      end

    when 'Condenser'

      # Much of the thought in this section
      # came from @jmarrec

      # Determine the design OATwb from the design days.
      # Per https://unmethours.com/question/16698/which-cooling-design-day-is-most-common-for-sizing-rooftop-units/
      # the WB=>MDB day is used to size cooling towers.
      summer_oat_wbs_f = []
      model.getDesignDays.each do |dd|
        next unless dd.dayType == 'SummerDesignDay'
        next unless dd.name.get.to_s.include?('WB=>MDB')
        if dd.humidityIndicatingType == 'Wetbulb'
          summer_oat_wb_c = dd.humidityIndicatingConditionsAtMaximumDryBulb
          summer_oat_wbs_f << OpenStudio.convert(summer_oat_wb_c, 'C', 'F').get
        else
          OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.PlantLoop', "For #{dd.name}, humidity is specified as #{dd.humidityIndicatingType}; cannot determine Twb.")
        end
      end

      # Use the value from the design days or
      # 78F, the CTI rating condition, if no
      # design day information is available.
      design_oat_wb_f = nil
      if summer_oat_wbs_f.size.zero?
        design_oat_wb_f = 78
        OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.PlantLoop', "For #{name}, no design day OATwb conditions were found.  CTI rating condition of 78F OATwb will be used for sizing cooling towers.")
      else
        # Take worst case condition
        design_oat_wb_f = summer_oat_wbs_f.max
      end

      # There is an EnergyPlus model limitation
      # that the design_oat_wb_f < 80F
      # for cooling towers
      ep_max_design_oat_wb_f = 80
      if design_oat_wb_f > ep_max_design_oat_wb_f
        OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.PlantLoop', "For #{name}, reduced design OATwb from #{design_oat_wb_f} F to E+ model max input of #{ep_max_design_oat_wb_f} F.")
        design_oat_wb_f = ep_max_design_oat_wb_f
      end

      # Determine the design CW temperature, approach, and range
      leaving_cw_t_f = nil
      approach_r = nil
      range_r = nil
      case template
      when 'DOE Ref Pre-1980', 'DOE Ref 1980-2004', '90.1-2004', '90.1-2007', '90.1-2010'
        # G3.1.3.11 - CW supply temp = 85F or 10F approaching design wet bulb temperature,
        # whichever is lower.  Design range = 10F
        # Design Temperature rise of 10F => Range: 10F
        range_r = 10

        # Determine the leaving CW temp
        max_leaving_cw_t_f = 85
        leaving_cw_t_10f_approach_f = design_oat_wb_f + 10
        leaving_cw_t_f = [max_leaving_cw_t_f, leaving_cw_t_10f_approach_f].max

        # Calculate the approach
        approach_r = leaving_cw_t_f - design_oat_wb_f

      when '90.1-2013'
        # G3.1.3.11 - CW supply temp shall be evaluated at 0.4% evaporative design OATwb
        # per the formulat approach_F = 25.72 - (0.24 * OATwb_F)
        # 55F <= OATwb <= 90F
        # Design range = 10F.
        range_r = 10

        # Limit the OATwb
        if design_oat_wb_f < 55
          design_oat_wb_f = 55
          OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.PlantLoop', "For #{name}, a design OATwb of 55F will be used for sizing the cooling towers because the actual design value is below the limit in G3.1.3.11.")
        elsif design_oat_wb_f > 90
          design_oat_wb_f = 90
          OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.PlantLoop', "For #{name}, a design OATwb of 90F will be used for sizing the cooling towers because the actual design value is above the limit in G3.1.3.11.")
        end

        # Calculate the approach
        approach_r = 25.72 - (0.24 * design_oat_wb_f)

        # Calculate the leaving CW temp
        leaving_cw_t_f = design_oat_wb_f + approach_r

      end

      # Report out design conditions
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.PlantLoop', "For #{name}, design OATwb = #{design_oat_wb_f.round(1)} F, approach = #{approach_r.round(1)} deltaF, range = #{range_r.round(1)} deltaF, leaving condenser water temperature = #{leaving_cw_t_f.round(1)} F.")

      # Convert to SI units
      leaving_cw_t_c = OpenStudio.convert(leaving_cw_t_f, 'F', 'C').get
      approach_k = OpenStudio.convert(approach_r, 'R', 'K').get
      range_k = OpenStudio.convert(range_r, 'R', 'K').get
      design_oat_wb_c = OpenStudio.convert(design_oat_wb_f, 'F', 'C').get

      # Set the CW sizing parameters
      sizing_plant.setDesignLoopExitTemperature(leaving_cw_t_c)
      sizing_plant.setLoopDesignTemperatureDifference(range_k)

      # Set Cooling Tower sizing parameters.
      # Only the variable speed cooling tower
      # in E+ allows you to set the design temperatures.
      #
      # Per the documentation
      # http://bigladdersoftware.com/epx/docs/8-4/input-output-reference/group-condenser-equipment.html#field-design-u-factor-times-area-value
      # for CoolingTowerSingleSpeed and CoolingTowerTwoSpeed
      # E+ uses the following values during sizing:
      # 95F entering water temp
      # 95F OATdb
      # 78F OATwb
      # range = loop design delta-T aka range (specified above)
      supplyComponents.each do |sc|
        if sc.to_CoolingTowerVariableSpeed.is_initialized
          ct = sc.to_CoolingTowerVariableSpeed.get
          # E+ has a minimum limit of 68F (20C) for this field.
          # Check against limit before attempting to set value.
          eplus_design_oat_wb_c_lim = 20
          if design_oat_wb_c < eplus_design_oat_wb_c_lim
            OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.PlantLoop', "For #{name}, a design OATwb of 68F will be used for sizing the cooling towers because the actual design value is below the limit EnergyPlus accepts for this input.")
            design_oat_wb_c = eplus_design_oat_wb_c_lim
          end
          ct.setDesignInletAirWetBulbTemperature(design_oat_wb_c)
          ct.setDesignApproachTemperature(approach_k)
          ct.setDesignRangeTemperature(range_k)
        end
      end

      # Set the min and max CW temps
      # Typical design of min temp is really around 40F
      # (that's what basin heaters, when used, are sized for usually)
      min_temp_f = 34
      max_temp_f = 200
      min_temp_c = OpenStudio.convert(min_temp_f, 'F', 'C').get
      max_temp_c = OpenStudio.convert(max_temp_f, 'F', 'C').get
      setMinimumLoopTemperature(min_temp_c)
      setMaximumLoopTemperature(max_temp_c)

      # Cooling Tower operational controls
      # G3.1.3.11 - Tower shall be controlled to maintain a 70F
      # LCnWT where weather permits,
      # floating up to leaving water at design conditions.
      float_down_to_f = 70
      float_down_to_c = OpenStudio.convert(float_down_to_f, 'F', 'C').get
      cw_t_stpt_manager = OpenStudio::Model::SetpointManagerFollowOutdoorAirTemperature.new(model)
      cw_t_stpt_manager.setName("CW Temp Follows OATwb w/ #{approach_r} deltaF approach min #{float_down_to_f.round(1)} F to max #{leaving_cw_t_f.round(1)}")
      cw_t_stpt_manager.setReferenceTemperatureType('OutdoorAirWetBulb')
      # At low design OATwb, it is possible to calculate
      # a maximum temperature below the minimum.  In this case,
      # make the maximum and minimum the same.
      if leaving_cw_t_c < float_down_to_c
        OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.PlantLoop', "For #{name}, the maximum leaving temperature of #{leaving_cw_t_f.round(1)} F is below the minimum of #{float_down_to_f.round(1)} F.  The maximum will be set to the same value as the minimum.")
        leaving_cw_t_c = float_down_to_c
      end
      cw_t_stpt_manager.setMaximumSetpointTemperature(leaving_cw_t_c)
      cw_t_stpt_manager.setMinimumSetpointTemperature(float_down_to_c)
      cw_t_stpt_manager.setOffsetTemperatureDifference(approach_k)
      cw_t_stpt_manager.addToNode(supplyOutletNode)

    end

    return true
  end

  def supply_water_temperature_reset_required?(template)
    reset_required = false

    case template
    when 'DOE Ref Pre-1980', 'DOE Ref 1980-2004'

      # Not required before 90.1-2004
      return reset_required

    when '90.1-2004', '90.1-2007', '90.1-2010', '90.1-2013'

      # Not required for service water heating systems
      if swh_loop?
        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.PlantLoop', "For #{name}: supply water temperature reset not required for service water heating systems.")
        return reset_required
      end

      # Not required for variable flow systems
      if variable_flow_system?
        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.PlantLoop', "For #{name}: supply water temperature reset not required for variable flow systems per 6.5.4.3 Exception b.")
        return reset_required
      end

      # Determine the capacity of the system
      heating_capacity_w = total_heating_capacity
      cooling_capacity_w = total_cooling_capacity

      heating_capacity_btu_per_hr = OpenStudio.convert(heating_capacity_w, 'W', 'Btu/hr').get
      cooling_capacity_btu_per_hr = OpenStudio.convert(cooling_capacity_w, 'W', 'Btu/hr').get

      # Compare against capacity minimum requirement
      min_cap_btu_per_hr = 300_000
      if heating_capacity_btu_per_hr > min_cap_btu_per_hr
        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.PlantLoop', "For #{name}: supply water temperature reset is required because heating capacity of #{heating_capacity_btu_per_hr.round} Btu/hr exceeds the minimum threshold of #{min_cap_btu_per_hr.round} Btu/hr.")
        reset_required = true
      elsif cooling_capacity_btu_per_hr > min_cap_btu_per_hr
        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.PlantLoop', "For #{name}: supply water temperature reset is required because cooling capacity of #{cooling_capacity_btu_per_hr.round} Btu/hr exceeds the minimum threshold of #{min_cap_btu_per_hr.round} Btu/hr.")
        reset_required = true
      else
        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.PlantLoop', "For #{name}: supply water temperature reset is not required because capacity is less than minimum of #{min_cap_btu_per_hr.round} Btu/hr.")
      end

    end

    return reset_required
  end

  def enable_supply_water_temperature_reset
    # Get the current setpoint manager on the outlet node
    # and determine if already has temperature reset
    spms = supplyOutletNode.setpointManagers
    spms.each do |spm|
      if spm.to_SetpointManagerOutdoorAirReset.is_initialized
        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.PlantLoop', "For #{name}: supply water temperature reset is already enabled.")
        return false
      end
    end

    # Get the design water temperature
    sizing_plant = sizingPlant
    design_temp_c = sizing_plant.designLoopExitTemperature
    design_temp_f = OpenStudio.convert(design_temp_c, 'C', 'F').get
    loop_type = sizing_plant.loopType

    # Apply the reset, depending on the type of loop.
    case loop_type
    when 'Heating'

      # Hot water as-designed when cold outside
      hwt_at_lo_oat_f = design_temp_f
      hwt_at_lo_oat_c = OpenStudio.convert(hwt_at_lo_oat_f, 'F', 'C').get
      # 30F decrease when it's hot outside,
      # and therefore less heating capacity is likely required.
      decrease_f = 30.0
      hwt_at_hi_oat_f = hwt_at_lo_oat_f - decrease_f
      hwt_at_hi_oat_c = OpenStudio.convert(hwt_at_hi_oat_f, 'F', 'C').get

      # Define the high and low outdoor air temperatures
      lo_oat_f = 20
      lo_oat_c = OpenStudio.convert(lo_oat_f, 'F', 'C').get
      hi_oat_f = 50
      hi_oat_c = OpenStudio.convert(hi_oat_f, 'F', 'C').get

      # Create a setpoint manager
      hwt_oa_reset = OpenStudio::Model::SetpointManagerOutdoorAirReset.new(model)
      hwt_oa_reset.setName("#{name} HW Temp Reset")
      hwt_oa_reset.setControlVariable('Temperature')
      hwt_oa_reset.setSetpointatOutdoorLowTemperature(hwt_at_lo_oat_c)
      hwt_oa_reset.setOutdoorLowTemperature(lo_oat_c)
      hwt_oa_reset.setSetpointatOutdoorHighTemperature(hwt_at_hi_oat_c)
      hwt_oa_reset.setOutdoorHighTemperature(hi_oat_c)
      hwt_oa_reset.addToNode(supplyOutletNode)

      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.PlantLoop', "For #{name}: hot water temperature reset from #{hwt_at_lo_oat_f.round}F to #{hwt_at_hi_oat_f.round}F between outdoor air temps of #{lo_oat_f.round}F and #{hi_oat_f.round}F.")

    when 'Cooling'

      # Chilled water as-designed when hot outside
      chwt_at_hi_oat_f = design_temp_f
      chwt_at_hi_oat_c = OpenStudio.convert(chwt_at_hi_oat_f, 'F', 'C').get
      # 10F increase when it's cold outside,
      # and therefore less cooling capacity is likely required.
      increase_f = 10.0
      chwt_at_lo_oat_f = chwt_at_hi_oat_f + increase_f
      chwt_at_lo_oat_c = OpenStudio.convert(chwt_at_lo_oat_f, 'F', 'C').get

      # Define the high and low outdoor air temperatures
      lo_oat_f = 60
      lo_oat_c = OpenStudio.convert(lo_oat_f, 'F', 'C').get
      hi_oat_f = 80
      hi_oat_c = OpenStudio.convert(hi_oat_f, 'F', 'C').get

      # Create a setpoint manager
      chwt_oa_reset = OpenStudio::Model::SetpointManagerOutdoorAirReset.new(model)
      chwt_oa_reset.setName("#{name} CHW Temp Reset")
      chwt_oa_reset.setControlVariable('Temperature')
      chwt_oa_reset.setSetpointatOutdoorLowTemperature(chwt_at_lo_oat_c)
      chwt_oa_reset.setOutdoorLowTemperature(lo_oat_c)
      chwt_oa_reset.setSetpointatOutdoorHighTemperature(chwt_at_hi_oat_c)
      chwt_oa_reset.setOutdoorHighTemperature(hi_oat_c)
      chwt_oa_reset.addToNode(supplyOutletNode)

      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.PlantLoop', "For #{name}: chilled water temperature reset from #{chwt_at_hi_oat_f.round}F to #{chwt_at_lo_oat_f.round}F between outdoor air temps of #{hi_oat_f.round}F and #{lo_oat_f.round}F.")

    else

      OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.PlantLoop', "For #{name}: cannot enable supply water temperature reset for a #{loop_type} loop.")
      return false

    end

    return true
  end

  # Get the total cooling capacity for the plant loop
  #
  # @return [Double] total cooling capacity
  #   units = Watts (W)
  def total_cooling_capacity
    # Sum the cooling capacity for all cooling components
    # on the plant loop.
    total_cooling_capacity_w = 0
    supplyComponents.each do |sc|
      # ChillerElectricEIR
      if sc.to_ChillerElectricEIR.is_initialized
        chiller = sc.to_ChillerElectricEIR.get
        if chiller.referenceCapacity.is_initialized
          total_cooling_capacity_w += chiller.referenceCapacity.get
        elsif chiller.autosizedReferenceCapacity.is_initialized
          total_cooling_capacity_w += chiller.autosizedReferenceCapacity.get
        else
          OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.AirLoopHVAC', "For #{name} capacity of #{chiller.name} is not available, total cooling capacity of plant loop will be incorrect when applying standard.")
        end
      # DistrictCooling
      elsif sc.to_DistrictCooling.is_initialized
        dist_clg = sc.to_DistrictCooling.get
        if dist_clg.nominalCapacity.is_initialized
          total_cooling_capacity_w += dist_clg.nominalCapacity.get
        elsif dist_clg.autosizedNominalCapacity.is_initialized
          total_cooling_capacity_w += dist_clg.autosizedNominalCapacity.get
        else
          OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.PlantLoop', "For #{self.name} capacity of DistrictCooling #{dist_clg.name} is not available, total heating capacity of plant loop will be incorrect when applying standard.")
        end
      end
    end

    total_cooling_capacity_tons = OpenStudio.convert(total_cooling_capacity_w, 'W', 'ton').get
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{name}, cooling capacity is #{total_cooling_capacity_tons.round} tons of refrigeration.")

    return total_cooling_capacity_w
  end

  # Get the total heating capacity for the plant loop
  #
  # @return [Double] total heating capacity
  #   units = Watts (W)
  # @todo Add district heating to plant loop heating capacity
  def total_heating_capacity
    # Sum the heating capacity for all heating components
    # on the plant loop.
    total_heating_capacity_w = 0
    supplyComponents.each do |sc|
      # BoilerHotWater
      if sc.to_BoilerHotWater.is_initialized
        boiler = sc.to_BoilerHotWater.get
        if boiler.nominalCapacity.is_initialized
          total_heating_capacity_w += boiler.nominalCapacity.get
        elsif boiler.autosizedNominalCapacity.is_initialized
          total_heating_capacity_w += boiler.autosizedNominalCapacity.get
        else
          OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.PlantLoop', "For #{self.name} capacity of Boiler:HotWater ' #{boiler.name} is not available, total heating capacity of plant loop will be incorrect when applying standard.")
        end
      # WaterHeater:Mixed
      elsif sc.to_WaterHeaterMixed.is_initialized
        water_heater = sc.to_WaterHeaterMixed.get
        if water_heater.heaterMaximumCapacity.is_initialized
          total_heating_capacity_w += water_heater.heaterMaximumCapacity.get
        elsif water_heater.autosizedHeaterMaximumCapacity.is_initialized
          total_heating_capacity_w += water_heater.autosizedHeaterMaximumCapacity.get
        else
          OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.PlantLoop', "For #{self.name} capacity of WaterHeater:Mixed #{water_heater.name} is not available, total heating capacity of plant loop will be incorrect when applying standard.")
        end
      # WaterHeater:Stratified
      elsif sc.to_WaterHeaterStratified.is_initialized
        water_heater = sc.to_WaterHeaterStratified.get
        if water_heater.heater1Capacity.is_initialized
          total_heating_capacity_w += water_heater.heater1Capacity.get
        end
        if water_heater.heater2Capacity.is_initialized
          total_heating_capacity_w += water_heater.heater2Capacity.get
        end
      # DistrictHeating
      elsif sc.to_DistrictHeating.is_initialized
        dist_htg = sc.to_DistrictHeating.get
        if dist_htg.nominalCapacity.is_initialized
          total_heating_capacity_w += dist_htg.nominalCapacity.get
        elsif dist_htg.autosizedNominalCapacity.is_initialized
          total_heating_capacity_w += dist_htg.autosizedNominalCapacity.get
        else
          OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.PlantLoop', "For #{self.name} capacity of DistrictHeating #{dist_htg.name} is not available, total heating capacity of plant loop will be incorrect when applying standard.")
        end
      end
    end # End loop on supplyComponents

    total_heating_capacity_kbtu_per_hr = OpenStudio.convert(total_heating_capacity_w,'W','kBtu/hr').get
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.PlantLoop', "For #{self.name}, heating capacity is #{total_heating_capacity_kbtu_per_hr.round} kBtu/hr.")

    return total_heating_capacity_w
  end

  def total_floor_area_served
    sizing_plant = sizingPlant
    loop_type = sizing_plant.loopType

    # Get all the coils served by this loop
    coils = []
    case loop_type
    when 'Heating'
      demandComponents.each do |dc|
        if dc.to_CoilHeatingWater.is_initialized
          coils << dc.to_CoilHeatingWater.get
        end
      end
    when 'Cooling'
      demandComponents.each do |dc|
        if dc.to_CoilCoolingWater.is_initialized
          coils << dc.to_CoilCoolingWater.get
        end
      end
    else
      return 0.0
    end

    # The coil can either be on an airloop (as a main heating coil)
    # in an HVAC Component (like a unitary system on an airloop),
    # or in a Zone HVAC Component (like a fan coil).
    zones_served = []
    coils.each do |coil|
      if coil.airLoopHVAC.is_initialized
        air_loop = coil.airLoopHVAC.get
        zones_served += air_loop.thermalZones
      elsif coil.containingHVACComponent.is_initialized
        containing_comp = coil.containingHVACComponent.get
        if containing_comp.airLoopHVAC.is_initialized
          air_loop = containing_comp.airLoopHVAC.get
          zones_served += air_loop.thermalZones
        end
      elsif coil.containingZoneHVACComponent.is_initialized
        zone_hvac = coil.containingZoneHVACComponent.get
        if zone_hvac.thermalZone.is_initialized
          zones_served << zone_hvac.thermalZone.get
        end
      end
    end

    # Add up the area of all zones served.
    # Make sure to only add unique zones in
    # case the same zone is served by multiple
    # coils served by the same loop.  For example,
    # a HW and Reheat
    area_served_m2 = 0.0
    zones_served.uniq.each do |zone|
      area_served_m2 += zone.floorArea
    end
    area_served_ft2 = OpenStudio.convert(area_served_m2, 'm^2', 'ft^2').get

    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{name}, serves #{area_served_ft2.round} ft^2.")

    return area_served_m2
  end

  def apply_prm_baseline_pumping_type(template)
    sizing_plant = sizingPlant
    loop_type = sizing_plant.loopType

    case loop_type
    when 'Heating'

      # Hot water systems

      # Determine the minimum area to determine
      # pumping type.
      minimum_area_ft2 = nil
      case template
      when '90.1-2004', '90.1-2007', '90.1-2010', '90.1-2013'
        minimum_area_ft2 = 120_000
      end

      # Determine the area served
      area_served_m2 = total_floor_area_served
      area_served_ft2 = OpenStudio.convert(area_served_m2, 'm^2', 'ft^2').get

      # Determine the pump type
      control_type = 'Riding Curve'
      if area_served_ft2 > minimum_area_ft2
        control_type = 'VSD No Reset'
      end

      # Modify all the primary pumps
      supplyComponents.each do |sc|
        if sc.to_PumpVariableSpeed.is_initialized
          pump = sc.to_PumpVariableSpeed.get
          pump.set_control_type(control_type)
        end
      end

      # Report out the pumping type
      unless control_type.nil?
        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{name}, pump type is #{control_type}.")
      end

    when 'Cooling'

      # Chilled water systems

      # Determine the pumping type.
      # For some templates, this is
      # based on area.  For others, it is built
      # on cooling capacity.
      pri_control_type = nil
      sec_control_type = nil
      case template
      when '90.1-2004'

        minimum_area_ft2 = 120_000

        # Determine the area served
        area_served_m2 = total_floor_area_served
        area_served_ft2 = OpenStudio.convert(area_served_m2, 'm^2', 'ft^2').get

        # Determine the primary pump type
        pri_control_type = 'Constant Flow'

        # Determine the secondary pump type
        sec_control_type = 'Riding Curve'
        if area_served_ft2 > minimum_area_ft2
          sec_control_type = 'VSD No Reset'
        end

      when '90.1-2007', '90.1-2010', '90.1-2013'

        minimum_cap_tons = 300

        # Determine the capacity
        cap_w = total_cooling_capacity
        cap_tons = OpenStudio.convert(cap_w, 'W', 'ton').get

        # Determine if it a district cooling system
        has_district_cooling = false
        supplyComponents.each do |sc|
          if sc.to_DistrictCooling.is_initialized
            has_district_cooling = true
          end
        end

        # Determine the primary and secondary pumping types
        pri_control_type = nil
        sec_control_type = nil
        if has_district_cooling
          pri_control_type = if cap_tons > minimum_cap_tons
                               'VSD No Reset'
                             else
                               'Riding Curve'
                             end
        else
          pri_control_type = 'Constant Flow'
          sec_control_type = if cap_tons > minimum_cap_tons
                               'VSD No Reset'
                             else
                               'Riding Curve'
                             end
        end
      end

      # Report out the pumping type
      unless pri_control_type.nil?
        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{name}, primary pump type is #{pri_control_type}.")
      end

      unless sec_control_type.nil?
        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{name}, secondary pump type is #{sec_control_type}.")
      end

      # Modify all the primary pumps
      supplyComponents.each do |sc|
        if sc.to_PumpVariableSpeed.is_initialized
          pump = sc.to_PumpVariableSpeed.get
          pump.set_control_type(pri_control_type)
        elsif sc.to_HeaderedPumpsVariableSpeed.is_initialized
          pump = sc.to_HeaderedPumpsVariableSpeed.get
          pump.set_control_type(control_type)        
        end
      end

      # Modify all the secondary pumps
      demandComponents.each do |sc|
        if sc.to_PumpVariableSpeed.is_initialized
          pump = sc.to_PumpVariableSpeed.get
          pump.set_control_type(sec_control_type)
        elsif sc.to_HeaderedPumpsVariableSpeed.is_initialized
          pump = sc.to_HeaderedPumpsVariableSpeed.get
          pump.set_control_type(control_type)
        end
      end

    when 'Condenser'

      # Condenser water systems

      # All condenser water loops are constant flow
      control_type = 'Constant Flow'

      # Report out the pumping type
      unless control_type.nil?
        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.AirLoopHVAC', "For #{name}, pump type is #{control_type}.")
      end

      # Modify all primary pumps
      supplyComponents.each do |sc|
        if sc.to_PumpVariableSpeed.is_initialized
          pump = sc.to_PumpVariableSpeed.get
          pump.set_control_type(control_type)
        elsif sc.to_HeaderedPumpsVariableSpeed.is_initialized
          pump = sc.to_HeaderedPumpsVariableSpeed.get
          pump.set_control_type(control_type)
        end
      end

    end

    return true
  end

  def apply_prm_number_of_boilers(template)
    # Skip non-heating plants
    return true unless sizingPlant.loopType == 'Heating'

    # Determine the minimum area to determine
    # number of boilers.
    minimum_area_ft2 = nil
    case template
    when '90.1-2004', '90.1-2007', '90.1-2010', '90.1-2013'
      minimum_area_ft2 = 15_000
    end

    # Determine the area served
    area_served_m2 = total_floor_area_served
    area_served_ft2 = OpenStudio.convert(area_served_m2, 'm^2', 'ft^2').get

    # Do nothing if only one boiler is required
    return true if area_served_ft2 < minimum_area_ft2

    # Get all existing boilers
    boilers = []
    supplyComponents.each do |sc|
      if sc.to_BoilerHotWater.is_initialized
        boilers << sc.to_BoilerHotWater.get
      end
    end

    # Ensure there is only 1 boiler to start
    first_boiler = nil
    if boilers.size.zero?
      return true
    elsif boilers.size > 1
      OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.PlantLoop', "For #{name}, found #{boilers.size}, cannot split up per performance rating method baseline requirements.")
    else
      first_boiler = boilers[0]
    end

    # Clone the existing boiler and create
    # a new branch for it
    second_boiler = first_boiler.clone(model)
    if second_boiler.to_BoilerHotWater.is_initialized
      second_boiler = second_boiler.to_BoilerHotWater.get
    else
      OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.PlantLoop', "For #{name}, could not clone boiler #{first_boiler.name}, cannot apply the performance rating method number of boilers.")
      return false
    end
    addSupplyBranchForComponent(second_boiler)
    final_boilers = [first_boiler, second_boiler]
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.PlantLoop', "For #{name}, added a second boiler.")

    # Set the sizing factor for all boilers evenly and Rename the boilers
    sizing_factor = (1.0 / final_boilers.size).round(2)
    final_boilers.each_with_index do |boiler, i|
      boiler.setSizingFactor(sizing_factor)
      boiler.setName("#{first_boiler.name} #{i + 1} of #{final_boilers.size}")
    end

    # Set the equipment to stage sequentially
    setLoadDistributionScheme('SequentialLoad')

    return true
  end

  def apply_prm_number_of_chillers(template)
    # Skip non-cooling plants
    return true unless sizingPlant.loopType == 'Cooling'

    # Determine the number and type of chillers
    num_chillers = nil
    chiller_cooling_type = nil
    chiller_compressor_type = nil
    case template
    when '90.1-2004', '90.1-2007', '90.1-2010', '90.1-2013'

      # Determine the capacity of the loop
      cap_w = total_cooling_capacity
      cap_tons = OpenStudio.convert(cap_w, 'W', 'ton').get

      if cap_tons <= 300
        num_chillers = 1
        chiller_cooling_type = 'WaterCooled'
        chiller_compressor_type = 'Rotary Screw'
      elsif cap_tons > 300 && cap_tons < 600
        num_chillers = 2
        chiller_cooling_type = 'WaterCooled'
        chiller_compressor_type = 'Rotary Screw'
      else
        # Max capacity of a single chiller
        max_cap_ton = 800.0
        num_chillers = (cap_tons / max_cap_ton).floor + 1
        # Must be at least 2 chillers
        num_chillers += 1 if num_chillers == 1
        chiller_cooling_type = 'WaterCooled'
        chiller_compressor_type = 'Centrifugal'
      end

    end

    # Get all existing chillers and pumps
    chillers = []
    pumps = []
    supplyComponents.each do |sc|
      if sc.to_ChillerElectricEIR.is_initialized
        chillers << sc.to_ChillerElectricEIR.get
      elsif sc.to_PumpConstantSpeed.is_initialized
        pumps << sc.to_PumpConstantSpeed.get
      elsif sc.to_PumpVariableSpeed.is_initialized
        pumps << sc.to_PumpVariableSpeed.get
      end
    end

    # Ensure there is only 1 chiller to start
    first_chiller = nil
    if chillers.size.zero?
      return true
    elsif chillers.size > 1
      OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.PlantLoop', "For #{name}, found #{chillers.size} chillers, cannot split up per performance rating method baseline requirements.")
    else
      first_chiller = chillers[0]
    end

    # Ensure there is only 1 pump to start
    orig_pump = nil
    if pumps.size.zero?
      OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.PlantLoop', "For #{name}, found #{pumps.size} pumps.  A loop must have at least one pump.")
      return false
    elsif pumps.size > 1
      OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.PlantLoop', "For #{name}, found #{pumps.size} pumps, cannot split up per performance rating method baseline requirements.")
      return false
    else
      orig_pump = pumps[0]
    end

    # Determine the per-chiller capacity
    # and sizing factor
    per_chiller_sizing_factor = (1.0 / num_chillers).round(2)
    # This is unused
    per_chiller_cap_tons = cap_tons / num_chillers

    # Set the sizing factor and the chiller type: could do it on the first chiller before cloning it, but renaming warrants looping on chillers anyways

    # Add any new chillers
    final_chillers = [first_chiller]
    (num_chillers - 1).times do
      new_chiller = first_chiller.clone(model)
      if new_chiller.to_ChillerElectricEIR.is_initialized
        new_chiller = new_chiller.to_ChillerElectricEIR.get
      else
        OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.PlantLoop', "For #{name}, could not clone chiller #{first_chiller.name}, cannot apply the performance rating method number of chillers.")
        return false
      end
      # Connect the new chiller to the same CHW loop
      # as the old chiller.
      addSupplyBranchForComponent(new_chiller)
      # Connect the new chiller to the same CW loop
      # as the old chiller, if it was water-cooled.
      cw_loop = first_chiller.secondaryPlantLoop
      if cw_loop.is_initialized
        cw_loop.get.addDemandBranchForComponent(new_chiller)
      end

      final_chillers << new_chiller
    end

    # If there is more than one cooling tower,
    # replace the original pump with a headered pump
    # of the same type and properties.
    if final_chillers.size > 1
      num_pumps = final_chillers.size
      new_pump = nil
      if orig_pump.to_PumpConstantSpeed.is_initialized
        new_pump = OpenStudio::Model::HeaderedPumpsConstantSpeed.new(model)
        new_pump.setNumberofPumpsinBank(num_pumps)
        new_pump.setName("#{orig_pump.name} Bank of #{num_pumps}")
        new_pump.setRatedPumpHead(orig_pump.ratedPumpHead)
        new_pump.setMotorEfficiency(orig_pump.motorEfficiency)
        new_pump.setFractionofMotorInefficienciestoFluidStream(orig_pump.fractionofMotorInefficienciestoFluidStream)
        new_pump.setPumpControlType(orig_pump.pumpControlType)
      elsif orig_pump.to_PumpVariableSpeed.is_initialized
        new_pump = OpenStudio::Model::HeaderedPumpsVariableSpeed.new(model)
        new_pump.setNumberofPumpsinBank(num_pumps)
        new_pump.setName("#{orig_pump.name} Bank of #{num_pumps}")
        new_pump.setRatedPumpHead(orig_pump.ratedPumpHead)
        new_pump.setMotorEfficiency(orig_pump.motorEfficiency)
        new_pump.setFractionofMotorInefficienciestoFluidStream(orig_pump.fractionofMotorInefficienciestoFluidStream)
        new_pump.setPumpControlType(orig_pump.pumpControlType)
        new_pump.setCoefficient1ofthePartLoadPerformanceCurve(orig_pump.coefficient1ofthePartLoadPerformanceCurve)
        new_pump.setCoefficient2ofthePartLoadPerformanceCurve(orig_pump.coefficient2ofthePartLoadPerformanceCurve)
        new_pump.setCoefficient3ofthePartLoadPerformanceCurve(orig_pump.coefficient3ofthePartLoadPerformanceCurve)
        new_pump.setCoefficient4ofthePartLoadPerformanceCurve(orig_pump.coefficient4ofthePartLoadPerformanceCurve)
      end
      # Remove the old pump
      orig_pump.remove
      # Attach the new headered pumps
      new_pump.addToNode(supplyInletNode)
    end

    # Set the sizing factor and the chiller types
    final_chillers.each_with_index do |final_chiller, i|
      final_chiller.setName("#{template} #{chiller_cooling_type} #{chiller_compressor_type} Chiller #{i + 1} of #{final_chillers.size}")
      final_chiller.setSizingFactor(per_chiller_sizing_factor)
      final_chiller.setCondenserType(chiller_cooling_type)
    end
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.PlantLoop', "For #{name}, there are #{final_chillers.size} #{chiller_cooling_type} #{chiller_compressor_type} chillers.")

    # Set the equipment to stage sequentially
    setLoadDistributionScheme('SequentialLoad')

    return true
  end

  def apply_prm_number_of_cooling_towers(template)
    # Skip non-cooling plants
    return true unless sizingPlant.loopType == 'Condenser'

    # Determine the number of chillers
    # already in the model
    num_chillers = model.getChillerElectricEIRs.size

    # Get all existing cooling towers and pumps
    clg_twrs = []
    pumps = []
    supplyComponents.each do |sc|
      if sc.to_CoolingTowerSingleSpeed.is_initialized
        clg_twrs << sc.to_CoolingTowerSingleSpeed.get
      elsif sc.to_CoolingTowerTwoSpeed.is_initialized
        clg_twrs << sc.to_CoolingTowerTwoSpeed.get
      elsif sc.to_CoolingTowerVariableSpeed.is_initialized
        clg_twrs << sc.to_CoolingTowerVariableSpeed.get
      elsif sc.to_PumpConstantSpeed.is_initialized
        pumps << sc.to_PumpConstantSpeed.get
      elsif sc.to_PumpVariableSpeed.is_initialized
        pumps << sc.to_PumpVariableSpeed.get
      end
    end

    # Ensure there is only 1 cooling tower to start
    orig_twr = nil
    if clg_twrs.size.zero?
      return true
    elsif clg_twrs.size > 1
      OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.PlantLoop', "For #{name}, found #{clg_twrs.size} cooling towers, cannot split up per performance rating method baseline requirements.")
      return false
    else
      orig_twr = clg_twrs[0]
    end

    # Ensure there is only 1 pump to start
    orig_pump = nil
    if pumps.size.zero?
      OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.PlantLoop', "For #{name}, found #{pumps.size} pumps.  A loop must have at least one pump.")
      return false
    elsif pumps.size > 1
      OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.PlantLoop', "For #{name}, found #{pumps.size} pumps, cannot split up per performance rating method baseline requirements.")
      return false
    else
      orig_pump = pumps[0]
    end

    # Determine the per-cooling_tower sizing factor
    clg_twr_sizing_factor = (1.0 / num_chillers).round(2)

    # Add a cooling tower for each chiller.
    # Add an accompanying CW pump for each cooling tower.
    final_twrs = [orig_twr]
    new_twr = nil
    (num_chillers - 1).times do
      if orig_twr.to_CoolingTowerSingleSpeed.is_initialized
        new_twr = orig_twr.clone(model)
        new_twr = new_twr.to_CoolingTowerSingleSpeed.get
      elsif orig_twr.to_CoolingTowerTwoSpeed.is_initialized
        new_twr = orig_twr.clone(model)
        new_twr = new_twr.to_CoolingTowerTwoSpeed.get
      elsif orig_twr.to_CoolingTowerVariableSpeed.is_initialized
        # TODO: remove workaround after resolving
        # https://github.com/NREL/OpenStudio/issues/2212
        # Workaround is to create a new tower
        # and replicate all the properties of the first tower.
        new_twr = OpenStudio::Model::CoolingTowerVariableSpeed.new(model)
        new_twr.setName(orig_twr.name.get.to_s)
        new_twr.setDesignInletAirWetBulbTemperature(orig_twr.designInletAirWetBulbTemperature.get)
        new_twr.setDesignApproachTemperature(orig_twr.designApproachTemperature.get)
        new_twr.setDesignRangeTemperature(orig_twr.designRangeTemperature.get)
        new_twr.setFractionofTowerCapacityinFreeConvectionRegime(orig_twr.fractionofTowerCapacityinFreeConvectionRegime.get)
        if orig_twr.fanPowerRatioFunctionofAirFlowRateRatioCurve.is_initialized
          new_twr.setFanPowerRatioFunctionofAirFlowRateRatioCurve(orig_twr.fanPowerRatioFunctionofAirFlowRateRatioCurve.get)
        end
      else
        OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.PlantLoop', "For #{name}, could not clone cooling tower #{orig_twr.name}, cannot apply the performance rating method number of cooling towers.")
        return false
      end

      # Connect the new cooling tower to the CW loop
      addSupplyBranchForComponent(new_twr)
      new_twr_inlet = new_twr.inletModelObject.get.to_Node.get

      final_twrs << new_twr
    end

    # If there is more than one cooling tower,
    # replace the original pump with a headered pump
    # of the same type and properties.
    if final_twrs.size > 1
      num_pumps = final_twrs.size
      new_pump = nil
      if orig_pump.to_PumpConstantSpeed.is_initialized
        new_pump = OpenStudio::Model::HeaderedPumpsConstantSpeed.new(model)
        new_pump.setNumberofPumpsinBank(num_pumps)
        new_pump.setName("#{orig_pump.name} Bank of #{num_pumps}")
        new_pump.setRatedPumpHead(orig_pump.ratedPumpHead)
        new_pump.setMotorEfficiency(orig_pump.motorEfficiency)
        new_pump.setFractionofMotorInefficienciestoFluidStream(orig_pump.fractionofMotorInefficienciestoFluidStream)
        new_pump.setPumpControlType(orig_pump.pumpControlType)
      elsif orig_pump.to_PumpVariableSpeed.is_initialized
        new_pump = OpenStudio::Model::HeaderedPumpsVariableSpeed.new(model)
        new_pump.setNumberofPumpsinBank(num_pumps)
        new_pump.setName("#{orig_pump.name} Bank of #{num_pumps}")
        new_pump.setRatedPumpHead(orig_pump.ratedPumpHead)
        new_pump.setMotorEfficiency(orig_pump.motorEfficiency)
        new_pump.setFractionofMotorInefficienciestoFluidStream(orig_pump.fractionofMotorInefficienciestoFluidStream)
        new_pump.setPumpControlType(orig_pump.pumpControlType)
        new_pump.setCoefficient1ofthePartLoadPerformanceCurve(orig_pump.coefficient1ofthePartLoadPerformanceCurve)
        new_pump.setCoefficient2ofthePartLoadPerformanceCurve(orig_pump.coefficient2ofthePartLoadPerformanceCurve)
        new_pump.setCoefficient3ofthePartLoadPerformanceCurve(orig_pump.coefficient3ofthePartLoadPerformanceCurve)
        new_pump.setCoefficient4ofthePartLoadPerformanceCurve(orig_pump.coefficient4ofthePartLoadPerformanceCurve)
      end
      # Remove the old pump
      orig_pump.remove
      # Attach the new headered pumps
      new_pump.addToNode(supplyInletNode)
    end

    # Set the sizing factors
    final_twrs.each_with_index do |final_cooling_tower, i|
      final_cooling_tower.setName("#{final_cooling_tower.name} #{i + 1} of #{final_twrs.size}")      
      final_cooling_tower.setSizingFactor(clg_twr_sizing_factor)
    end
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.PlantLoop', "For #{name}, there are #{final_twrs.size} cooling towers, one for each chiller.")

    # Set the equipment to stage sequentially
    setLoadDistributionScheme('SequentialLoad')
  end

  # Determines the total rated watts per GPM of the loop
  #
  # @return [Double] rated power consumption per flow
  #   @units Watts per GPM (W*s/m^3)
  def total_rated_w_per_gpm
    sizing_plant = sizingPlant
    loop_type = sizing_plant.loopType

    # Supply W/GPM
    supply_w_per_gpm = 0
    demand_w_per_gpm = 0

    supplyComponents.each do |component|
      if component.to_PumpConstantSpeed.is_initialized
        pump = component.to_PumpConstantSpeed.get
        pump_rated_w_per_gpm = pump.rated_w_per_gpm
        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Pump', "'#{loop_type}' Loop #{name} - Primary (Supply) Constant Speed Pump '#{pump.name}' - pump_rated_w_per_gpm #{pump_rated_w_per_gpm} W/GPM")
        supply_w_per_gpm += pump_rated_w_per_gpm
      elsif component.to_PumpVariableSpeed.is_initialized
        pump = component.to_PumpVariableSpeed.get
        pump_rated_w_per_gpm = pump.rated_w_per_gpm
        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Pump', "'#{loop_type}' Loop #{name} - Primary (Supply) VSD Pump '#{pump.name}' - pump_rated_w_per_gpm #{pump_rated_w_per_gpm} W/GPM")
        supply_w_per_gpm += pump_rated_w_per_gpm
      end
    end

    # Determine if primary only or primary-secondary
    # IF there's a pump on the demand side it's primary-secondary
    demand_pumps = demandComponents('OS:Pump:VariableSpeed'.to_IddObjectType) + demandComponents('OS:Pump:ConstantSpeed'.to_IddObjectType)
    demand_pumps.each do |component|
      if component.to_PumpConstantSpeed.is_initialized
        pump = component.to_PumpConstantSpeed.get
        pump_rated_w_per_gpm = pump.rated_w_per_gpm
        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Pump', "'#{loop_type}' Loop #{name} - Secondary (Demand) Constant Speed Pump '#{pump.name}' - pump_rated_w_per_gpm #{pump_rated_w_per_gpm} W/GPM")
        demand_w_per_gpm += pump_rated_w_per_gpm
      elsif component.to_PumpVariableSpeed.is_initialized
        pump = component.to_PumpVariableSpeed.get
        pump_rated_w_per_gpm = pump.rated_w_per_gpm
        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Pump', "'#{loop_type}' Loop #{name} - Secondary (Demand) VSD Pump '#{pump.name}' - pump_rated_w_per_gpm #{pump_rated_w_per_gpm} W/GPM")
        demand_w_per_gpm += pump_rated_w_per_gpm
      end
    end

    total_rated_w_per_gpm = supply_w_per_gpm + demand_w_per_gpm

    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Loop', "'#{loop_type}' Loop #{name} - Total #{total_rated_w_per_gpm} W/GPM - Supply #{supply_w_per_gpm} W/GPM - Demand #{demand_w_per_gpm} W/GPM")

    return total_rated_w_per_gpm
  end

  # find maximum_loop_flow_rate
  #
  # @return [Double]  maximum_loop_flow_rate m^3/s
  def find_maximum_loop_flow_rate
    # Get the maximum_loop_flow_rate
    maximum_loop_flow_rate = nil
    if maximumLoopFlowRate.is_initialized
      maximum_loop_flow_rate = maximumLoopFlowRate.get
    elsif autosizedMaximumLoopFlowRate.is_initialized
      maximum_loop_flow_rate = autosizedMaximumLoopFlowRate.get
    else
      OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.PlantLoop', "For #{name} maximum loop flow rate is not available.")
    end

    return maximum_loop_flow_rate
  end

  # Determines if the loop is a Service Water Heating loop by checking if there is a WaterUseConnection on the demand side
  #
  # @return [Boolean] true if it's indeed a SHW loop, false otherwise
  def swh_loop?()

    serves_swh = false
    self.demandComponents.each do |comp|
      if comp.to_WaterUseConnections.is_initialized
        serves_swh = true
        break
      end
    end

    return serves_swh
  end
  
  # Classifies the service water system and returns information
  # about fuel types, whether it serves both heating and service water heating,
  # the water storage volume, and the total heating capacity.
  #
  # @return [Array<Array<String>, Bool, Double, Double>] An array of:
  # fuel types, combination_system (true/false), storage_capacity (m^3), total_heating_capacity (W)
  def swh_system_type
    combination_system = true
    storage_capacity = 0
    primary_fuels = []
    secondary_fuels = []

    # @Todo: to work correctly, plantloop.total_heating_capacity requires to have either hardsized capacities or a sizing run.
    primary_heating_capacity = total_heating_capacity
    secondary_heating_capacity = 0

    supplyComponents.each do |component|

      # Get the object type
      obj_type = component.iddObjectType.valueName.to_s

      case obj_type
      when 'OS_DistrictHeating'
        primary_fuels << 'DistrictHeating'
        combination_system = false
      when 'OS_HeatPump_WaterToWater_EquationFit_Heating'
        primary_fuels << 'Electricity'
      when 'OS_SolarCollector_FlatPlate_PhotovoltaicThermal'
        primary_fuels << 'SolarEnergy'
      when 'OS_SolarCollector_FlatPlate_Water'
        primary_fuels << 'SolarEnergy'
      when 'OS_SolarCollector_IntegralCollectorStorage'
        primary_fuels << 'SolarEnergy'
      when 'OS_WaterHeater_HeatPump'
        primary_fuels << 'Electricity'
      when 'OS_WaterHeater_Mixed'
        component = component.to_WaterHeaterMixed.get
        # Check it it's actually a heater, not just a storage tank
        if component.heaterMaximumCapacity.empty? || component.heaterMaximumCapacity.get != 0
          # If it does, we add the heater Fuel Type
          primary_fuels << component.heaterFuelType
          # And in this case we'll reuse this object
          combination_system = false
        end  # @Todo: not sure about whether it should be an elsif or not
        # Check the plant loop connection on the source side
        if component.secondaryPlantLoop.is_initialized
          source_plant_loop = component.secondaryPlantLoop.get
          secondary_fuels += model.plant_loop_heating_fuels(source_plant_loop)
          secondary_heating_capacity += source_plant_loop.total_heating_capacity
        end

        # Storage capacity
        if component.tankVolume.is_initialized
          storage_capacity = component.tankVolume.get
        end

      when 'OS_WaterHeater_Stratified'
        component = component.to_WaterHeaterStratified.get

        # Check if the heater actually has a capacity (otherwise it's simply a Storage Tank)
        if component.heaterMaximumCapacity.empty? || component.heaterMaximumCapacity.get != 0
          # If it does, we add the heater Fuel Type
          primary_fuels << component.heaterFuelType
          # And in this case we'll reuse this object
          combination_system = false
        end # @Todo: not sure about whether it should be an elsif or not
        # Check the plant loop connection on the source side
        if component.secondaryPlantLoop.is_initialized
          source_plant_loop = component.secondaryPlantLoop.get
          secondary_fuels += model.plant_loop_heating_fuels(source_plant_loop)
          secondary_heating_capacity += source_plant_loop.total_heating_capacity
        end

        # Storage capacity
        if component.tankVolume.is_initialized
          storage_capacity = component.tankVolume.get
        end

      when 'OS_HeatExchanger_FluidToFluid'
        hx = component.to_HeatExchangerFluidToFluid.get
        cooling_hx_control_types = ["CoolingSetpointModulated", "CoolingSetpointOnOff", "CoolingDifferentialOnOff", "CoolingSetpointOnOffWithComponentOverride"]
        cooling_hx_control_types.each {|x| x.downcase!}
        if !cooling_hx_control_types.include?(hx.controlType.downcase) && hx.secondaryPlantLoop.is_initialized
          source_plant_loop = hx.secondaryPlantLoop.get
          secondary_fuels += model.plant_loop_heating_fuels(source_plant_loop)
          secondary_heating_capacity += source_plant_loop.total_heating_capacity
        end

      when 'OS_Node', 'OS_Pump_ConstantSpeed', 'OS_Pump_VariableSpeed', 'OS_Connector_Splitter', 'OS_Connector_Mixer', 'OS_Pipe_Adiabatic'
        # To avoid extraneous debug messages
      else
        #OpenStudio::logFree(OpenStudio::Debug, 'openstudio.sizing.Model', "No heating fuel types found for #{obj_type}")
      end

    end

    # @Todo: decide how to handle primary and secondary stuff
    fuels = primary_fuels + secondary_fuels
    total_heating_capacity = primary_heating_capacity + secondary_heating_capacity
    # If the primary heating capacity is bigger than secondary, assume the secondary is just a backup and disregard it?
    # if primary_heating_capacity > secondary_heating_capacity
    #   total_heating_capacity = primary_heating_capacity
    #   fuels = primary_fuels
    # end

    return fuels.uniq.sort, combination_system, storage_capacity, total_heating_capacity

  end # end classify_swh_system_type  
  
end