From e702c32b824451f0842ee7ba88082b2b516814ca Mon Sep 17 00:00:00 2001 From: gonz102 Date: Wed, 8 May 2024 12:45:26 -0400 Subject: [PATCH 01/11] OSSTD-458 - Added EMS for staging multiple chillers using primary/secondary configurations. --- .../common/objects/Prototype.hvac_systems.rb | 52 ++- .../standards/Standards.Model.rb | 2 +- .../standards/Standards.PlantLoop.rb | 3 +- .../ashrae_90_1_prm.PlantLoop.rb | 401 +++++++++++++----- 4 files changed, 341 insertions(+), 117 deletions(-) diff --git a/lib/openstudio-standards/prototypes/common/objects/Prototype.hvac_systems.rb b/lib/openstudio-standards/prototypes/common/objects/Prototype.hvac_systems.rb index df172cf6e4..b891a21450 100644 --- a/lib/openstudio-standards/prototypes/common/objects/Prototype.hvac_systems.rb +++ b/lib/openstudio-standards/prototypes/common/objects/Prototype.hvac_systems.rb @@ -309,10 +309,15 @@ def model_add_chw_loop(model, secondary_chilled_water_loop.setName(secondary_loop_name) chw_sizing_control(model, secondary_chilled_water_loop, dsgn_sup_wtr_temp, dsgn_sup_wtr_temp_delt) chilled_water_loop.additionalProperties.setFeature('is_primary_loop', true) + chilled_water_loop.additionalProperties.setFeature('secondary_loop_name', secondary_chilled_water_loop.name.to_s) secondary_chilled_water_loop.additionalProperties.setFeature('is_secondary_loop', true) - # primary chilled water pump + # primary chilled water pumps are added when adding chillers # Add Constant pump, in plant loop, the number of chiller adjustment will assign pump to each chiller - pri_chw_pump = OpenStudio::Model::PumpConstantSpeed.new(model) + #pri_chw_pump = OpenStudio::Model::PumpConstantSpeed.new(model) + pri_chw_pump = OpenStudio::Model::PumpVariableSpeed.new(model) + pump_variable_speed_set_control_type(pri_chw_pump, control_type = 'Riding Curve') + # This pump name is important for function add_ems_for_multiple_chiller_pumps_w_secondary_plant. If you update + # it here, you must update the logic there to account for this pri_chw_pump.setName("#{chilled_water_loop.name} Primary Pump") # Will need to adjust the pump power after a sizing run pri_chw_pump.setRatedPumpHead(OpenStudio.convert(15.0, 'ftH_{2}O', 'Pa').get / num_chillers) @@ -375,11 +380,36 @@ def model_add_chw_loop(model, dist_clg.autosizeNominalCapacity chilled_water_loop.addSupplyBranchForComponent(dist_clg) else + + # use default efficiency from 90.1-2019 + # 1.188 kw/ton for a 150 ton AirCooled chiller + # 0.66 kw/ton for a 150 ton Water Cooled positive displacement chiller + case chiller_cooling_type + when 'AirCooled' + default_cop = kw_per_ton_to_cop(1.188) + when 'WaterCooled' + default_cop = kw_per_ton_to_cop(0.66) + else + default_cop = kw_per_ton_to_cop(0.66) + end + + # Create list of consolidated chiller names. This will reduce the number of chillers to no greater than 3 chillers + chiller_name_list = ["#{chiller_condenser_type}_first_stage"] + if num_chillers > 3 + OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.PlantLoop', "EMS Code for multiple chiller pump has not been written for greater than 3 chillers. This has #{num_of_chillers} chillers") + elsif num_chillers == 3 + chiller_name_list << "#{chiller_condenser_type}_second_stage_1" + chiller_name_list << "#{chiller_condenser_type}_second_stage_2" + elsif num_chillers == 2 + chiller_name_list << "#{chiller_condenser_type}_second_stage" + end + # make the correct type of chiller based these properties - chiller_sizing_factor = (1.0 / num_chillers).round(2) - num_chillers.times do |i| + chiller_sizing_factor = (1.0 / chiller_name_list.length).round(2) + + chiller_name_list.each do |chiller_name| chiller = OpenStudio::Model::ChillerElectricEIR.new(model) - chiller.setName("#{template} #{chiller_cooling_type} #{chiller_condenser_type} #{chiller_compressor_type} Chiller #{i}") + chiller.setName("#{template} #{chiller_cooling_type} #{chiller_condenser_type} #{chiller_name}") chilled_water_loop.addSupplyBranchForComponent(chiller) dsgn_sup_wtr_temp_c = OpenStudio.convert(dsgn_sup_wtr_temp, 'F', 'C').get chiller.setReferenceLeavingChilledWaterTemperature(dsgn_sup_wtr_temp_c) @@ -391,18 +421,6 @@ def model_add_chw_loop(model, chiller.setMinimumUnloadingRatio(0.25) chiller.setChillerFlowMode('ConstantFlow') chiller.setSizingFactor(chiller_sizing_factor) - - # use default efficiency from 90.1-2019 - # 1.188 kw/ton for a 150 ton AirCooled chiller - # 0.66 kw/ton for a 150 ton Water Cooled positive displacement chiller - case chiller_cooling_type - when 'AirCooled' - default_cop = kw_per_ton_to_cop(1.188) - when 'WaterCooled' - default_cop = kw_per_ton_to_cop(0.66) - else - default_cop = kw_per_ton_to_cop(0.66) - end chiller.setReferenceCOP(default_cop) # connect the chiller to the condenser loop if one was supplied diff --git a/lib/openstudio-standards/standards/Standards.Model.rb b/lib/openstudio-standards/standards/Standards.Model.rb index 41cf143614..a99b1bed9c 100644 --- a/lib/openstudio-standards/standards/Standards.Model.rb +++ b/lib/openstudio-standards/standards/Standards.Model.rb @@ -445,7 +445,7 @@ def model_create_prm_any_baseline_building(user_model, building_type, climate_zo next if plant_loop_swh_loop?(plant_loop) plant_loop_apply_prm_number_of_boilers(plant_loop) - plant_loop_apply_prm_number_of_chillers(plant_loop, sizing_run_dir) + plant_loop_apply_prm_number_of_chillers(model, plant_loop) end # Set the baseline number of cooling towers diff --git a/lib/openstudio-standards/standards/Standards.PlantLoop.rb b/lib/openstudio-standards/standards/Standards.PlantLoop.rb index 62f544e97e..0b93f5f554 100644 --- a/lib/openstudio-standards/standards/Standards.PlantLoop.rb +++ b/lib/openstudio-standards/standards/Standards.PlantLoop.rb @@ -55,7 +55,8 @@ def chw_sizing_control(model, chilled_water_loop, dsgn_sup_wtr_temp, dsgn_sup_wt # @param model [OpenStudio::Model::Model] OpenStudio model object # @return [String] common_pipe or heat_exchanger def plant_loop_set_chw_pri_sec_configuration(model) - pri_sec_config = 'common_pipe' + # pri_sec_config = 'common_pipe' + pri_sec_config = 'heat_exchanger' return pri_sec_config end diff --git a/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.PlantLoop.rb b/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.PlantLoop.rb index a221b370cb..16ca10991d 100644 --- a/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.PlantLoop.rb +++ b/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.PlantLoop.rb @@ -91,22 +91,16 @@ def plant_loop_apply_prm_number_of_cooling_towers(plant_loop) end # Splits the single chiller used for the initial sizing run - # into multiple separate chillers based on Appendix G. - # + # into multiple separate chillers based on Appendix G. Also applies + # EMS to stage chillers properly # @param plant_loop [OpenStudio::Model::PlantLoop] chilled water loop - # @param sizing_run_dir [String] sizing run directory # @return [Boolean] returns true if successful, false if not - def plant_loop_apply_prm_number_of_chillers(plant_loop, sizing_run_dir = nil) + def plant_loop_apply_prm_number_of_chillers(model, plant_loop) # Skip non-cooling plants & secondary cooling loop return true unless plant_loop.sizingPlant.loopType == 'Cooling' # If the loop is cooling but it is a secondary loop, then skip. return true if plant_loop.additionalProperties.hasFeature('is_secondary_loop') - # Determine the number and type of chillers - num_chillers = nil - chiller_cooling_type = nil - chiller_compressor_type = nil - # Set the equipment to stage sequentially or uniformload if there is secondary loop if plant_loop.additionalProperties.hasFeature('is_primary_loop') plant_loop.setLoadDistributionScheme('UniformLoad') @@ -114,9 +108,53 @@ def plant_loop_apply_prm_number_of_chillers(plant_loop, sizing_run_dir = nil) plant_loop.setLoadDistributionScheme('SequentialLoad') end + # Get all existing chillers and pumps. Copy chiller properties needed when duplicating existing settings + chillers = [] + pumps = [] + default_cop = nil + condenser_water_loop = nil + dsgn_sup_wtr_temp_c = nil + + plant_loop.supplyComponents.each do |sc| + if sc.to_ChillerElectricEIR.is_initialized + chiller = sc.to_ChillerElectricEIR.get + + # Copy the last chillers COP, leaving chilled water temperature, and reference cooling tower. These will be the + # default for any extra chillers. + default_cop = chiller.referenceCOP + dsgn_sup_wtr_temp_c = chiller.referenceLeavingChilledWaterTemperature + condenser_water_loop = chiller.condenserWaterLoop + chillers << chiller + + elsif sc.to_PumpConstantSpeed.is_initialized + pumps << sc.to_PumpConstantSpeed.get + elsif sc.to_PumpVariableSpeed.is_initialized + pumps << sc.to_PumpVariableSpeed.get + end + end + + # Get existing plant loop pump. We'll copy this pumps parameters before removing it. Throw exception for multiple pumps on supply side + if pumps.size.zero? + OpenStudio.logFree(OpenStudio::Error, 'prm.log', "For #{plant_loop.name}, found #{pumps.size} pumps. A loop must have at least one pump.") + return false + elsif pumps.size > 1 + OpenStudio.logFree(OpenStudio::Error, 'prm.log', "For #{plant_loop.name}, found #{pumps.size} pumps, cannot split up per performance rating method baseline requirements.") + return false + else + original_pump = pumps[0] + end + + return true if chillers.empty? + # Determine the capacity of the loop cap_w = plant_loop_total_cooling_capacity(plant_loop) cap_tons = OpenStudio.convert(cap_w, 'W', 'ton').get + + # Throw exception for > 2,400 tons as this breaks our staging strategy cap of 3 chillers + if cap_tons > 2400 + OpenStudio.logFree(OpenStudio::Error, 'prm.log', "For #{plant_loop.name}, the total capacity (#{cap_w}) exceeded 2400 tons and would require more than 3 chillers. The existing code base cannot accommodate the staging required for this") + end + if cap_tons <= 300 num_chillers = 1 chiller_cooling_type = 'WaterCooled' @@ -135,116 +173,283 @@ def plant_loop_apply_prm_number_of_chillers(plant_loop, sizing_run_dir = nil) chiller_compressor_type = 'Centrifugal' end - # Get all existing chillers and pumps - chillers = [] - pumps = [] - plant_loop.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 + if chillers.length > num_chillers + OpenStudio.logFree(OpenStudio::Error, 'prm.log', "For #{plant_loop.name}, the existing number of chillers exceeds the recommended amount. We have not accounted for this in the codebase yet.") + end + + # Determine the per-chiller capacity and sizing factor + per_chiller_sizing_factor = (1.0 / num_chillers).round(2) + per_chiller_cap_w = cap_w / num_chillers + + # Set the sizing factor and the chiller types + # chillers.each_with_index do |chiller, i| + for i in 0..num_chillers - 1 + # if not enough chillers exist, create a new one. Else reference the i'th chiller + if i <= chillers.length - 1 + chiller = chillers[i] + else + chiller = OpenStudio::Model::ChillerElectricEIR.new(model) + plant_loop.addSupplyBranchForComponent(chiller) + chiller.setReferenceLeavingChilledWaterTemperature(dsgn_sup_wtr_temp_c) + chiller.setLeavingChilledWaterLowerTemperatureLimit(OpenStudio.convert(36.0, 'F', 'C').get) + chiller.setReferenceEnteringCondenserFluidTemperature(OpenStudio.convert(95.0, 'F', 'C').get) + chiller.setMinimumPartLoadRatio(0.15) + chiller.setMaximumPartLoadRatio(1.0) + chiller.setOptimumPartLoadRatio(1.0) + chiller.setMinimumUnloadingRatio(0.25) + chiller.setChillerFlowMode('ConstantFlow') + chiller.setReferenceCOP(default_cop) + + condenser_water_loop.get.addDemandBranchForComponent(chiller) if condenser_water_loop.is_initialized + end + + chiller.setName("#{template} #{chiller_cooling_type} #{chiller_compressor_type} Chiller #{i + 1} of #{num_chillers}") + chiller.setSizingFactor(per_chiller_sizing_factor) + chiller.setReferenceCapacity(per_chiller_cap_w) + chiller.setCondenserType(chiller_cooling_type) + chiller.additionalProperties.setFeature('compressor_type', chiller_compressor_type) + + # Add inlet pump + new_pump = OpenStudio::Model::PumpVariableSpeed.new(plant_loop.model) + new_pump.setName("#{chiller.name} Inlet Pump") + new_pump.setRatedPumpHead(original_pump.ratedPumpHead / num_chillers) + new_pump.setCoefficient1ofthePartLoadPerformanceCurve(original_pump.coefficient1ofthePartLoadPerformanceCurve) + new_pump.setCoefficient2ofthePartLoadPerformanceCurve(original_pump.coefficient2ofthePartLoadPerformanceCurve) + new_pump.setCoefficient3ofthePartLoadPerformanceCurve(original_pump.coefficient3ofthePartLoadPerformanceCurve) + new_pump.setCoefficient4ofthePartLoadPerformanceCurve(original_pump.coefficient4ofthePartLoadPerformanceCurve) + chiller_inlet_node = chiller.connectedObject(chiller.supplyInletPort).get.to_Node.get + new_pump.addToNode(chiller_inlet_node) + end - # Ensure there is only 1 chiller to start - first_chiller = nil - return true if chillers.size.zero? + # Remove original pump, dedicated chiller pumps have all been added + original_pump.remove - if chillers.size > 1 - OpenStudio.logFree(OpenStudio::Error, 'prm.log', "For #{plant_loop.name}, found #{chillers.size} chillers, cannot split up per performance rating method baseline requirements.") - else - first_chiller = chillers[0] + OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.PlantLoop', "For #{plant_loop.name}, there are #{chillers.size} #{chiller_cooling_type} #{chiller_compressor_type} chillers.") + + # Check for a heat exchanger fluid to fluid-- that lets you know if this is a primary loop + has_secondary_plant_loop = !plant_loop.demandComponents(OpenStudio::Model::HeatExchangerFluidToFluid.iddObjectType).empty? + + if has_secondary_plant_loop + # Add EMS to stage chillers if there's a primary/secondary configuration + if num_chillers > 3 + OpenStudio.logFree(OpenStudio::Error, 'prm.log', "For #{plant_loop.name} has more than 3 chillers. We do not have an EMS strategy for that yet.") + elsif num_chillers > 1 + add_ems_for_multiple_chiller_pumps_w_secondary_plant(model, plant_loop) + else + OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.PlantLoop', "No EMS for multiple chillers required for #{plant_loop.name}, as there's only 1 chiller.") + end end - # Ensure there is only 1 pump to start - orig_pump = nil - if pumps.size.zero? - OpenStudio.logFree(OpenStudio::Error, 'prm.log', "For #{plant_loop.name}, found #{pumps.size} pumps. A loop must have at least one pump.") - return false - elsif pumps.size > 1 - OpenStudio.logFree(OpenStudio::Error, 'prm.log', "For #{plant_loop.name}, found #{pumps.size} pumps, cannot split up per performance rating method baseline requirements.") - return false - else - orig_pump = pumps[0] + return true + end + + # Adds EMS program for pumps serving 3 chillers on primary + secondary loop. This was due to an issue when modeling two + # dedicated loops. The headered pumps or dedicated constant speed pumps operate at full flow as long as there's a + # load on the loop unless this EMS is in place. + # @param model [OpenStudio::Model] OpenStudio model with plant loops + # @param primary_plant [OpenStudio::Model::PlantLoop] Primary chilled water loop with chillers + def add_ems_for_multiple_chiller_pumps_w_secondary_plant(model, primary_plant) + + # Aggregate array of chillers on primary plant supply side + chiller_list = [] + + primary_plant.supplyComponents.each do |sc| + if sc.to_ChillerElectricEIR.is_initialized + chiller_list << sc.to_ChillerElectricEIR.get + end 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 - per_chiller_cap_w = cap_w / num_chillers + num_of_chillers = chiller_list.length # Either 2 or 3 - # 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 + return if num_of_chillers <= 1 - # Add any new chillers - final_chillers = [first_chiller] - (num_chillers - 1).times do - new_chiller = first_chiller.clone(plant_loop.model) - if new_chiller.to_ChillerElectricEIR.is_initialized - new_chiller = new_chiller.to_ChillerElectricEIR.get - else - OpenStudio.logFree(OpenStudio::Error, 'prm.log', "For #{plant_loop.name}, could not clone chiller #{first_chiller.name}, cannot apply the performance rating method number of chillers.") - return false + plant_name = primary_plant.name.to_s + + # Make a variable to track the chilled water demand + chw_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Plant Supply Side Cooling Demand Rate') + chw_sensor.setKeyName(plant_name) + chw_sensor.setName("#{plant_name.gsub(/[-\s]+/, '_')}_CHW_DEMAND") + + sorted_chiller_list = Array.new(num_of_chillers) + + if num_of_chillers >= 3 + chiller_list.each_with_index do |chiller, i| + sorted_chiller_list[0] = chiller if chiller.name.to_s.include? 'first_stage' + sorted_chiller_list[1] = chiller if chiller.name.to_s.include? 'second_stage_1' + sorted_chiller_list[2] = chiller if chiller.name.to_s.include? 'second_stage_2' end - # Connect the new chiller to the same CHW loop - # as the old chiller. - plant_loop.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) + else + # 2 chiller setups are simply sorted such that the small chiller is staged first + if chiller_list[0].referenceCapacity.get > chiller_list[1].referenceCapacity.get + sorted_chiller_list[0] = chiller_list[1] + sorted_chiller_list[1] = chiller_list[0] + else + sorted_chiller_list[0] = chiller_list[0] + sorted_chiller_list[1] = chiller_list[1] end - final_chillers << new_chiller end - # If there is more than one cooling tower, - # add one pump to each chiller, assume chillers are equally sized - if final_chillers.size > 1 - num_pumps = final_chillers.size - final_chillers.each do |chiller| - if orig_pump.to_PumpConstantSpeed.is_initialized - new_pump = OpenStudio::Model::PumpConstantSpeed.new(plant_loop.model) - new_pump.setName("#{chiller.name} Primary Pump") - # Will need to adjust the pump power after a sizing run - new_pump.setRatedPumpHead(orig_pump.ratedPumpHead / num_pumps) - new_pump.setMotorEfficiency(0.9) - new_pump.setPumpControlType('Intermittent') - chiller_inlet_node = chiller.connectedObject(chiller.supplyInletPort).get.to_Node.get - new_pump.addToNode(chiller_inlet_node) - elsif orig_pump.to_PumpVariableSpeed.is_initialized - new_pump = OpenStudio::Model::PumpVariableSpeed.new(plant_loop.model) - new_pump.setName("#{chiller.name} Primary Pump") - new_pump.setRatedPumpHead(orig_pump.ratedPumpHead / num_pumps) - new_pump.setCoefficient1ofthePartLoadPerformanceCurve(orig_pump.coefficient1ofthePartLoadPerformanceCurve) - new_pump.setCoefficient2ofthePartLoadPerformanceCurve(orig_pump.coefficient2ofthePartLoadPerformanceCurve) - new_pump.setCoefficient3ofthePartLoadPerformanceCurve(orig_pump.coefficient3ofthePartLoadPerformanceCurve) - new_pump.setCoefficient4ofthePartLoadPerformanceCurve(orig_pump.coefficient4ofthePartLoadPerformanceCurve) - chiller_inlet_node = chiller.connectedObject(chiller.supplyInletPort).get.to_Node.get - new_pump.addToNode(chiller_inlet_node) - end - end - # Remove the old pump - orig_pump.remove + + # Make pump specific parameters for EMS. Use counter + sorted_chiller_list.each_with_index do |chiller, i| + + # Get chiller pump + pump_name = "#{chiller.name} Inlet Pump" + pump = model.getPumpVariableSpeedByName(pump_name).get + + # Set EMS names + ems_pump_flow_name = "CHILLER_PUMP_#{i + 1}_FLOW" + ems_pump_status_name = "CHILLER_PUMP_#{i + 1}_STATUS" + ems_pump_design_flow_name = "CHILLER_PUMP_#{i + 1}_DES_FLOW" + + # ---- Actuators ---- + + # Pump Flow Actuator + actuator_pump_flow = OpenStudio::Model::EnergyManagementSystemActuator.new(pump, 'Pump', 'Pump Mass Flow Rate') + actuator_pump_flow.setName(ems_pump_flow_name) + + # Pump Status Actuator + actuator_pump_status = OpenStudio::Model::EnergyManagementSystemActuator.new(pump, + 'Plant Component Pump:VariableSpeed', + 'On/Off Supervisory') + actuator_pump_status.setName(ems_pump_status_name) + + # ---- Internal Variable ---- + + internal_variable = OpenStudio::Model::EnergyManagementSystemInternalVariable.new(model, 'Pump Maximum Mass Flow Rate') + internal_variable.setInternalDataIndexKeyName(pump_name) + internal_variable.setName(ems_pump_design_flow_name) + 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.setReferenceCapacity(per_chiller_cap_w) - final_chiller.setCondenserType(chiller_cooling_type) - final_chiller.additionalProperties.setFeature('compressor_type', chiller_compressor_type) + # Write EMS program + if num_of_chillers > 3 + OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.PlantLoop', "EMS Code for multiple chiller pump has not been written for greater than 2 chillers. This has #{num_of_chillers} chillers") + elsif num_of_chillers == 3 + add_ems_program_for_3_pump_chiller_plant(model, sorted_chiller_list, primary_plant) + elsif num_of_chillers == 2 + add_ems_program_for_2_pump_chiller_plant(model, sorted_chiller_list, primary_plant) end - OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.PlantLoop', "For #{plant_loop.name}, there are #{final_chillers.size} #{chiller_cooling_type} #{chiller_compressor_type} chillers.") - return true end + # Adds EMS program for pumps serving 2 chillers on primary + secondary loop. This was due to an issue when modeling two + # dedicated loops. The headered pumps or dedicated constant speed pumps operate at full flow as long as there's a + # load on the loop unless this EMS is in place. + # @param model [OpenStudio::Model] OpenStudio model with plant loops + # @param sorted_chiller_list [Array] Array of chillers in primary_plant sorted by capacity + # @param primary_plant [OpenStudio::Model::PlantLoop] Primary chilled water loop with chillers + def add_ems_program_for_2_pump_chiller_plant(model, sorted_chiller_list, primary_plant) + + plant_name = primary_plant.name.to_s + + # Break out sorted chillers and get their respective capacities + small_chiller = sorted_chiller_list[0] + large_chiller = sorted_chiller_list[1] + + capacity_small_chiller = small_chiller.referenceCapacity.get + capacity_large_chiller = large_chiller.referenceCapacity.get + + chw_demand = "#{primary_plant.name.to_s.gsub(/[-\s]+/, '_')}_CHW_DEMAND" + + ems_pump_program = OpenStudio::Model::EnergyManagementSystemProgram.new(model) + ems_pump_program.setName("#{plant_name.gsub(/[-\s]+/, '_')}_Pump_EMS") + ems_pump_program.addLine('SET CHILLER_PUMP_1_STATUS = NULL, !- Program Line 1') + ems_pump_program.addLine('SET CHILLER_PUMP_2_STATUS = NULL, !- Program Line 2') + ems_pump_program.addLine('SET CHILLER_PUMP_1_FLOW = NULL, !- A3') + ems_pump_program.addLine('SET CHILLER_PUMP_2_FLOW = NULL, !- A4') + ems_pump_program.addLine("IF #{chw_demand} <= #{0.8 * capacity_small_chiller}, !- A5") + ems_pump_program.addLine('SET CHILLER_PUMP_2_STATUS = 0, !- A6') + ems_pump_program.addLine('SET CHILLER_PUMP_2_FLOW = 0, !- A7') + ems_pump_program.addLine("ELSEIF #{chw_demand} <= #{capacity_large_chiller}, !- A8") + ems_pump_program.addLine('SET CHILLER_PUMP_1_STATUS = 0, !- A9') + ems_pump_program.addLine('SET CHILLER_PUMP_2_STATUS = 1, !- A10') + ems_pump_program.addLine('SET CHILLER_PUMP_1_FLOW = 0, !- A11') + ems_pump_program.addLine('SET CHILLER_PUMP_2_FLOW = CHILLER_PUMP_2_DES_FLOW, !- A12') + ems_pump_program.addLine("ELSEIF #{chw_demand} > #{capacity_small_chiller + capacity_large_chiller}, !- A13") + ems_pump_program.addLine('SET CHILLER_PUMP_1_STATUS = 1, !- A14') + ems_pump_program.addLine('SET CHILLER_PUMP_2_STATUS = 1, !- A15') + ems_pump_program.addLine('SET CHILLER_PUMP_1_FLOW = CHILLER_PUMP_1_DES_FLOW, !- A16') + ems_pump_program.addLine('SET CHILLER_PUMP_2_FLOW = CHILLER_PUMP_2_DES_FLOW, !- A17') + ems_pump_program.addLine('ENDIF !- A18') + + ems_pump_program_manager = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model) + ems_pump_program_manager.setName("#{plant_name.gsub(/[-\s]+/, '_')}_Pump_Program_Manager") + ems_pump_program_manager.setCallingPoint('InsideHVACSystemIterationLoop') + ems_pump_program_manager.addProgram(ems_pump_program) + + end + + def add_ems_program_for_3_pump_chiller_plant(model, sorted_chiller_list, primary_plant) + + plant_name = primary_plant.name.to_s + + # Break out sorted chillers and get their respective capacities + primary_chiller = sorted_chiller_list[0] + medium_chiller = sorted_chiller_list[1] + large_chiller = sorted_chiller_list[2] + + capacity_80_pct_small = 0.8 * primary_chiller.referenceCapacity.get + capacity_medium_chiller = medium_chiller.referenceCapacity.get + capacity_large_chiller = large_chiller.referenceCapacity.get + + if capacity_80_pct_small >= capacity_medium_chiller + first_stage_capacity = capacity_medium_chiller + else + first_stage_capacity = capacity_80_pct_small + end + + chw_demand = "#{primary_plant.name.to_s.gsub(/[-\s]+/, '_')}_CHW_DEMAND" + + ems_pump_program = OpenStudio::Model::EnergyManagementSystemProgram.new(model) + ems_pump_program.setName("#{plant_name.gsub(/[-\s]+/, '_')}_Pump_EMS") + ems_pump_program.addLine('SET CHILLER_PUMP_1_STATUS = NULL, !- Program Line 1') + ems_pump_program.addLine('SET CHILLER_PUMP_2_STATUS = NULL, !- Program Line 2') + ems_pump_program.addLine('SET CHILLER_PUMP_3_STATUS = NULL, !- A4') + ems_pump_program.addLine('SET CHILLER_PUMP_1_FLOW = NULL, !- A5') + ems_pump_program.addLine('SET CHILLER_PUMP_2_FLOW = NULL, !- A6') + ems_pump_program.addLine('SET CHILLER_PUMP_3_FLOW = NULL, !- A7') + ems_pump_program.addLine("IF #{chw_demand} <= #{first_stage_capacity}, !- A8") + ems_pump_program.addLine('SET CHILLER_PUMP_2_STATUS = 0, !- A9') + ems_pump_program.addLine('SET CHILLER_PUMP_3_STATUS = 0, !- A10') + ems_pump_program.addLine('SET CHILLER_PUMP_2_FLOW = 0, !- A11') + ems_pump_program.addLine('SET CHILLER_PUMP_3_FLOW = 0, !- A12') + + if capacity_80_pct_small < capacity_medium_chiller + ems_pump_program.addLine("ELSEIF #{chw_demand} <= #{capacity_medium_chiller}, !- A13") + ems_pump_program.addLine('SET CHILLER_PUMP_1_STATUS = 0, !- A14') + ems_pump_program.addLine('SET CHILLER_PUMP_2_STATUS = 1, !- A15') + ems_pump_program.addLine('SET CHILLER_PUMP_3_STATUS = 0, !- A16') + ems_pump_program.addLine('SET CHILLER_PUMP_1_FLOW = 0, !- A17') + ems_pump_program.addLine('SET CHILLER_PUMP_2_FLOW = CHILLER_PUMP_2_DES_FLOW, !- A18') + ems_pump_program.addLine('SET CHILLER_PUMP_3_FLOW = 0, !- A19') + end + + ems_pump_program.addLine("ELSEIF #{chw_demand} <= #{capacity_medium_chiller + capacity_large_chiller}, !- A20") + ems_pump_program.addLine('SET CHILLER_PUMP_1_STATUS = 0, !- A21') + ems_pump_program.addLine('SET CHILLER_PUMP_2_STATUS = 1, !- A22') + ems_pump_program.addLine('SET CHILLER_PUMP_3_STATUS = 1, !- A23') + ems_pump_program.addLine('SET CHILLER_PUMP_1_FLOW = 0, !- A24') + ems_pump_program.addLine('SET CHILLER_PUMP_2_FLOW = CHILLER_PUMP_2_DES_FLOW, !- A25') + ems_pump_program.addLine('SET CHILLER_PUMP_3_FLOW = CHILLER_PUMP_3_DES_FLOW, !- A26') + ems_pump_program.addLine("ELSEIF #{chw_demand} > #{capacity_medium_chiller + capacity_large_chiller}, !- A27") + ems_pump_program.addLine('SET CHILLER_PUMP_1_STATUS = 1, !- A28') + ems_pump_program.addLine('SET CHILLER_PUMP_2_STATUS = 1, !- A29') + ems_pump_program.addLine('SET CHILLER_PUMP_3_STATUS = 1, !- A30') + ems_pump_program.addLine('SET CHILLER_PUMP_1_FLOW = CHILLER_PUMP_1_DES_FLOW, !- A31') + ems_pump_program.addLine('SET CHILLER_PUMP_2_FLOW = CHILLER_PUMP_2_DES_FLOW, !- A32') + ems_pump_program.addLine('SET CHILLER_PUMP_3_FLOW = CHILLER_PUMP_3_DES_FLOW, !- A33') + ems_pump_program.addLine('ENDIF !- A34') + + ems_pump_program_manager = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model) + ems_pump_program_manager.setName("#{plant_name.gsub(/[-\s]+/, '_')}_Pump_Program_Manager") + ems_pump_program_manager.setCallingPoint('InsideHVACSystemIterationLoop') + ems_pump_program_manager.addProgram(ems_pump_program) + + end # Apply prm baseline pump power # @note I think it makes more sense to sense the motor efficiency right there... # But actually it's completely irrelevant... From defb5f4365d9cd685949b5e8e74c7ff846a5584d Mon Sep 17 00:00:00 2001 From: gonz102 Date: Mon, 3 Jun 2024 12:26:47 -0400 Subject: [PATCH 02/11] Added chilled water loop operation scheme for primary/secondary staged chillers. This should fix issue where EMS for staged chiller pumps were not performing as expected. --- .../common/objects/Prototype.hvac_systems.rb | 4 +- .../ashrae_90_1_prm.PlantLoop.rb | 94 ++++++++++++++++++- 2 files changed, 96 insertions(+), 2 deletions(-) diff --git a/lib/openstudio-standards/prototypes/common/objects/Prototype.hvac_systems.rb b/lib/openstudio-standards/prototypes/common/objects/Prototype.hvac_systems.rb index b891a21450..61f05c13af 100644 --- a/lib/openstudio-standards/prototypes/common/objects/Prototype.hvac_systems.rb +++ b/lib/openstudio-standards/prototypes/common/objects/Prototype.hvac_systems.rb @@ -407,9 +407,11 @@ def model_add_chw_loop(model, # make the correct type of chiller based these properties chiller_sizing_factor = (1.0 / chiller_name_list.length).round(2) + # Create chillers and set plant operation scheme chiller_name_list.each do |chiller_name| + chiller_name = "#{template} #{chiller_cooling_type} #{chiller_condenser_type} #{chiller_name}" chiller = OpenStudio::Model::ChillerElectricEIR.new(model) - chiller.setName("#{template} #{chiller_cooling_type} #{chiller_condenser_type} #{chiller_name}") + chiller.setName(chiller_name) chilled_water_loop.addSupplyBranchForComponent(chiller) dsgn_sup_wtr_temp_c = OpenStudio.convert(dsgn_sup_wtr_temp, 'F', 'C').get chiller.setReferenceLeavingChilledWaterTemperature(dsgn_sup_wtr_temp_c) diff --git a/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.PlantLoop.rb b/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.PlantLoop.rb index 16ca10991d..17e5326ee5 100644 --- a/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.PlantLoop.rb +++ b/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.PlantLoop.rb @@ -334,6 +334,98 @@ def add_ems_for_multiple_chiller_pumps_w_secondary_plant(model, primary_plant) add_ems_program_for_2_pump_chiller_plant(model, sorted_chiller_list, primary_plant) end + # Update chilled water loop operation scheme to work with updated EMS ranges + stage_chilled_water_loop_operation_schemes(model, primary_plant) + + end + + def stage_chilled_water_loop_operation_schemes(model, chilled_water_loop) + + # Initialize array of cooling plant systems + chillers = [] + + # Gets all associated chillers from the supply side and adds them to the chillers list + chilled_water_loop.supplyComponents(OpenStudio::Model::ChillerElectricEIR.iddObjectType).each do |chiller| + chillers << chiller.to_ChillerElectricEIR.get + end + + # Skip those without chillers or only 1 (i.e., nothing to stage) + return if chillers.empty? + return if chillers.length == 1 + + # Sort chillers by capacity + sorted_chillers = chillers.sort_by { |chiller| chiller.referenceCapacity.get } + + primary_chiller = sorted_chillers[0] + secondary_1_chiller = sorted_chillers[1] + secondary_2_chiller = sorted_chillers[2] if chillers.length == 3 + + equip_operation_cool_load = OpenStudio::Model::PlantEquipmentOperationCoolingLoad.new(model) + + # Calculate load ranges into the PlantEquipmentOperation:CoolingLoad + loading_factor = 0.8 + # # when the capacity of primary chiller is larger than the capacity of secondary chiller - the loading factor + # # will need to be adjusted to avoid load range intersect. + # if secondary_1_chiller.referenceCapacity.get <= primary_chiller.referenceCapacity.get * loading_factor + # # Adjustment_factor can creates a bandwidth for step 2 staging strategy. + # # set adjustment_factor = 1.0 means the step 2 staging strategy is skipped + # adjustment_factor = 1.0 + # loading_factor = secondary_1_chiller.referenceCapacity.get / primary_chiller.referenceCapacity.get * adjustment_factor + # end + + if chillers.length == 3 + + # Add four ranges for small, medium, and large chiller capacities + # 1: 0 W -> 80% of smallest chiller capacity + # 2: 80% of primary chiller -> medium size chiller capacity + # 3: medium chiller capacity -> medium + large chiller capacity + # 4: medium + large chiller capacity -> infinity + # Control strategy first stage + equipment_list = [primary_chiller] + range = primary_chiller.referenceCapacity.get * loading_factor + equip_operation_cool_load.addLoadRange(range, equipment_list) + + # Control strategy second stage + equipment_list = [secondary_1_chiller] + range = secondary_1_chiller.referenceCapacity.get + equip_operation_cool_load.addLoadRange(range, equipment_list) + + # Control strategy third stage + equipment_list = [secondary_1_chiller, secondary_2_chiller] + range = secondary_1_chiller.referenceCapacity.get + secondary_2_chiller.referenceCapacity.get + equip_operation_cool_load.addLoadRange(range, equipment_list) + + equipment_list = [primary_chiller, secondary_1_chiller, secondary_2_chiller] + range = 999999999 + equip_operation_cool_load.addLoadRange(range, equipment_list) + + elsif chillers.length == 2 + + # Add three ranges for primary and secondary chiller capacities + # 1: 0 W -> 80% of smallest chiller capacity + # 2: 80% of primary chiller -> secondary chiller capacity + # 3: secondary chiller capacity -> infinity + # Control strategy first stage + equipment_list = [primary_chiller] + range = primary_chiller.referenceCapacity.get * loading_factor + equip_operation_cool_load.addLoadRange(range, equipment_list) + + # Control strategy second stage + equipment_list = [secondary_1_chiller] + range = secondary_1_chiller.referenceCapacity.get + equip_operation_cool_load.addLoadRange(range, equipment_list) + + # Control strategy third stage + equipment_list = [primary_chiller, secondary_1_chiller] + range = 999999999 + equip_operation_cool_load.addLoadRange(range, equipment_list) + + else + raise "Failed to stage chillers, #{chillers.length} chillers found in the loop.Logic for staging chillers has only been done for either 2 or 3 chillers" + end + + chilled_water_loop.setPlantEquipmentOperationCoolingLoad(equip_operation_cool_load) + end # Adds EMS program for pumps serving 2 chillers on primary + secondary loop. This was due to an issue when modeling two @@ -369,7 +461,7 @@ def add_ems_program_for_2_pump_chiller_plant(model, sorted_chiller_list, primary ems_pump_program.addLine('SET CHILLER_PUMP_2_STATUS = 1, !- A10') ems_pump_program.addLine('SET CHILLER_PUMP_1_FLOW = 0, !- A11') ems_pump_program.addLine('SET CHILLER_PUMP_2_FLOW = CHILLER_PUMP_2_DES_FLOW, !- A12') - ems_pump_program.addLine("ELSEIF #{chw_demand} > #{capacity_small_chiller + capacity_large_chiller}, !- A13") + ems_pump_program.addLine("ELSEIF #{chw_demand} > #{capacity_large_chiller}, !- A13") ems_pump_program.addLine('SET CHILLER_PUMP_1_STATUS = 1, !- A14') ems_pump_program.addLine('SET CHILLER_PUMP_2_STATUS = 1, !- A15') ems_pump_program.addLine('SET CHILLER_PUMP_1_FLOW = CHILLER_PUMP_1_DES_FLOW, !- A16') From be0d4a658b4b86ff3c9b4decbd553e760f076773 Mon Sep 17 00:00:00 2001 From: gonz102 Date: Wed, 12 Jun 2024 10:18:15 -0400 Subject: [PATCH 03/11] Rubocop corrections --- .../common/objects/Prototype.hvac_systems.rb | 3 +-- .../ashrae_90_1_prm/ashrae_90_1_prm.PlantLoop.rb | 12 +----------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/lib/openstudio-standards/prototypes/common/objects/Prototype.hvac_systems.rb b/lib/openstudio-standards/prototypes/common/objects/Prototype.hvac_systems.rb index 61f05c13af..b472f690a0 100644 --- a/lib/openstudio-standards/prototypes/common/objects/Prototype.hvac_systems.rb +++ b/lib/openstudio-standards/prototypes/common/objects/Prototype.hvac_systems.rb @@ -313,7 +313,7 @@ def model_add_chw_loop(model, secondary_chilled_water_loop.additionalProperties.setFeature('is_secondary_loop', true) # primary chilled water pumps are added when adding chillers # Add Constant pump, in plant loop, the number of chiller adjustment will assign pump to each chiller - #pri_chw_pump = OpenStudio::Model::PumpConstantSpeed.new(model) + # pri_chw_pump = OpenStudio::Model::PumpConstantSpeed.new(model) pri_chw_pump = OpenStudio::Model::PumpVariableSpeed.new(model) pump_variable_speed_set_control_type(pri_chw_pump, control_type = 'Riding Curve') # This pump name is important for function add_ems_for_multiple_chiller_pumps_w_secondary_plant. If you update @@ -632,7 +632,6 @@ def model_add_cw_loop(model, next unless dd.dayType == 'SummerDesignDay' next unless dd.name.get.to_s.include?('WB=>MDB') - if condenser_water_loop.model.version < OpenStudio::VersionString.new('3.3.0') if dd.humidityIndicatingType == 'Wetbulb' summer_oat_wb_c = dd.humidityIndicatingConditionsAtMaximumDryBulb diff --git a/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.PlantLoop.rb b/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.PlantLoop.rb index 17e5326ee5..5bd2b27d33 100644 --- a/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.PlantLoop.rb +++ b/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.PlantLoop.rb @@ -251,7 +251,6 @@ def plant_loop_apply_prm_number_of_chillers(model, plant_loop) # @param model [OpenStudio::Model] OpenStudio model with plant loops # @param primary_plant [OpenStudio::Model::PlantLoop] Primary chilled water loop with chillers def add_ems_for_multiple_chiller_pumps_w_secondary_plant(model, primary_plant) - # Aggregate array of chillers on primary plant supply side chiller_list = [] @@ -292,10 +291,8 @@ def add_ems_for_multiple_chiller_pumps_w_secondary_plant(model, primary_plant) end - # Make pump specific parameters for EMS. Use counter sorted_chiller_list.each_with_index do |chiller, i| - # Get chiller pump pump_name = "#{chiller.name} Inlet Pump" pump = model.getPumpVariableSpeedByName(pump_name).get @@ -322,7 +319,6 @@ def add_ems_for_multiple_chiller_pumps_w_secondary_plant(model, primary_plant) internal_variable = OpenStudio::Model::EnergyManagementSystemInternalVariable.new(model, 'Pump Maximum Mass Flow Rate') internal_variable.setInternalDataIndexKeyName(pump_name) internal_variable.setName(ems_pump_design_flow_name) - end # Write EMS program @@ -336,11 +332,9 @@ def add_ems_for_multiple_chiller_pumps_w_secondary_plant(model, primary_plant) # Update chilled water loop operation scheme to work with updated EMS ranges stage_chilled_water_loop_operation_schemes(model, primary_plant) - end def stage_chilled_water_loop_operation_schemes(model, chilled_water_loop) - # Initialize array of cooling plant systems chillers = [] @@ -425,7 +419,6 @@ def stage_chilled_water_loop_operation_schemes(model, chilled_water_loop) end chilled_water_loop.setPlantEquipmentOperationCoolingLoad(equip_operation_cool_load) - end # Adds EMS program for pumps serving 2 chillers on primary + secondary loop. This was due to an issue when modeling two @@ -435,7 +428,6 @@ def stage_chilled_water_loop_operation_schemes(model, chilled_water_loop) # @param sorted_chiller_list [Array] Array of chillers in primary_plant sorted by capacity # @param primary_plant [OpenStudio::Model::PlantLoop] Primary chilled water loop with chillers def add_ems_program_for_2_pump_chiller_plant(model, sorted_chiller_list, primary_plant) - plant_name = primary_plant.name.to_s # Break out sorted chillers and get their respective capacities @@ -472,11 +464,9 @@ def add_ems_program_for_2_pump_chiller_plant(model, sorted_chiller_list, primary ems_pump_program_manager.setName("#{plant_name.gsub(/[-\s]+/, '_')}_Pump_Program_Manager") ems_pump_program_manager.setCallingPoint('InsideHVACSystemIterationLoop') ems_pump_program_manager.addProgram(ems_pump_program) - end def add_ems_program_for_3_pump_chiller_plant(model, sorted_chiller_list, primary_plant) - plant_name = primary_plant.name.to_s # Break out sorted chillers and get their respective capacities @@ -540,8 +530,8 @@ def add_ems_program_for_3_pump_chiller_plant(model, sorted_chiller_list, primary ems_pump_program_manager.setName("#{plant_name.gsub(/[-\s]+/, '_')}_Pump_Program_Manager") ems_pump_program_manager.setCallingPoint('InsideHVACSystemIterationLoop') ems_pump_program_manager.addProgram(ems_pump_program) - end + # Apply prm baseline pump power # @note I think it makes more sense to sense the motor efficiency right there... # But actually it's completely irrelevant... From cb46fd1a3bec9c94d637259d2443cc04c44f4884 Mon Sep 17 00:00:00 2001 From: gonz102 Date: Wed, 12 Jun 2024 11:01:37 -0400 Subject: [PATCH 04/11] Added yard documentation for stage_chiller_water_loop --- .../standards/ashrae_90_1_prm/ashrae_90_1_prm.PlantLoop.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.PlantLoop.rb b/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.PlantLoop.rb index 5bd2b27d33..5e1461044e 100644 --- a/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.PlantLoop.rb +++ b/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.PlantLoop.rb @@ -334,6 +334,10 @@ def add_ems_for_multiple_chiller_pumps_w_secondary_plant(model, primary_plant) stage_chilled_water_loop_operation_schemes(model, primary_plant) end + # Updates a chilled water plant's operation scheme to match the EMS written by either + # add_ems_program_for_3_pump_chiller_plant or add_ems_program_for_2_pump_chiller_plant + # @param model [OpenStudio::Model] OpenStudio model with plant loops + # @param chilled_water_loop [OpenStudio::Model::PlantLoop] chilled water loop def stage_chilled_water_loop_operation_schemes(model, chilled_water_loop) # Initialize array of cooling plant systems chillers = [] From 58def6b3e00289b1d77161c6938a0f18e85c7f68 Mon Sep 17 00:00:00 2001 From: gonz102 Date: Wed, 19 Jun 2024 12:21:05 -0400 Subject: [PATCH 05/11] Updated repo based on comments in primary_secondary_loops PR. --- .../standards/Standards.Model.rb | 2 +- .../standards/Standards.PlantLoop.rb | 1 - .../ashrae_90_1_prm.PlantLoop.rb | 18 +++++++++++------- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/openstudio-standards/standards/Standards.Model.rb b/lib/openstudio-standards/standards/Standards.Model.rb index b445905128..4361c65831 100644 --- a/lib/openstudio-standards/standards/Standards.Model.rb +++ b/lib/openstudio-standards/standards/Standards.Model.rb @@ -445,7 +445,7 @@ def model_create_prm_any_baseline_building(user_model, building_type, climate_zo next if plant_loop_swh_loop?(plant_loop) plant_loop_apply_prm_number_of_boilers(plant_loop) - plant_loop_apply_prm_number_of_chillers(model, plant_loop) + plant_loop_apply_prm_number_of_chillers(plant_loop) end # Set the baseline number of cooling towers diff --git a/lib/openstudio-standards/standards/Standards.PlantLoop.rb b/lib/openstudio-standards/standards/Standards.PlantLoop.rb index 2d737b03ae..e6a34f587b 100644 --- a/lib/openstudio-standards/standards/Standards.PlantLoop.rb +++ b/lib/openstudio-standards/standards/Standards.PlantLoop.rb @@ -55,7 +55,6 @@ def chw_sizing_control(model, chilled_water_loop, dsgn_sup_wtr_temp, dsgn_sup_wt # @param model [OpenStudio::Model::Model] OpenStudio model object # @return [String] common_pipe or heat_exchanger def plant_loop_set_chw_pri_sec_configuration(model) - # pri_sec_config = 'common_pipe' pri_sec_config = 'heat_exchanger' return pri_sec_config end diff --git a/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.PlantLoop.rb b/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.PlantLoop.rb index 5e1461044e..a171cd3f42 100644 --- a/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.PlantLoop.rb +++ b/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.PlantLoop.rb @@ -95,7 +95,7 @@ def plant_loop_apply_prm_number_of_cooling_towers(plant_loop) # EMS to stage chillers properly # @param plant_loop [OpenStudio::Model::PlantLoop] chilled water loop # @return [Boolean] returns true if successful, false if not - def plant_loop_apply_prm_number_of_chillers(model, plant_loop) + def plant_loop_apply_prm_number_of_chillers(plant_loop) # Skip non-cooling plants & secondary cooling loop return true unless plant_loop.sizingPlant.loopType == 'Cooling' # If the loop is cooling but it is a secondary loop, then skip. @@ -108,6 +108,8 @@ def plant_loop_apply_prm_number_of_chillers(model, plant_loop) plant_loop.setLoadDistributionScheme('SequentialLoad') end + model = plant_loop.model + # Get all existing chillers and pumps. Copy chiller properties needed when duplicating existing settings chillers = [] pumps = [] @@ -214,10 +216,8 @@ def plant_loop_apply_prm_number_of_chillers(model, plant_loop) new_pump = OpenStudio::Model::PumpVariableSpeed.new(plant_loop.model) new_pump.setName("#{chiller.name} Inlet Pump") new_pump.setRatedPumpHead(original_pump.ratedPumpHead / num_chillers) - new_pump.setCoefficient1ofthePartLoadPerformanceCurve(original_pump.coefficient1ofthePartLoadPerformanceCurve) - new_pump.setCoefficient2ofthePartLoadPerformanceCurve(original_pump.coefficient2ofthePartLoadPerformanceCurve) - new_pump.setCoefficient3ofthePartLoadPerformanceCurve(original_pump.coefficient3ofthePartLoadPerformanceCurve) - new_pump.setCoefficient4ofthePartLoadPerformanceCurve(original_pump.coefficient4ofthePartLoadPerformanceCurve) + + pump_variable_speed_set_control_type(new_pump, control_type = 'Riding Curve') chiller_inlet_node = chiller.connectedObject(chiller.supplyInletPort).get.to_Node.get new_pump.addToNode(chiller_inlet_node) @@ -237,8 +237,6 @@ def plant_loop_apply_prm_number_of_chillers(model, plant_loop) OpenStudio.logFree(OpenStudio::Error, 'prm.log', "For #{plant_loop.name} has more than 3 chillers. We do not have an EMS strategy for that yet.") elsif num_chillers > 1 add_ems_for_multiple_chiller_pumps_w_secondary_plant(model, plant_loop) - else - OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.PlantLoop', "No EMS for multiple chillers required for #{plant_loop.name}, as there's only 1 chiller.") end end @@ -470,6 +468,12 @@ def add_ems_program_for_2_pump_chiller_plant(model, sorted_chiller_list, primary ems_pump_program_manager.addProgram(ems_pump_program) end + # Adds EMS program for pumps serving 3 chillers on primary + secondary loop. This was due to an issue when modeling two + # dedicated loops. The headered pumps or dedicated constant speed pumps operate at full flow as long as there's a + # load on the loop unless this EMS is in place. + # @param model [OpenStudio::Model] OpenStudio model with plant loops + # @param sorted_chiller_list [Array] Array of chillers in primary_plant sorted by capacity + # @param primary_plant [OpenStudio::Model::PlantLoop] Primary chilled water loop with chillers def add_ems_program_for_3_pump_chiller_plant(model, sorted_chiller_list, primary_plant) plant_name = primary_plant.name.to_s From ee788d3a53cccdad7a4267b909b6a8738aff02f6 Mon Sep 17 00:00:00 2001 From: gonz102 Date: Wed, 17 Jul 2024 19:35:01 -0400 Subject: [PATCH 06/11] Simplified primary and secondary loop staging strategy to simply sort the chillers by reference capacity and revert back to old naming convention. --- .../common/objects/Prototype.hvac_systems.rb | 16 +++++--------- .../ashrae_90_1_prm.PlantLoop.rb | 21 ++----------------- 2 files changed, 7 insertions(+), 30 deletions(-) diff --git a/lib/openstudio-standards/prototypes/common/objects/Prototype.hvac_systems.rb b/lib/openstudio-standards/prototypes/common/objects/Prototype.hvac_systems.rb index 142d17afeb..822eb653c6 100644 --- a/lib/openstudio-standards/prototypes/common/objects/Prototype.hvac_systems.rb +++ b/lib/openstudio-standards/prototypes/common/objects/Prototype.hvac_systems.rb @@ -393,23 +393,17 @@ def model_add_chw_loop(model, default_cop = kw_per_ton_to_cop(0.66) end - # Create list of consolidated chiller names. This will reduce the number of chillers to no greater than 3 chillers - chiller_name_list = ["#{chiller_condenser_type}_first_stage"] + # Check number of chillers if num_chillers > 3 - OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.PlantLoop', "EMS Code for multiple chiller pump has not been written for greater than 3 chillers. This has #{num_of_chillers} chillers") - elsif num_chillers == 3 - chiller_name_list << "#{chiller_condenser_type}_second_stage_1" - chiller_name_list << "#{chiller_condenser_type}_second_stage_2" - elsif num_chillers == 2 - chiller_name_list << "#{chiller_condenser_type}_second_stage" + OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.PlantLoop', "EMS Code for multiple chiller pump has not been written for greater than 3 chillers. This has #{num_chillers} chillers") end # make the correct type of chiller based these properties - chiller_sizing_factor = (1.0 / chiller_name_list.length).round(2) + chiller_sizing_factor = (1.0 / num_chillers).round(2) # Create chillers and set plant operation scheme - chiller_name_list.each do |chiller_name| - chiller_name = "#{template} #{chiller_cooling_type} #{chiller_condenser_type} #{chiller_name}" + num_chillers.times do |i| + chiller_name = "#{template} #{chiller_cooling_type} #{chiller_condenser_type}" chiller = OpenStudio::Model::ChillerElectricEIR.new(model) chiller.setName(chiller_name) chilled_water_loop.addSupplyBranchForComponent(chiller) diff --git a/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.PlantLoop.rb b/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.PlantLoop.rb index a171cd3f42..4f55a8de69 100644 --- a/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.PlantLoop.rb +++ b/lib/openstudio-standards/standards/ashrae_90_1_prm/ashrae_90_1_prm.PlantLoop.rb @@ -269,25 +269,8 @@ def add_ems_for_multiple_chiller_pumps_w_secondary_plant(model, primary_plant) chw_sensor.setKeyName(plant_name) chw_sensor.setName("#{plant_name.gsub(/[-\s]+/, '_')}_CHW_DEMAND") - sorted_chiller_list = Array.new(num_of_chillers) - - if num_of_chillers >= 3 - chiller_list.each_with_index do |chiller, i| - sorted_chiller_list[0] = chiller if chiller.name.to_s.include? 'first_stage' - sorted_chiller_list[1] = chiller if chiller.name.to_s.include? 'second_stage_1' - sorted_chiller_list[2] = chiller if chiller.name.to_s.include? 'second_stage_2' - end - else - # 2 chiller setups are simply sorted such that the small chiller is staged first - if chiller_list[0].referenceCapacity.get > chiller_list[1].referenceCapacity.get - sorted_chiller_list[0] = chiller_list[1] - sorted_chiller_list[1] = chiller_list[0] - else - sorted_chiller_list[0] = chiller_list[0] - sorted_chiller_list[1] = chiller_list[1] - end - - end + # Sort chillers by their reference capacity + sorted_chiller_list = chiller_list.sort_by { |chiller| chiller.referenceCapacity.get.to_f} # Make pump specific parameters for EMS. Use counter sorted_chiller_list.each_with_index do |chiller, i| From 57143347e50284fdac7da82b11d93f56728d10f4 Mon Sep 17 00:00:00 2001 From: gonz102 Date: Wed, 17 Jul 2024 19:35:48 -0400 Subject: [PATCH 07/11] Added test case for primary/secondary plant loops --- test/90_1_prm/data/prototype_list.json | 5 ++ test/90_1_prm/prm_check.rb | 73 ++++++++++++++++++++++++++ test/90_1_prm/test_appendix_g_prm.rb | 7 +++ 3 files changed, 85 insertions(+) diff --git a/test/90_1_prm/data/prototype_list.json b/test/90_1_prm/data/prototype_list.json index 02672bfc69..f07f6bf476 100644 --- a/test/90_1_prm/data/prototype_list.json +++ b/test/90_1_prm/data/prototype_list.json @@ -186,6 +186,11 @@ ["PrimarySchool", "90.1-2013", "ASHRAE 169-2013-4A", "userdata_default_test", [["remove_transformer", []]]], ["PrimarySchool", "90.1-2013", "ASHRAE 169-2013-2A", "userdata_default_test", [["remove_transformer", []]]] ], + + "pri_sec_loop": [ + ["LargeOffice", "90.1-2013", "ASHRAE 169-2013-2A", "userdata_default_test", [["remove_transformer", []]]] + ], + "proposed_model_residential_lpd": [ ["LargeHotel", "90.1-2013", "ASHRAE 169-2013-2A", "userdata_res_ltg", [["remove_transformer", []], ["reduce_lpd" , []]]] ], diff --git a/test/90_1_prm/prm_check.rb b/test/90_1_prm/prm_check.rb index 989a1c6e87..4c3c47c404 100644 --- a/test/90_1_prm/prm_check.rb +++ b/test/90_1_prm/prm_check.rb @@ -1988,4 +1988,77 @@ def check_wwr(prototypes_base) end end end + + # Check primary/secondary chilled water loop for the baseline models + # + # @param prototypes_base [Hash] Baseline prototypes + def check_pri_sec_loop(prototypes_base) + + prototypes_base.each do |prototype, model_baseline| + + building_type, template, climate_zone, user_data_dir, mod = prototype + + has_primary_chilled_water_loop = false + has_secondary_chilled_water_loop = false + + # Check primary and secondary chilled water loops + model_baseline.getPlantLoops.each do |plant_loop| + + sizing_plant = plant_loop.sizingPlant + + next if sizing_plant.loopType != 'Cooling' + + # Check primary loop for components + if plant_loop.name.to_s.include? 'Chilled Water Loop_Primary' + + has_primary_chilled_water_loop = true + + n_chillers = 0 + + # Count chillers + plant_loop.supplyComponents.each do |sc| + if sc.to_ChillerElectricEIR.is_initialized + n_chillers += 1 + end + end + + assert(n_chillers == 2, "The number of chillers in the primary loop is incorrect. The test results in #{n_chillers} when it should be 2.") + + has_heat_exchanger = false + + # Check for heat exchanger on demand side + plant_loop.demandComponents.each do |dc| + if dc.to_HeatExchangerFluidToFluid.is_initialized + has_heat_exchanger = true + end + end + + assert(has_heat_exchanger, "The primary chilled water loop should have a HeatExchangerFluidToFluid on the demand side but it does not.") + + # Check secondary loop for components + elsif plant_loop.name.to_s.include? 'Chilled Water Loop' + + has_secondary_chilled_water_loop = true + has_heat_exchanger = false + + # Check for heat exchanger on supply side + plant_loop.supplyComponents.each do |sc| + if sc.to_HeatExchangerFluidToFluid.is_initialized + has_heat_exchanger = true + end + end + + assert(has_heat_exchanger, "The secondary chilled water loop should have a HeatExchangerFluidToFluid on the supply side but it does not.") + + end + + end + + assert(has_primary_chilled_water_loop, "The primary/secondary test did not find a primary chilled water loop for #{building_type}, #{template}, #{climate_zone}.") + assert(has_secondary_chilled_water_loop, "The primary/secondary test did not find a secondary chilled water loop for #{building_type}, #{template}, #{climate_zone}.") + + end + + end + end \ No newline at end of file diff --git a/test/90_1_prm/test_appendix_g_prm.rb b/test/90_1_prm/test_appendix_g_prm.rb index 3abbe09dd3..f38c07a700 100644 --- a/test/90_1_prm/test_appendix_g_prm.rb +++ b/test/90_1_prm/test_appendix_g_prm.rb @@ -254,5 +254,12 @@ def test_wwr model_hash = prm_test_helper('wwr', require_prototype = false, require_baseline = true) check_wwr(model_hash['baseline']) end + + def test_pri_sec_loop_configuration + model_hash = prm_test_helper('pri_sec_loop', require_prototype = false, require_baseline = true) + + check_pri_sec_loop(model_hash['baseline']) + end + end From 3ac70619088013c23050a27c6ded3e6fa011a831 Mon Sep 17 00:00:00 2001 From: gonz102 Date: Thu, 1 Aug 2024 16:45:32 -0400 Subject: [PATCH 08/11] Trailing white space was causing issues when looking up chiller capacities from the sizing run. This happens if there's no compressor type defined and it was causing issues in a handful of test cases. --- .../prototypes/common/objects/Prototype.hvac_systems.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/openstudio-standards/prototypes/common/objects/Prototype.hvac_systems.rb b/lib/openstudio-standards/prototypes/common/objects/Prototype.hvac_systems.rb index 822eb653c6..8d2e905223 100644 --- a/lib/openstudio-standards/prototypes/common/objects/Prototype.hvac_systems.rb +++ b/lib/openstudio-standards/prototypes/common/objects/Prototype.hvac_systems.rb @@ -405,7 +405,7 @@ def model_add_chw_loop(model, num_chillers.times do |i| chiller_name = "#{template} #{chiller_cooling_type} #{chiller_condenser_type}" chiller = OpenStudio::Model::ChillerElectricEIR.new(model) - chiller.setName(chiller_name) + chiller.setName(chiller_name.strip) chilled_water_loop.addSupplyBranchForComponent(chiller) dsgn_sup_wtr_temp_c = OpenStudio.convert(dsgn_sup_wtr_temp, 'F', 'C').get chiller.setReferenceLeavingChilledWaterTemperature(dsgn_sup_wtr_temp_c) From 1212b0a028ff1b2413dd4bb0d209d89630acb203 Mon Sep 17 00:00:00 2001 From: gonz102 Date: Thu, 8 Aug 2024 16:27:54 -0400 Subject: [PATCH 09/11] Made it such that only PRM methods utilized primary/secondary loops. The rest will use the legacy common_pipe configuration once again. --- lib/openstudio-standards/standards/Standards.PlantLoop.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/openstudio-standards/standards/Standards.PlantLoop.rb b/lib/openstudio-standards/standards/Standards.PlantLoop.rb index e6a34f587b..bec8b62c87 100644 --- a/lib/openstudio-standards/standards/Standards.PlantLoop.rb +++ b/lib/openstudio-standards/standards/Standards.PlantLoop.rb @@ -55,7 +55,7 @@ def chw_sizing_control(model, chilled_water_loop, dsgn_sup_wtr_temp, dsgn_sup_wt # @param model [OpenStudio::Model::Model] OpenStudio model object # @return [String] common_pipe or heat_exchanger def plant_loop_set_chw_pri_sec_configuration(model) - pri_sec_config = 'heat_exchanger' + pri_sec_config = 'common_pipe' return pri_sec_config end From 27f3422604712049f37279c3439099efb79cf0ab Mon Sep 17 00:00:00 2001 From: Weili Xu Date: Mon, 12 Aug 2024 10:20:53 -0700 Subject: [PATCH 10/11] move chiller error handler to inside the heat exchanger code block --- .../prototypes/common/objects/Prototype.hvac_systems.rb | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/openstudio-standards/prototypes/common/objects/Prototype.hvac_systems.rb b/lib/openstudio-standards/prototypes/common/objects/Prototype.hvac_systems.rb index 8d2e905223..c689569bd2 100644 --- a/lib/openstudio-standards/prototypes/common/objects/Prototype.hvac_systems.rb +++ b/lib/openstudio-standards/prototypes/common/objects/Prototype.hvac_systems.rb @@ -296,6 +296,10 @@ def model_add_chw_loop(model, # Change the chilled water loop to have a two-way common pipes chilled_water_loop.setCommonPipeSimulation('CommonPipe') elsif pri_sec_config == 'heat_exchanger' + # Check number of chillers + if num_chillers > 3 + OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.PlantLoop', "EMS Code for multiple chiller pump has not been written for greater than 3 chillers. This has #{num_chillers} chillers") + end # NOTE: PRECONDITIONING for `const_pri_var_sec` pump type is only applicable for PRM routine and only applies to System Type 7 and System Type 8 # See: model_add_prm_baseline_system under Model object. # In this scenario, we will need to create a primary and secondary configuration: @@ -393,11 +397,6 @@ def model_add_chw_loop(model, default_cop = kw_per_ton_to_cop(0.66) end - # Check number of chillers - if num_chillers > 3 - OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.PlantLoop', "EMS Code for multiple chiller pump has not been written for greater than 3 chillers. This has #{num_chillers} chillers") - end - # make the correct type of chiller based these properties chiller_sizing_factor = (1.0 / num_chillers).round(2) From a502d67d7e7754cc4ede1d775c96a3a45aa3b2ee Mon Sep 17 00:00:00 2001 From: gonz102 Date: Wed, 21 Aug 2024 10:58:30 -0400 Subject: [PATCH 11/11] Change in chiller name caused unintended regression issues. Reverted this change. --- .../prototypes/common/objects/Prototype.hvac_systems.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/openstudio-standards/prototypes/common/objects/Prototype.hvac_systems.rb b/lib/openstudio-standards/prototypes/common/objects/Prototype.hvac_systems.rb index c689569bd2..eddaf08562 100644 --- a/lib/openstudio-standards/prototypes/common/objects/Prototype.hvac_systems.rb +++ b/lib/openstudio-standards/prototypes/common/objects/Prototype.hvac_systems.rb @@ -402,9 +402,8 @@ def model_add_chw_loop(model, # Create chillers and set plant operation scheme num_chillers.times do |i| - chiller_name = "#{template} #{chiller_cooling_type} #{chiller_condenser_type}" chiller = OpenStudio::Model::ChillerElectricEIR.new(model) - chiller.setName(chiller_name.strip) + chiller.setName("#{template} #{chiller_cooling_type} #{chiller_condenser_type} #{chiller_compressor_type} Chiller #{i}") chilled_water_loop.addSupplyBranchForComponent(chiller) dsgn_sup_wtr_temp_c = OpenStudio.convert(dsgn_sup_wtr_temp, 'F', 'C').get chiller.setReferenceLeavingChilledWaterTemperature(dsgn_sup_wtr_temp_c)