From 2797645af9b065503ef7b3c0dc1cd4b624023f53 Mon Sep 17 00:00:00 2001 From: urwahah Date: Thu, 18 Jun 2026 13:44:16 -0700 Subject: [PATCH 1/8] add cooling-based sizing --- app.py | 4 +- src/config.py | 8 ++- src/energy.py | 188 ++++++++++++++++++++++++++++++++++++------------- utils/units.py | 8 ++- 4 files changed, 150 insertions(+), 58 deletions(-) diff --git a/app.py b/app.py index 9bbbd9b..d3a8561 100644 --- a/app.py +++ b/app.py @@ -10,8 +10,8 @@ from utils.logging_config import get_logger, setup_logging # Read level from environment variable (or default to INFO) -log_level_name = os.environ.get("LOG_LEVEL", "INFO") -log_level = getattr(logging, log_level_name.upper(), logging.INFO) +log_level_name = os.environ.get("LOG_LEVEL", "DEBUG") +log_level = getattr(logging, log_level_name.upper(), logging.DEBUG) setup_logging(level=log_level) # Get a logger for this module diff --git a/src/config.py b/src/config.py index 6f28ec0..d53ea21 100644 --- a/src/config.py +++ b/src/config.py @@ -99,8 +99,10 @@ class Columns(StrEnum): ELEC_HR_WH = "elec_hr_Wh" # --- Air-Water Heat Pump (Heating) --- - AWHP_NUM_H = "awhp_num_h" - AWHP_NUM_H_R = "awhp_num_h_redundant" + # AWHP_NUM_H = "awhp_num_h" + # AWHP_NUM_H_R = "awhp_num_h_redundant" + AWHP_NUM = "awhp_num" + AWHP_NUM_R = "awhp_num_redundant" AWHP_CAP_H_W = "awhp_cap_h_W" AWHP_COP_H = "awhp_cop_h" AWHP_HHW_W = "awhp_hhw_W" @@ -116,7 +118,7 @@ class Columns(StrEnum): ELEC_RES_WH = "elec_res_Wh" # --- Air-Water Heat Pump (Cooling) --- - AWHP_NUM_C = "awhp_num_c" + # AWHP_NUM_C = "awhp_num_c" AWHP_CAP_C_W = "awhp_cap_c_W" AWHP_COP_C = "awhp_cop_c" AWHP_CHW_W = "awhp_chw_W" diff --git a/src/energy.py b/src/energy.py index e990bfc..793b771 100644 --- a/src/energy.py +++ b/src/energy.py @@ -434,6 +434,26 @@ def _capacity_constraints( return cap +# def _awhp_cooling_reference_capacity(performance: PerformanceCurves) -> float: +# """AWHP cooling reference capacity [W] for sizing.""" + +# if isinstance(performance.capacity_W, list): +# ref_supply_temp = awhp_c_supply_t +# ref_capacity_W = awhp_c.performance_cooling.leaving_supply_t[ +# ref_supply_temp +# ].capacity_W + +# ref_temp_C = 30.0 # Conservative outdoor temperature for sizing +# cap_ref = interp_vector( +# awhp_c.performance_cooling.t_out_C, +# ref_capacity_W, +# np.array([ref_temp_C]), +# )[0] +# else: +# cap_ref = float(performance.capacity_W) + +# return cap_ref + def loads_to_site_energy( load: StandardLoad, @@ -489,8 +509,10 @@ def loads_to_site_energy( Col.AWHP_HHW_W.value, Col.AWHP_COP_H.value, Col.AWHP_CAP_H_W.value, - Col.AWHP_NUM_H.value, - Col.AWHP_NUM_H_R.value, + # Col.AWHP_NUM_H.value, + # Col.AWHP_NUM_H_R.value, + Col.AWHP_NUM.value, + Col.AWHP_NUM_R.value, Col.ELEC_AWHP_H_WH.value, # Boiler Col.BOILER_HHW_W.value, @@ -503,7 +525,7 @@ def loads_to_site_energy( Col.AWHP_CHW_W.value, Col.AWHP_COP_C.value, Col.AWHP_CAP_C_W.value, - Col.AWHP_NUM_C.value, + # Col.AWHP_NUM_C.value, Col.ELEC_AWHP_C_WH.value, # Electric chiller Col.CHILLER_CHW_W.value, @@ -649,63 +671,120 @@ def loads_to_site_energy( awhp_h, temps, awhp_h_performance, awhp_h_performance_model ) - # Determine number of units + # --- Sizing Logic --- sizing_mode = scen.awhp_sizing_mode sizing_value = scen.awhp_sizing_value redundancy = scen.awhp_redundancy - ref_temp_C = 0.0 # Conservative outdoor temperature for sizing - if isinstance(awhp_h_performance.capacity_W, np.ndarray): - # closest supply temperature to input (for reset, this defaults to the lowest) - awhp_h_supply_temps_str = list(awhp_h.performance_heating.leaving_supply_t.keys()) - awhp_h_supply_temps = np.array(awhp_h_supply_temps_str, dtype=float) - ref_supply_temp = awhp_h_supply_temps_str[ - np.argmin(np.abs(awhp_h_supply_temps - awhp_h_supply_t)) - ] - ref_capacity_W = awhp_h.performance_heating.leaving_supply_t[ - ref_supply_temp - ].capacity_W - - cap_ref = interp_vector( - awhp_h.performance_heating.t_out_C, - ref_capacity_W, - np.array([ref_temp_C]), - )[0] + if scen.awhp_use_cooling: + sizing_priority = "cooling" # scen.awhp_sizing_priority + awhp_c = library.get_equipment(scen.awhp) + # we don't have any HPs with >1 CHWST, this can be edited later to match HHWST if needed + # this extracts the first value and uses that performance data + awhp_c_supply_t = next(iter(awhp_c.performance_cooling.leaving_supply_t.keys())) + # logger.debug(f"AWHP cooling water supply temperature: {awhp_c_supply_t}°C") + awhp_c_performance = awhp_c.performance_cooling.leaving_supply_t[awhp_c_supply_t] else: - cap_ref = float(awhp_h_performance.capacity_W) + sizing_priority = "heating" + + # Determine reference capacity + if sizing_priority == "heating": + sizing_load = "hhw_W" + + if isinstance(awhp_h_performance.capacity_W, np.ndarray): + # closest supply temperature to input (for reset, this defaults to the lowest) + awhp_h_supply_temps_str = list(awhp_h.performance_heating.leaving_supply_t.keys()) + awhp_h_supply_temps = np.array(awhp_h_supply_temps_str, dtype=float) + ref_supply_temp = awhp_h_supply_temps_str[ + np.argmin(np.abs(awhp_h_supply_temps - awhp_h_supply_t)) + ] + + ref_temp_C = 0.0 # Conservative outdoor temperature for sizing + ref_capacity_W = awhp_h.performance_heating.leaving_supply_t[ + ref_supply_temp + ].capacity_W + + cap_ref = interp_vector( + awhp_h.performance_heating.t_out_C, + ref_capacity_W, + np.array([ref_temp_C]), + )[0] - # --- Sizing Logic --- + else: + cap_ref = float(awhp_h_performance.capacity_W) + + elif sizing_priority == "cooling": + sizing_load = "chw_W" # check column names + + if isinstance(awhp_c_performance.capacity_W, list): + ref_supply_temp = awhp_c_supply_t + ref_capacity_W = awhp_c.performance_cooling.leaving_supply_t[ + ref_supply_temp + ].capacity_W + + ref_temp_C = 30.0 # Conservative outdoor temperature for sizing + cap_ref = interp_vector( + awhp_c.performance_cooling.t_out_C, + ref_capacity_W, + np.array([ref_temp_C]), + )[0] + else: + cap_ref = float(awhp_c_performance.capacity_W) + + elif sizing_priority == "both": + pass # come back to this + + # Determine number of units if sizing_mode in [ "integer_sizing_peak_load", "fractional_sizing_peak_load", ]: # Fraction of peak HHW load at reference temperature - peak_hhw_W = float(df["hhw_W"].max()) - target_load_W = peak_hhw_W * sizing_value + # peak_hhw_W = float(df["hhw_W"].max()) + # target_load_W = peak_hhw_W * sizing_value + + peak_load_W = float(df[sizing_load].max()) + target_load_W = peak_load_W * sizing_value + + # if sizing_mode == "integer_sizing_peak_load": + # awhp_num_h = np.ceil(target_load_W / cap_ref) + # awhp_num_h = int(max(1, awhp_num_h)) # Ensure at least one unit + # else: + # awhp_num_h = target_load_W / cap_ref + + # elif sizing_mode == "fixed_num_units": + # awhp_num_h = np.ceil(sizing_value) + # awhp_num_h = int(max(1, awhp_num_h)) # Ensure at least one unit + + # awhp_num_h = max(awhp_num_h, 0) if sizing_mode == "integer_sizing_peak_load": - awhp_num_h = np.ceil(target_load_W / cap_ref) - awhp_num_h = int(max(1, awhp_num_h)) # Ensure at least one unit + awhp_num = np.ceil(target_load_W / cap_ref) + awhp_num = int(max(1, awhp_num)) # Ensure at least one unit else: - awhp_num_h = target_load_W / cap_ref + awhp_num = target_load_W / cap_ref elif sizing_mode == "fixed_num_units": - awhp_num_h = np.ceil(sizing_value) - awhp_num_h = int(max(1, awhp_num_h)) # Ensure at least one unit + awhp_num = np.ceil(sizing_value) + awhp_num = int(max(1, awhp_num)) # Ensure at least one unit - awhp_num_h = max(awhp_num_h, 0) + awhp_num = max(awhp_num, 0) # --- Redundancy Logic --- - awhp_num_h_r = awhp_num_h + redundancy + # awhp_num_h_r = awhp_num_h + redundancy + awhp_num_r = awhp_num + redundancy logger.debug( - f"AWHP sizing: mode={sizing_mode}, value={sizing_value}, " - f"units={awhp_num_h:.2f}, with {redundancy} redundant = {awhp_num_h_r:.2f} total" + # f"AWHP sizing: mode={sizing_mode}, value={sizing_value}, " + f"AWHP sizing: priority={sizing_priority}, mode={sizing_mode}, value={sizing_value}, " + # f"units={awhp_num_h:.2f}, with {redundancy} redundant = {awhp_num_h_r:.2f} total" + f"units={awhp_num:.2f}, with {redundancy} redundant = {awhp_num_r:.2f} total" ) # capacity calculations use the original sizing number - cap_total_h_W = awhp_cap_h * awhp_num_h + # cap_total_h_W = awhp_cap_h * awhp_num_h + cap_total_h_W = awhp_cap_h * awhp_num served_h_W = np.minimum(df[Col.HHW_REM_W.value].to_numpy(), cap_total_h_W) elec_h_Wh = served_h_W / awhp_cop_h @@ -715,7 +794,8 @@ def loads_to_site_energy( total_awhp_refrigerant_weight_kg = ( awhp_h.refrigerant_weight_g * 0.001 - * awhp_num_h_r + # * awhp_num_h_r + * awhp_num_r / num_hours # emissions calculations use the redundancy sizing number if awhp_h.refrigerant_weight_g else 0.0 @@ -739,8 +819,10 @@ def loads_to_site_energy( df[Col.ELEC_AWHP_H_WH.value] = elec_h_Wh df[Col.ELEC_WH.value] += elec_h_Wh df[Col.HHW_REM_W.value] -= served_h_W - df[Col.AWHP_NUM_H.value] = float(awhp_num_h) - df[Col.AWHP_NUM_H_R.value] = float(awhp_num_h_r) + # df[Col.AWHP_NUM_H.value] = float(awhp_num_h) + # df[Col.AWHP_NUM_H_R.value] = float(awhp_num_h_r) + df[Col.AWHP_NUM.value] = float(awhp_num) + df[Col.AWHP_NUM_R.value] = float(awhp_num_r) df[Col.AWHP_REFRIGERANT.value] = awhp_refrigerant df[Col.AWHP_REFRIGERANT_WEIGHT_KG.value] = total_awhp_refrigerant_weight_kg df[Col.AWHP_REFRIGERANT_GWP.value] = total_awhp_refrigerant_gwp_kg @@ -832,22 +914,26 @@ def loads_to_site_energy( # Phase 5 - AWHP Cooling # ========================= if scen.awhp and scen.awhp_use_cooling: - awhp_c = library.get_equipment(scen.awhp) + # awhp_c = library.get_equipment(scen.awhp) - # we don't have any HPs with >1 CHWST, this can be edited later to match HHWST if needed - # this extracts the first value and uses that performance data - awhp_c_supply_t = next(iter(awhp_c.performance_cooling.leaving_supply_t.keys())) + # # we don't have any HPs with >1 CHWST, this can be edited later to match HHWST if needed + # # this extracts the first value and uses that performance data + # awhp_c_supply_t = next(iter(awhp_c.performance_cooling.leaving_supply_t.keys())) # logger.debug(f"AWHP cooling water supply temperature: {awhp_c_supply_t}°C") - awhp_c_performance = awhp_c.performance_cooling.leaving_supply_t[awhp_c_supply_t] + # awhp_c_performance = awhp_c.performance_cooling.leaving_supply_t[awhp_c_supply_t] awhp_cap_c = _per_unit_cooling_capacity_W(awhp_c, temps, awhp_c_performance) awhp_cop_c = _per_unit_cooling_cop(awhp_c, temps, awhp_c_performance) - awhp_num_c = awhp_num_h # use same number of units as heating + # awhp_num_c = awhp_num # use same number of units as heating + + # logger.debug(f"Phase 5: AWHP Cooling with {awhp_num_c:.2f} units") + + # cap_total_c_W = awhp_cap_c * awhp_num_c - logger.debug(f"Phase 5: AWHP Cooling with {awhp_num_c:.2f} units") + logger.debug(f"Phase 5: AWHP Cooling with {awhp_num:.2f} units") - cap_total_c_W = awhp_cap_c * awhp_num_c + cap_total_c_W = awhp_cap_c * awhp_num mask = ( df[Col.AWHP_HHW_W.value] == 0 @@ -865,7 +951,7 @@ def loads_to_site_energy( df[Col.ELEC_AWHP_C_WH.value] = elec_c_Wh df[Col.ELEC_WH.value] += elec_c_Wh df[Col.CHW_REM_W.value] -= served_c_W - df[Col.AWHP_NUM_C] = float(awhp_num_c) + # df[Col.AWHP_NUM_C] = float(awhp_num_c) awhp_c_coverage = ( (np.nansum(served_c_W) / np.nansum(df[Col.CHW_W.value])) * 100 @@ -998,8 +1084,10 @@ def _finalize_columns(df: pd.DataFrame, detail: bool) -> list[str]: Col.HR_WWHP_REFRIGERANT.value, Col.HR_WWHP_REFRIGERANT_WEIGHT_KG.value, Col.HR_WWHP_REFRIGERANT_GWP.value, - Col.AWHP_NUM_H.value, - Col.AWHP_NUM_H_R.value, + # Col.AWHP_NUM_H.value, + # Col.AWHP_NUM_H_R.value, + Col.AWHP_NUM.value, + Col.AWHP_NUM_R.value, Col.AWHP_CAP_H_W.value, Col.AWHP_COP_H.value, Col.AWHP_HHW_W.value, @@ -1012,7 +1100,7 @@ def _finalize_columns(df: pd.DataFrame, detail: bool) -> list[str]: Col.GAS_BOILER_WH.value, Col.RES_HHW_W.value, Col.ELEC_RES_WH.value, - Col.AWHP_NUM_C.value, + # Col.AWHP_NUM_C.value, Col.AWHP_CAP_C_W.value, Col.AWHP_COP_C.value, Col.AWHP_CHW_W.value, diff --git a/utils/units.py b/utils/units.py index e6bc981..45b1fa9 100644 --- a/utils/units.py +++ b/utils/units.py @@ -315,9 +315,11 @@ def sqft_to_sqm(sqft): "chiller_cop": (None, "Chiller COP"), "boiler_eff": (None, "Boiler Efficiency"), # === Equipment Counts (dimensionless) === - "awhp_num_h": (None, "AWHP Count (Heating)"), - "awhp_num_h_redundant": (None, "AWHP Redundant (Heating)"), - "awhp_num_c": (None, "AWHP Count (Cooling)"), + # "awhp_num_h": (None, "AWHP Count (Heating)"), + # "awhp_num_h_redundant": (None, "AWHP Redundant (Heating)"), + # "awhp_num_c": (None, "AWHP Count (Cooling)"), + "awhp_num": (None, "AWHP Count"), + "awhp_num_redundant": (None, "AWHP Redundant Count"), # === Refrigerant Type (text - no conversion) === "chiller_refrigerant": (None, "Chiller Refrigerant"), "hr_wwhp_refrigerant": (None, "HR-WWHP Refrigerant"), From 76c8c92858a972a6505d477f4558b7f7e6a8c25d Mon Sep 17 00:00:00 2001 From: urwahah Date: Thu, 18 Jun 2026 20:34:29 -0700 Subject: [PATCH 2/8] add larger-load sizing --- src/energy.py | 155 +++++++++++++++++++------------------------------- 1 file changed, 58 insertions(+), 97 deletions(-) diff --git a/src/energy.py b/src/energy.py index 793b771..af60bac 100644 --- a/src/energy.py +++ b/src/energy.py @@ -434,25 +434,44 @@ def _capacity_constraints( return cap -# def _awhp_cooling_reference_capacity(performance: PerformanceCurves) -> float: -# """AWHP cooling reference capacity [W] for sizing.""" - -# if isinstance(performance.capacity_W, list): -# ref_supply_temp = awhp_c_supply_t -# ref_capacity_W = awhp_c.performance_cooling.leaving_supply_t[ -# ref_supply_temp -# ].capacity_W +def _awhp_reference_capacity( + e: Equipment, + performance: PerformanceCurves, + supply_t: float, + load_type: str, +) -> float: + """AWHP reference capacity [W] for sizing.""" + + cap_type = {"heating": np.ndarray, "cooling": list} + + if isinstance(performance.capacity_W, cap_type[load_type]): + if load_type == "heating": + ref_temp_C = 0.0 # Conservative outdoor temperature for sizing + + # closest supply temperature to input (for reset, this defaults to the lowest) + awhp_h_supply_temps_str = list(e.performance_heating.leaving_supply_t.keys()) + awhp_h_supply_temps = np.array(awhp_h_supply_temps_str, dtype=float) + ref_supply_temp = awhp_h_supply_temps_str[ + np.argmin(np.abs(awhp_h_supply_temps - supply_t)) + ] + + elif load_type == "cooling": + ref_temp_C = 30.0 # Conservative outdoor temperature for sizing + ref_supply_temp = supply_t + + ref_capacity_W = e.performance[load_type].leaving_supply_t[ + ref_supply_temp + ].capacity_W -# ref_temp_C = 30.0 # Conservative outdoor temperature for sizing -# cap_ref = interp_vector( -# awhp_c.performance_cooling.t_out_C, -# ref_capacity_W, -# np.array([ref_temp_C]), -# )[0] -# else: -# cap_ref = float(performance.capacity_W) + cap_ref = interp_vector( + e.performance[load_type].t_out_C, + ref_capacity_W, + np.array([ref_temp_C]), + )[0] + else: + cap_ref = float(e.capacity_W) -# return cap_ref + return cap_ref def loads_to_site_energy( @@ -677,7 +696,7 @@ def loads_to_site_energy( redundancy = scen.awhp_redundancy if scen.awhp_use_cooling: - sizing_priority = "cooling" # scen.awhp_sizing_priority + sizing_priority = "both" # scen.awhp_sizing_priority awhp_c = library.get_equipment(scen.awhp) # we don't have any HPs with >1 CHWST, this can be edited later to match HHWST if needed @@ -691,74 +710,39 @@ def loads_to_site_energy( # Determine reference capacity if sizing_priority == "heating": sizing_load = "hhw_W" + cap_ref = _awhp_reference_capacity(awhp_h, awhp_h_performance, awhp_h_supply_t, "heating") - if isinstance(awhp_h_performance.capacity_W, np.ndarray): - # closest supply temperature to input (for reset, this defaults to the lowest) - awhp_h_supply_temps_str = list(awhp_h.performance_heating.leaving_supply_t.keys()) - awhp_h_supply_temps = np.array(awhp_h_supply_temps_str, dtype=float) - ref_supply_temp = awhp_h_supply_temps_str[ - np.argmin(np.abs(awhp_h_supply_temps - awhp_h_supply_t)) - ] - - ref_temp_C = 0.0 # Conservative outdoor temperature for sizing - ref_capacity_W = awhp_h.performance_heating.leaving_supply_t[ - ref_supply_temp - ].capacity_W - - cap_ref = interp_vector( - awhp_h.performance_heating.t_out_C, - ref_capacity_W, - np.array([ref_temp_C]), - )[0] - - else: - cap_ref = float(awhp_h_performance.capacity_W) - elif sizing_priority == "cooling": - sizing_load = "chw_W" # check column names - - if isinstance(awhp_c_performance.capacity_W, list): - ref_supply_temp = awhp_c_supply_t - ref_capacity_W = awhp_c.performance_cooling.leaving_supply_t[ - ref_supply_temp - ].capacity_W - - ref_temp_C = 30.0 # Conservative outdoor temperature for sizing - cap_ref = interp_vector( - awhp_c.performance_cooling.t_out_C, - ref_capacity_W, - np.array([ref_temp_C]), - )[0] - else: - cap_ref = float(awhp_c_performance.capacity_W) + sizing_load = "chw_W" + cap_ref = _awhp_reference_capacity(awhp_c, awhp_c_performance, awhp_c_supply_t, "cooling") - elif sizing_priority == "both": - pass # come back to this + elif sizing_priority == "both" and sizing_mode in [ + "integer_sizing_peak_load", + "fractional_sizing_peak_load", + ]: + cap_ref = { + "hhw_W": _awhp_reference_capacity(awhp_h, awhp_h_performance, awhp_h_supply_t, "heating"), + "chw_W": _awhp_reference_capacity(awhp_c, awhp_c_performance, awhp_c_supply_t, "cooling") + } + num = { + "hhw_W": float(df["hhw_W"].max()) * sizing_value / cap_ref["hhw_W"], + "chw_W": float(df["chw_W"].max()) * sizing_value / cap_ref["chw_W"] + } + sizing_load = max(num, key = num.get) + cap_ref = cap_ref[sizing_load] + + logger.debug(f"{sizing_load} drives AWHP sizing.") + # Determine number of units if sizing_mode in [ "integer_sizing_peak_load", "fractional_sizing_peak_load", ]: - # Fraction of peak HHW load at reference temperature - # peak_hhw_W = float(df["hhw_W"].max()) - # target_load_W = peak_hhw_W * sizing_value - + # Fraction of peak load at reference temperature peak_load_W = float(df[sizing_load].max()) target_load_W = peak_load_W * sizing_value - # if sizing_mode == "integer_sizing_peak_load": - # awhp_num_h = np.ceil(target_load_W / cap_ref) - # awhp_num_h = int(max(1, awhp_num_h)) # Ensure at least one unit - # else: - # awhp_num_h = target_load_W / cap_ref - - # elif sizing_mode == "fixed_num_units": - # awhp_num_h = np.ceil(sizing_value) - # awhp_num_h = int(max(1, awhp_num_h)) # Ensure at least one unit - - # awhp_num_h = max(awhp_num_h, 0) - if sizing_mode == "integer_sizing_peak_load": awhp_num = np.ceil(target_load_W / cap_ref) awhp_num = int(max(1, awhp_num)) # Ensure at least one unit @@ -772,18 +756,14 @@ def loads_to_site_energy( awhp_num = max(awhp_num, 0) # --- Redundancy Logic --- - # awhp_num_h_r = awhp_num_h + redundancy awhp_num_r = awhp_num + redundancy logger.debug( - # f"AWHP sizing: mode={sizing_mode}, value={sizing_value}, " f"AWHP sizing: priority={sizing_priority}, mode={sizing_mode}, value={sizing_value}, " - # f"units={awhp_num_h:.2f}, with {redundancy} redundant = {awhp_num_h_r:.2f} total" f"units={awhp_num:.2f}, with {redundancy} redundant = {awhp_num_r:.2f} total" ) # capacity calculations use the original sizing number - # cap_total_h_W = awhp_cap_h * awhp_num_h cap_total_h_W = awhp_cap_h * awhp_num served_h_W = np.minimum(df[Col.HHW_REM_W.value].to_numpy(), cap_total_h_W) elec_h_Wh = served_h_W / awhp_cop_h @@ -794,7 +774,6 @@ def loads_to_site_energy( total_awhp_refrigerant_weight_kg = ( awhp_h.refrigerant_weight_g * 0.001 - # * awhp_num_h_r * awhp_num_r / num_hours # emissions calculations use the redundancy sizing number if awhp_h.refrigerant_weight_g @@ -819,8 +798,6 @@ def loads_to_site_energy( df[Col.ELEC_AWHP_H_WH.value] = elec_h_Wh df[Col.ELEC_WH.value] += elec_h_Wh df[Col.HHW_REM_W.value] -= served_h_W - # df[Col.AWHP_NUM_H.value] = float(awhp_num_h) - # df[Col.AWHP_NUM_H_R.value] = float(awhp_num_h_r) df[Col.AWHP_NUM.value] = float(awhp_num) df[Col.AWHP_NUM_R.value] = float(awhp_num_r) df[Col.AWHP_REFRIGERANT.value] = awhp_refrigerant @@ -914,27 +891,12 @@ def loads_to_site_energy( # Phase 5 - AWHP Cooling # ========================= if scen.awhp and scen.awhp_use_cooling: - # awhp_c = library.get_equipment(scen.awhp) - - # # we don't have any HPs with >1 CHWST, this can be edited later to match HHWST if needed - # # this extracts the first value and uses that performance data - # awhp_c_supply_t = next(iter(awhp_c.performance_cooling.leaving_supply_t.keys())) - # logger.debug(f"AWHP cooling water supply temperature: {awhp_c_supply_t}°C") - # awhp_c_performance = awhp_c.performance_cooling.leaving_supply_t[awhp_c_supply_t] - awhp_cap_c = _per_unit_cooling_capacity_W(awhp_c, temps, awhp_c_performance) awhp_cop_c = _per_unit_cooling_cop(awhp_c, temps, awhp_c_performance) - # awhp_num_c = awhp_num # use same number of units as heating - - # logger.debug(f"Phase 5: AWHP Cooling with {awhp_num_c:.2f} units") - - # cap_total_c_W = awhp_cap_c * awhp_num_c - logger.debug(f"Phase 5: AWHP Cooling with {awhp_num:.2f} units") cap_total_c_W = awhp_cap_c * awhp_num - mask = ( df[Col.AWHP_HHW_W.value] == 0 ) # create a mask for hours when no heating is served by AWHP @@ -951,7 +913,6 @@ def loads_to_site_energy( df[Col.ELEC_AWHP_C_WH.value] = elec_c_Wh df[Col.ELEC_WH.value] += elec_c_Wh df[Col.CHW_REM_W.value] -= served_c_W - # df[Col.AWHP_NUM_C] = float(awhp_num_c) awhp_c_coverage = ( (np.nansum(served_c_W) / np.nansum(df[Col.CHW_W.value])) * 100 From bf88442d80ac43a54adb310defe06d858108bac2 Mon Sep 17 00:00:00 2001 From: urwahah Date: Thu, 18 Jun 2026 21:39:06 -0700 Subject: [PATCH 3/8] allow simultaneous AWHP operation --- src/config.py | 4 +--- src/energy.py | 29 +++++++++++++++-------------- utils/units.py | 4 +--- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/config.py b/src/config.py index d53ea21..f4d1d54 100644 --- a/src/config.py +++ b/src/config.py @@ -99,8 +99,6 @@ class Columns(StrEnum): ELEC_HR_WH = "elec_hr_Wh" # --- Air-Water Heat Pump (Heating) --- - # AWHP_NUM_H = "awhp_num_h" - # AWHP_NUM_H_R = "awhp_num_h_redundant" AWHP_NUM = "awhp_num" AWHP_NUM_R = "awhp_num_redundant" AWHP_CAP_H_W = "awhp_cap_h_W" @@ -118,7 +116,7 @@ class Columns(StrEnum): ELEC_RES_WH = "elec_res_Wh" # --- Air-Water Heat Pump (Cooling) --- - # AWHP_NUM_C = "awhp_num_c" + AWHP_NUM_C = "awhp_num_c" AWHP_CAP_C_W = "awhp_cap_c_W" AWHP_COP_C = "awhp_cop_c" AWHP_CHW_W = "awhp_chw_W" diff --git a/src/energy.py b/src/energy.py index af60bac..fbf59fe 100644 --- a/src/energy.py +++ b/src/energy.py @@ -528,8 +528,6 @@ def loads_to_site_energy( Col.AWHP_HHW_W.value, Col.AWHP_COP_H.value, Col.AWHP_CAP_H_W.value, - # Col.AWHP_NUM_H.value, - # Col.AWHP_NUM_H_R.value, Col.AWHP_NUM.value, Col.AWHP_NUM_R.value, Col.ELEC_AWHP_H_WH.value, @@ -544,7 +542,7 @@ def loads_to_site_energy( Col.AWHP_CHW_W.value, Col.AWHP_COP_C.value, Col.AWHP_CAP_C_W.value, - # Col.AWHP_NUM_C.value, + Col.AWHP_NUM_C.value, Col.ELEC_AWHP_C_WH.value, # Electric chiller Col.CHILLER_CHW_W.value, @@ -896,15 +894,19 @@ def loads_to_site_energy( logger.debug(f"Phase 5: AWHP Cooling with {awhp_num:.2f} units") - cap_total_c_W = awhp_cap_c * awhp_num - mask = ( - df[Col.AWHP_HHW_W.value] == 0 - ) # create a mask for hours when no heating is served by AWHP - served_c_W = np.zeros(len(df)) # Initialize served_c_W as zeros - served_c_W[mask] = np.minimum( - df.loc[mask, Col.CHW_REM_W.value].to_numpy(), cap_total_c_W[mask] - ) # Compute only where mask is True + # cap_total_c_W = awhp_cap_c * awhp_num + # assuming that AWHPs all have 50% turndown (2 compressors) + num_compressors = awhp_num * 2 + # calculate number of "compressors" used to serve heating load + num_compressors_h = np.ceil((df[Col.AWHP_HHW_W.value] / df[Col.AWHP_CAP_H_W.value]) * num_compressors) + # remaining compressors can serve cooling load + num_compressors_c = num_compressors - num_compressors_h + awhp_num_c = num_compressors_c / 2 # number of compressors available to operate in cooling + + cap_total_c_W = awhp_cap_c * awhp_num_c + served_c_W = np.minimum(df[Col.CHW_REM_W.value].to_numpy(), cap_total_c_W) + # Compute electricity only where cooling is served elec_c_Wh = served_c_W / awhp_cop_c df[Col.AWHP_CHW_W.value] = served_c_W @@ -913,6 +915,7 @@ def loads_to_site_energy( df[Col.ELEC_AWHP_C_WH.value] = elec_c_Wh df[Col.ELEC_WH.value] += elec_c_Wh df[Col.CHW_REM_W.value] -= served_c_W + df[Col.AWHP_NUM_C] = awhp_num_c awhp_c_coverage = ( (np.nansum(served_c_W) / np.nansum(df[Col.CHW_W.value])) * 100 @@ -1045,8 +1048,6 @@ def _finalize_columns(df: pd.DataFrame, detail: bool) -> list[str]: Col.HR_WWHP_REFRIGERANT.value, Col.HR_WWHP_REFRIGERANT_WEIGHT_KG.value, Col.HR_WWHP_REFRIGERANT_GWP.value, - # Col.AWHP_NUM_H.value, - # Col.AWHP_NUM_H_R.value, Col.AWHP_NUM.value, Col.AWHP_NUM_R.value, Col.AWHP_CAP_H_W.value, @@ -1061,7 +1062,7 @@ def _finalize_columns(df: pd.DataFrame, detail: bool) -> list[str]: Col.GAS_BOILER_WH.value, Col.RES_HHW_W.value, Col.ELEC_RES_WH.value, - # Col.AWHP_NUM_C.value, + Col.AWHP_NUM_C.value, Col.AWHP_CAP_C_W.value, Col.AWHP_COP_C.value, Col.AWHP_CHW_W.value, diff --git a/utils/units.py b/utils/units.py index 45b1fa9..b30c83e 100644 --- a/utils/units.py +++ b/utils/units.py @@ -315,9 +315,7 @@ def sqft_to_sqm(sqft): "chiller_cop": (None, "Chiller COP"), "boiler_eff": (None, "Boiler Efficiency"), # === Equipment Counts (dimensionless) === - # "awhp_num_h": (None, "AWHP Count (Heating)"), - # "awhp_num_h_redundant": (None, "AWHP Redundant (Heating)"), - # "awhp_num_c": (None, "AWHP Count (Cooling)"), + "awhp_num_c": (None, "AWHP Count (Cooling)"), "awhp_num": (None, "AWHP Count"), "awhp_num_redundant": (None, "AWHP Redundant Count"), # === Refrigerant Type (text - no conversion) === From 4abb37006ef7cf6afa0501e80e0cc62854ff0e1d Mon Sep 17 00:00:00 2001 From: urwahah Date: Thu, 18 Jun 2026 22:18:42 -0700 Subject: [PATCH 4/8] add sizing priority input --- data/input/equipment_data.JSON | 15 +++++++++++++ docs/documentation/equipment.md | 4 ++++ layout/input.py | 37 ++++++++++++++++++++++++++++----- layout/output.py | 1 + pages/equipment_page.py | 13 ++++++++++-- src/config.py | 1 + src/energy.py | 6 +++--- src/equipment.py | 3 +++ 8 files changed, 70 insertions(+), 10 deletions(-) diff --git a/data/input/equipment_data.JSON b/data/input/equipment_data.JSON index af2fada..8d2cb04 100644 --- a/data/input/equipment_data.JSON +++ b/data/input/equipment_data.JSON @@ -740,6 +740,7 @@ "awhp_redundancy": 1, "awhp_h_supply_t": 38, "awhp_use_cooling": false, + "awhp_sizing_priority": "heating", "backup_heating": "bo03", "chiller": "ch03" }, @@ -756,6 +757,7 @@ "awhp_redundancy": 1, "awhp_h_supply_t": 38, "awhp_use_cooling": false, + "awhp_sizing_priority": "heating", "backup_heating": "res02", "chiller": "ch03" }, @@ -772,6 +774,7 @@ "awhp_redundancy": 1, "awhp_h_supply_t": 38, "awhp_use_cooling": true, + "awhp_sizing_priority": "heating", "backup_heating": "bo03", "chiller": "ch03" }, @@ -788,6 +791,7 @@ "awhp_redundancy": 1, "awhp_h_supply_t": 38, "awhp_use_cooling": true, + "awhp_sizing_priority": "heating", "backup_heating": "res02", "chiller": "ch03" }, @@ -804,6 +808,7 @@ "awhp_redundancy": 1, "awhp_h_supply_t": 38, "awhp_use_cooling": true, + "awhp_sizing_priority": "heating", "backup_heating": "res02", "chiller": "ch03" }, @@ -820,6 +825,7 @@ "awhp_redundancy": 1, "awhp_h_supply_t": 38, "awhp_use_cooling": true, + "awhp_sizing_priority": "heating", "backup_heating": "bo03", "chiller": "ch03" }, @@ -836,6 +842,7 @@ "awhp_redundancy": 1, "awhp_h_supply_t": 38, "awhp_use_cooling": true, + "awhp_sizing_priority": "heating", "backup_heating": "bo03", "chiller": "ch03" }, @@ -852,6 +859,7 @@ "awhp_redundancy": 1, "awhp_h_supply_t": 38, "awhp_use_cooling": true, + "awhp_sizing_priority": "heating", "backup_heating": "bo03", "chiller": "ch03" }, @@ -868,6 +876,7 @@ "awhp_redundancy": 1, "awhp_h_supply_t": 38, "awhp_use_cooling": true, + "awhp_sizing_priority": "heating", "backup_heating": "bo03", "chiller": "ch03" }, @@ -884,6 +893,7 @@ "awhp_redundancy": 1, "awhp_h_supply_t": 38, "awhp_use_cooling": false, + "awhp_sizing_priority": "heating", "backup_heating": "bo03", "chiller": "ch03" }, @@ -900,6 +910,7 @@ "awhp_redundancy": 1, "awhp_h_supply_t": 38, "awhp_use_cooling": false, + "awhp_sizing_priority": "heating", "backup_heating": "res02", "chiller": "ch03" }, @@ -916,6 +927,7 @@ "awhp_redundancy": 1, "awhp_h_supply_t": 38, "awhp_use_cooling": true, + "awhp_sizing_priority": "heating", "backup_heating": "res02", "chiller": "ch03" }, @@ -932,6 +944,7 @@ "awhp_redundancy": 1, "awhp_h_supply_t": 52, "awhp_use_cooling": true, + "awhp_sizing_priority": "heating", "backup_heating": "res02", "chiller": "ch03" }, @@ -948,6 +961,7 @@ "awhp_redundancy": 1, "awhp_h_supply_t": 38, "awhp_use_cooling": true, + "awhp_sizing_priority": "heating", "backup_heating": "res02", "chiller": "ch03" }, @@ -964,6 +978,7 @@ "awhp_redundancy": 1, "awhp_h_supply_t": 52, "awhp_use_cooling": true, + "awhp_sizing_priority": "heating", "backup_heating": "res02", "chiller": "ch03" } diff --git a/docs/documentation/equipment.md b/docs/documentation/equipment.md index be954df..58dc2c1 100644 --- a/docs/documentation/equipment.md +++ b/docs/documentation/equipment.md @@ -66,6 +66,10 @@ These two inputs determine how the total number of AWHP units, not including red * **Fractional sizing (peak load)**: The number of units required to meet the user-specified percentage of annual peak heating load, unrounded. * **Fixed number of units**: User-specified actual number of units. +#### AWHP Sizing Priority + +This input determines which load profile is used to calculate the number of units per the sizing mode and value inputs. + #### AWHP Redundancy An integer number of additional units for redundancy. The default value is 1, i.e., N+1 redundancy. The total number of AWHP units, including redundancy, is used in refrigerant leakage calculations. diff --git a/layout/input.py b/layout/input.py index ce6f791..710875c 100644 --- a/layout/input.py +++ b/layout/input.py @@ -526,6 +526,7 @@ def build_equipment_table( ("awhp_sizing_value", "AWHP Sizing Value"), ("awhp_redundancy", "AWHP Redundancy"), ("awhp_use_cooling", "AWHP Use Cooling"), + ("awhp_sizing_priority", "AWHP Sizing Priority"), ("backup_heating", "Backup Heating"), ("chiller", "Backup Cooling"), ] @@ -976,12 +977,38 @@ def edit_equipment_modal(): ], grow=True, ), - dmc.Switch( - id="edit-awhp-use-cooling", - label="Use heat pump also for cooling", - mt="xs", + dmc.Group( + [ + dmc.Switch( + id="edit-awhp-use-cooling", + label="Use heat pump also for cooling", + mt="xs", + ), + dmc.Select( + id="edit-awhp-sizing-priority", + label="Sizing priority", + placeholder="None", + data=[ + { + "label": "Heating load", + "value": "heating", + }, + { + "label": "Cooling load", + "value": "cooling", + }, + { + "label": "Larger of heating and cooling load", + "value": "larger", + }, + ], + clearable=True, + searchable=True, + ), + ], + grow = True, ), - ] + ], ), dmc.Divider(label="Backup equipment", labelPosition="center"), dmc.SimpleGrid( diff --git a/layout/output.py b/layout/output.py index 492369b..dd80415 100644 --- a/layout/output.py +++ b/layout/output.py @@ -163,6 +163,7 @@ def get_value(self, path: str): ("awhp_sizing_value", "AWHP Sizing Value"), ("awhp_redundancy", "AWHP Redundancy"), ("awhp_use_cooling", "AWHP Use Cooling"), + ("awhp_sizing_priority", "AWHP Sizing Priority"), ("backup_heating", "Backup Heating"), ("chiller", "Chiller"), ] diff --git a/pages/equipment_page.py b/pages/equipment_page.py index efc5c20..4fd1017 100644 --- a/pages/equipment_page.py +++ b/pages/equipment_page.py @@ -681,6 +681,7 @@ def reset_equipment(n_clicks, initial_data): Output("edit-awhp-sizing-value", "value"), Output("edit-awhp-redundancy", "value"), Output("edit-awhp-use-cooling", "checked"), + Output("edit-awhp-sizing-priority", "value"), Output("edit-backup-heating-select", "data"), Output("edit-backup-heating-select", "value"), Output("edit-chiller-select", "data"), @@ -697,7 +698,7 @@ def open_edit_modal(edit_clicks, equipment_data, unit_mode): pre-filling all editable fields. """ if not any(edit_clicks or []): - return (no_update,) * 20 + return (no_update,) * 21 if not equipment_data: return ( @@ -716,6 +717,7 @@ def open_edit_modal(edit_clicks, equipment_data, unit_mode): None, None, False, + None, [], None, [], @@ -728,7 +730,7 @@ def open_edit_modal(edit_clicks, equipment_data, unit_mode): triggered = callback_context.triggered if not triggered: - return (no_update,) * 20 + return (no_update,) * 21 prop_id = triggered[0]["prop_id"] id_str = prop_id.split(".")[0] @@ -752,6 +754,7 @@ def open_edit_modal(edit_clicks, equipment_data, unit_mode): None, None, False, + None, [], None, [], @@ -781,6 +784,7 @@ def open_edit_modal(edit_clicks, equipment_data, unit_mode): None, None, False, + None, [], None, [], @@ -844,6 +848,7 @@ def open_edit_modal(edit_clicks, equipment_data, unit_mode): sizing_value = scenario.get("awhp_sizing_value", 1.0) redundancy = scenario.get("awhp_redundancy", 1) use_cooling = scenario.get("awhp_use_cooling", False) + sizing_priority = scenario.get("awhp_sizing_priority") or "heating" backup_heating_val = scenario.get("backup_heating") chiller_val = scenario.get("chiller") @@ -864,6 +869,7 @@ def open_edit_modal(edit_clicks, equipment_data, unit_mode): sizing_value, redundancy, use_cooling, + sizing_priority, backup_heating_options, backup_heating_val, chiller_options, @@ -889,6 +895,7 @@ def open_edit_modal(edit_clicks, equipment_data, unit_mode): State("edit-awhp-sizing-value", "value"), State("edit-awhp-redundancy", "value"), State("edit-awhp-use-cooling", "checked"), + State("edit-awhp-sizing-priority", "value"), State("edit-backup-heating-select", "value"), State("edit-chiller-select", "value"), State("equipment-store", "data"), @@ -909,6 +916,7 @@ def save_edit_scenario( sizing_value, redundancy, use_cooling, + sizing_priority, backup_heating_val, chiller_val, equipment_data, @@ -980,6 +988,7 @@ def save_edit_scenario( new_scen["awhp_sizing_value"] = sizing_value new_scen["awhp_redundancy"] = redundancy new_scen["awhp_use_cooling"] = use_cooling + new_scen["awhp_sizing_priority"] = sizing_priority new_scen["backup_heating"] = backup_heating_val new_scen["chiller"] = chiller_val new_scenarios.append(new_scen) diff --git a/src/config.py b/src/config.py index f4d1d54..f7d6bda 100644 --- a/src/config.py +++ b/src/config.py @@ -34,6 +34,7 @@ class EquipmentTableRows(Enum): "awhp_sizing_value", "awhp_redundancy", "awhp_use_cooling", + "awhp_sizing_priority", "backup_heating", "chiller", ) diff --git a/src/energy.py b/src/energy.py index fbf59fe..0fde1c4 100644 --- a/src/energy.py +++ b/src/energy.py @@ -694,7 +694,7 @@ def loads_to_site_energy( redundancy = scen.awhp_redundancy if scen.awhp_use_cooling: - sizing_priority = "both" # scen.awhp_sizing_priority + sizing_priority = scen.awhp_sizing_priority awhp_c = library.get_equipment(scen.awhp) # we don't have any HPs with >1 CHWST, this can be edited later to match HHWST if needed @@ -714,7 +714,7 @@ def loads_to_site_energy( sizing_load = "chw_W" cap_ref = _awhp_reference_capacity(awhp_c, awhp_c_performance, awhp_c_supply_t, "cooling") - elif sizing_priority == "both" and sizing_mode in [ + elif sizing_priority == "larger" and sizing_mode in [ "integer_sizing_peak_load", "fractional_sizing_peak_load", ]: @@ -906,7 +906,7 @@ def loads_to_site_energy( cap_total_c_W = awhp_cap_c * awhp_num_c served_c_W = np.minimum(df[Col.CHW_REM_W.value].to_numpy(), cap_total_c_W) - + # Compute electricity only where cooling is served elec_c_Wh = served_c_W / awhp_cop_c df[Col.AWHP_CHW_W.value] = served_c_W diff --git a/src/equipment.py b/src/equipment.py index 573a8cd..0d13532 100644 --- a/src/equipment.py +++ b/src/equipment.py @@ -74,6 +74,9 @@ class EquipmentScenario(DotAccessMixin, BaseModel): awhp_sizing_value: float awhp_redundancy: int awhp_use_cooling: bool + awhp_sizing_priority: ( + Literal["heating", "cooling", "larger"] | None + ) = None backup_heating: str | None = None chiller: str | None = None From e782235efe897c9864b9730f8b7e3c874021b05e Mon Sep 17 00:00:00 2001 From: urwahah Date: Thu, 18 Jun 2026 22:30:01 -0700 Subject: [PATCH 5/8] add disable callback --- pages/equipment_page.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/pages/equipment_page.py b/pages/equipment_page.py index 4fd1017..3de6e41 100644 --- a/pages/equipment_page.py +++ b/pages/equipment_page.py @@ -1162,8 +1162,22 @@ def update_perf_model_on_hp_change(hr_hp_id, awhp_id): return (not hr_selected, not awhp_selected) -# helper to build equipment options for Selects +@callback( + Output("edit-awhp-sizing-priority", "disabled"), + Input("edit-awhp-use-cooling", "checked"), + Input("edit-awhp-sizing-mode", "value"), + prevent_initial_call=True, +) +def update_sizing_priority(use_cooling, sizing_mode): + """Enable/disable AWHP sizing priority input when use-cooling or sizing mode changes.""" + # Disable input if AWHP is not used for cooling or if sizing is not based on peak load + disabled = (use_cooling == False) or (sizing_mode == "fixed_num_units") + + return disabled + + +# helper to build equipment options for Selects def _build_equipment_options( equipment_list, eq_type, unit_mode, include_none=False, none_label="None" From d7a096cb58dfa9488e4cad6bd17855e3d39ab962 Mon Sep 17 00:00:00 2001 From: urwahah Date: Fri, 19 Jun 2026 10:41:24 -0700 Subject: [PATCH 6/8] turn off debug --- app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app.py b/app.py index d3a8561..9bbbd9b 100644 --- a/app.py +++ b/app.py @@ -10,8 +10,8 @@ from utils.logging_config import get_logger, setup_logging # Read level from environment variable (or default to INFO) -log_level_name = os.environ.get("LOG_LEVEL", "DEBUG") -log_level = getattr(logging, log_level_name.upper(), logging.DEBUG) +log_level_name = os.environ.get("LOG_LEVEL", "INFO") +log_level = getattr(logging, log_level_name.upper(), logging.INFO) setup_logging(level=log_level) # Get a logger for this module From 0265975fec2a74e3ccbace238788a71ffb172a67 Mon Sep 17 00:00:00 2001 From: urwahah Date: Fri, 19 Jun 2026 21:10:46 -0700 Subject: [PATCH 7/8] add valueerror --- src/energy.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/energy.py b/src/energy.py index 0fde1c4..f4275cf 100644 --- a/src/energy.py +++ b/src/energy.py @@ -175,6 +175,9 @@ def _equipment_data_validation(library: EquipmentLibrary, scenario_ids: list[str if scen.awhp_use_cooling: awhp_c = library.get_equipment(scen.awhp) + if scen.awhp_sizing_priority is None: + raise ValueError(f"AWHP scenario '{scen.eq_scen_id}' requires 'awhp_sizing_priority'.") + if not awhp_c.performance_cooling: raise ValueError(f"Equipment '{awhp_c.eq_id}' lacks cooling performance data.") From 79f81a24984dccbc58c3927b047a557d1a4049b9 Mon Sep 17 00:00:00 2001 From: urwahah Date: Wed, 24 Jun 2026 09:25:17 -0700 Subject: [PATCH 8/8] remove docs change --- docs/documentation/equipment.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/documentation/equipment.md b/docs/documentation/equipment.md index 58dc2c1..be954df 100644 --- a/docs/documentation/equipment.md +++ b/docs/documentation/equipment.md @@ -66,10 +66,6 @@ These two inputs determine how the total number of AWHP units, not including red * **Fractional sizing (peak load)**: The number of units required to meet the user-specified percentage of annual peak heating load, unrounded. * **Fixed number of units**: User-specified actual number of units. -#### AWHP Sizing Priority - -This input determines which load profile is used to calculate the number of units per the sizing mode and value inputs. - #### AWHP Redundancy An integer number of additional units for redundancy. The default value is 1, i.e., N+1 redundancy. The total number of AWHP units, including redundancy, is used in refrigerant leakage calculations.