From 0fe2e762afdabfafdb70bc00f36d62a393542f4d Mon Sep 17 00:00:00 2001 From: urwahah Date: Wed, 12 Aug 2026 16:18:16 -0700 Subject: [PATCH 1/4] add fuel switching calculation --- app.py | 4 +- src/config.py | 6 ++ src/energy.py | 200 ++++++++++++++++++++++++++++++++++--------------- utils/units.py | 2 + 4 files changed, 151 insertions(+), 61 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 f7d6bda..6becc65 100644 --- a/src/config.py +++ b/src/config.py @@ -106,11 +106,17 @@ class Columns(StrEnum): AWHP_COP_H = "awhp_cop_h" AWHP_HHW_W = "awhp_hhw_W" ELEC_AWHP_H_WH = "elec_awhp_h_Wh" + HHW_REM_W_NG_MODE = "hhw_rem_W_ng_mode" # --- Boiler --- + BOILER_NUM = "boiler_num" BOILER_EFF = "boiler_eff" BOILER_HHW_W = "boiler_hhw_W" + BOILER_EQ_CALC = "boiler_eq_calc_type" + BOILER_CAP_W = "boiler_cap_W" GAS_BOILER_WH = "gas_boiler_Wh" + BOILER_HHW_W_NG_MODE = "boiler_hhw_W_ng_mode" + GAS_BOILER_WH_NG_MODE = "gas_boiler_Wh_ng_mode" # --- Resistance heater backup --- RES_HHW_W = "res_hhw_W" diff --git a/src/energy.py b/src/energy.py index ebc04c9..5946ef6 100644 --- a/src/energy.py +++ b/src/energy.py @@ -176,8 +176,10 @@ def _equipment_data_validation(library: EquipmentLibrary, scenario_ids: list[str 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'.") - + 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.") @@ -437,6 +439,7 @@ def _capacity_constraints( return cap + def _awhp_reference_capacity( e: Equipment, performance: PerformanceCurves, @@ -462,10 +465,8 @@ def _awhp_reference_capacity( 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_capacity_W = e.performance[load_type].leaving_supply_t[ref_supply_temp].capacity_W + cap_ref = interp_vector( e.performance[load_type].t_out_C, ref_capacity_W, @@ -534,10 +535,16 @@ def loads_to_site_energy( Col.AWHP_NUM.value, Col.AWHP_NUM_R.value, Col.ELEC_AWHP_H_WH.value, + Col.HHW_REM_W_NG_MODE.value, # for fuel switching # Boiler + Col.BOILER_NUM.value, Col.BOILER_HHW_W.value, Col.BOILER_EFF.value, + Col.BOILER_EQ_CALC.value, + Col.BOILER_CAP_W.value, Col.GAS_BOILER_WH.value, + Col.BOILER_HHW_W_NG_MODE.value, # for fuel switching + Col.GAS_BOILER_WH_NG_MODE.value, # for fuel switching # Resistance heater Col.RES_HHW_W.value, Col.ELEC_RES_WH.value, @@ -711,30 +718,37 @@ 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") - + cap_ref = _awhp_reference_capacity( + awhp_h, awhp_h_performance, awhp_h_supply_t, "heating" + ) + elif sizing_priority == "cooling": sizing_load = "chw_W" - cap_ref = _awhp_reference_capacity(awhp_c, awhp_c_performance, awhp_c_supply_t, "cooling") - + cap_ref = _awhp_reference_capacity( + awhp_c, awhp_c_performance, awhp_c_supply_t, "cooling" + ) + elif sizing_priority == "larger" 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") + "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"] + "chw_W": float(df["chw_W"].max()) * sizing_value / cap_ref["chw_W"], } - sizing_load = max(num, key = num.get) + 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", @@ -769,6 +783,11 @@ def loads_to_site_energy( 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 + fuel_switching = True + if fuel_switching: + df[Col.HHW_REM_W_NG_MODE.value] = df[Col.HHW_REM_W.value].to_numpy() + # store remaining HHW load before it gets updated (in case there is heat recovery preceding) + # add refrigerant information awhp_refrigerant = awhp_h.refrigerant if awhp_h.refrigerant else "Unknown" num_hours = len(df) # Use actual data length (handles leap years) @@ -823,24 +842,20 @@ def loads_to_site_energy( boiler_served_W = df[Col.HHW_REM_W].to_numpy() gas_Wh = boiler_served_W / eff - boiler_peak_served_W = np.nanmax(boiler_served_W) - # sizing logic - if backup_heating.eq_calc_type == "generic": - # generic equipment - do not account for space, electric capacity, etc. - boiler_num = 0 - else: - # specific equipment model - boiler_cap = backup_heating.capacity_W - boiler_num = np.ceil(boiler_peak_served_W / boiler_cap) - boiler_num = max(boiler_num, 1) # ensure at least one unit - - logger.debug(f"Gas boiler sizing: {boiler_num:.0f} units") + if fuel_switching: + # create intermediate columns to store boiler HHW and gas usage if AWHP did not operate (i.e., NG mode) + df[Col.BOILER_HHW_W_NG_MODE.value] = df[Col.HHW_REM_W_NG_MODE.value] + gas_Wh_NG_mode = df[Col.HHW_REM_W_NG_MODE.value] / eff + df[Col.GAS_BOILER_WH_NG_MODE.value] = gas_Wh_NG_mode df[Col.BOILER_HHW_W.value] = boiler_served_W df[Col.GAS_BOILER_WH.value] = gas_Wh df[Col.BOILER_EFF.value] = eff df[Col.GAS_WH.value] += gas_Wh - df[Col.HHW_REM_W.value] = 0.0 + df[Col.HHW_REM_W.value] -= boiler_served_W + # boiler sizing information + df[Col.BOILER_EQ_CALC.value] = backup_heating.eq_calc_type + df[Col.BOILER_CAP_W.value] = backup_heating.capacity_W boiler_coverage = ( (np.nansum(boiler_served_W) / np.nansum(df[Col.HHW_W.value])) * 100 @@ -901,17 +916,17 @@ def loads_to_site_energy( awhp_turndown = 0.5 num_compressors = awhp_num / awhp_turndown # calculate number of "compressors" used to serve heating load - num_compressors_h = np.maximum(0, - np.ceil( - num_compressors - * df[Col.AWHP_HHW_W.value] - / df[Col.AWHP_CAP_H_W.value] - ) - ) - num_compressors_h[np.isnan(num_compressors_h)] = 0 # for hours where AWHP heating capacity is 0 + num_compressors_h = np.maximum( + 0, np.ceil(num_compressors * df[Col.AWHP_HHW_W.value] / df[Col.AWHP_CAP_H_W.value]) + ) + num_compressors_h[np.isnan(num_compressors_h)] = ( + 0 # for hours where AWHP heating capacity is 0 + ) # remaining compressors can serve cooling load num_compressors_c = np.maximum(0, num_compressors - num_compressors_h) - awhp_num_c = num_compressors_c * awhp_turndown # number of compressors available to operate in cooling + awhp_num_c = ( + num_compressors_c * awhp_turndown + ) # 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) @@ -1066,9 +1081,14 @@ def _finalize_columns(df: pd.DataFrame, detail: bool) -> list[str]: Col.AWHP_REFRIGERANT.value, Col.AWHP_REFRIGERANT_WEIGHT_KG.value, Col.AWHP_REFRIGERANT_GWP.value, + Col.BOILER_NUM.value, Col.BOILER_EFF.value, Col.BOILER_HHW_W.value, + Col.BOILER_HHW_W_NG_MODE.value, + Col.BOILER_EQ_CALC.value, + Col.BOILER_CAP_W.value, Col.GAS_BOILER_WH.value, + Col.GAS_BOILER_WH_NG_MODE.value, Col.RES_HHW_W.value, Col.ELEC_RES_WH.value, Col.AWHP_NUM_C.value, @@ -1183,20 +1203,82 @@ def site_to_source( # used with non-leap emission scenario years. Emissions are still correct # because they're matched by month+hour pattern. + ## fuel switching logic + fuel_switching = True + if fuel_switching: + emissions_intensity_HP = ( + merged[Col.ELEC_EMISSIONS_RATE_G_PER_KWH.value] / merged[Col.AWHP_COP_H.value] + ) + emissions_intensity_NG = gas_emissions_rate / merged[Col.BOILER_EFF.value] + NG_mode = ( + emissions_intensity_HP > emissions_intensity_NG + ) # disable HP if electricity emissions intensity is higher + + # update AWHP and electricity total + merged.loc[NG_mode, Col.ELEC_WH.value] -= merged[Col.ELEC_AWHP_H_WH.value][NG_mode] + merged.loc[NG_mode, Col.ELEC_AWHP_H_WH.value] = 0.0 + merged.loc[NG_mode, Col.AWHP_HHW_W.value] = 0.0 + + # update boiler and gas total + merged.loc[NG_mode, Col.GAS_WH.value] += ( + merged[Col.GAS_BOILER_WH_NG_MODE.value][NG_mode] + - merged[Col.GAS_BOILER_WH.value][NG_mode] + ) + # boilers are the only gas consuming equipment, but this logic included for consistency + merged.loc[NG_mode, Col.BOILER_HHW_W.value] = merged[Col.BOILER_HHW_W_NG_MODE.value][ + NG_mode + ] + merged.loc[NG_mode, Col.GAS_BOILER_WH.value] = merged[Col.GAS_BOILER_WH_NG_MODE.value][ + NG_mode + ] + + ## boiler sizing, after gas has been updated + # determine peak boiler load for each equipment scenario + df_peak_hhw = ( + merged[[Col.EQ_SCEN_ID.value, Col.BOILER_HHW_W.value]] + .groupby([Col.EQ_SCEN_ID.value]) + .max() + .reset_index() + ) + df_peak_hhw = df_peak_hhw.rename(columns={"boiler_hhw_W": "boiler_peak_hhw_W"}) + final_df = merged.merge(df_peak_hhw, on=[Col.EQ_SCEN_ID.value], how="left") + + specific_scen = final_df[Col.BOILER_EQ_CALC.value] == "specific" + final_df.loc[~specific_scen, Col.BOILER_NUM.value] = ( + 0.0 # 0 for generic equipment - do not account for space, electricaL capacity, etc. + ) + final_df.loc[specific_scen, Col.BOILER_NUM.value] = np.maximum( + 1, + np.ceil( + final_df["boiler_peak_hhw_W"][specific_scen] + / final_df[Col.BOILER_CAP_W.value][specific_scen] + ), + ) + final_df = final_df.drop( + [ + Col.BOILER_HHW_W_NG_MODE.value, + Col.GAS_BOILER_WH_NG_MODE.value, + Col.BOILER_EQ_CALC.value, + "boiler_peak_hhw_W", + ], + axis=1, + ) + + ## emissions calculations # electricity emissions - merged[Col.ELEC_EMISSIONS_KG_CO2E.value] = ( - merged[Col.ELEC_WH.value] - * merged[Col.ELEC_EMISSIONS_RATE_G_PER_KWH.value] + final_df[Col.ELEC_EMISSIONS_KG_CO2E.value] = ( + final_df[Col.ELEC_WH.value] + * final_df[Col.ELEC_EMISSIONS_RATE_G_PER_KWH.value] / 1_000_000 #! make cleaner ) # gas emissions - if Col.GAS_WH.value in merged.columns: - merged[Col.GAS_EMISSIONS_KG_CO2E.value] = ( - gas_emissions_rate * merged[Col.GAS_WH.value] / 1_000_000 + if Col.GAS_WH.value in final_df.columns: + final_df[Col.GAS_EMISSIONS_KG_CO2E.value] = ( + gas_emissions_rate * final_df[Col.GAS_WH.value] / 1_000_000 ) else: - merged[Col.GAS_EMISSIONS_KG_CO2E.value] = 0.0 + final_df[Col.GAS_EMISSIONS_KG_CO2E.value] = 0.0 # refrigerant emissions refrig_cols = [ @@ -1205,17 +1287,17 @@ def site_to_source( Col.CHILLER_REFRIGERANT_GWP.value, ] - existing_refrig_cols = [c for c in refrig_cols if c in merged.columns] + existing_refrig_cols = [c for c in refrig_cols if c in final_df.columns] if existing_refrig_cols: # Compute the total refrigerant emissions inventory by summing available columns - merged[Col.TOTAL_REFRIG_GWP_KG.value] = merged[existing_refrig_cols].sum(axis=1) + final_df[Col.TOTAL_REFRIG_GWP_KG.value] = final_df[existing_refrig_cols].sum(axis=1) else: # If none exist, default to zero - merged[Col.TOTAL_REFRIG_GWP_KG.value] = 0.0 + final_df[Col.TOTAL_REFRIG_GWP_KG.value] = 0.0 - merged[Col.TOTAL_REFRIG_EMISSIONS_KG_CO2E.value] = ( - merged[Col.TOTAL_REFRIG_GWP_KG.value] * annual_refrig_leakage_percent + final_df[Col.TOTAL_REFRIG_EMISSIONS_KG_CO2E.value] = ( + final_df[Col.TOTAL_REFRIG_GWP_KG.value] * annual_refrig_leakage_percent ) # DEBUGGING CODE TO IDENTIFY BAD TIMESTAMPS @@ -1230,25 +1312,25 @@ def site_to_source( # print(bad_rows[["year", "month", "day", "hour"]].head()) - merged[Col.TIMESTAMP.value] = pd.to_datetime( - merged[[Col.YEAR.value, Col.MONTH.value, Col.DAY.value, Col.HOUR.value]] + final_df[Col.TIMESTAMP.value] = pd.to_datetime( + final_df[[Col.YEAR.value, Col.MONTH.value, Col.DAY.value, Col.HOUR.value]] ) - merged = merged.drop( + final_df = final_df.drop( columns=[Col.MONTH.value, Col.DAY.value, Col.DOY.value, Col.HOUR.value] ).set_index(Col.TIMESTAMP.value) - merged[Col.TOTAL_EMISSIONS_KG_CO2E.value] = ( - merged[Col.ELEC_EMISSIONS_KG_CO2E.value] - + merged[Col.GAS_EMISSIONS_KG_CO2E.value] - + merged[Col.TOTAL_REFRIG_EMISSIONS_KG_CO2E.value] + final_df[Col.TOTAL_EMISSIONS_KG_CO2E.value] = ( + final_df[Col.ELEC_EMISSIONS_KG_CO2E.value] + + final_df[Col.GAS_EMISSIONS_KG_CO2E.value] + + final_df[Col.TOTAL_REFRIG_EMISSIONS_KG_CO2E.value] ) - merged[Col.EM_SCEN_ID.value] = em_scen_id # tag scenario + final_df[Col.EM_SCEN_ID.value] = em_scen_id # tag scenario - results.append(merged) + results.append(final_df) - total_emissions_kg = sum(r[Col.TOTAL_EMISSIONS_KG_CO2E.value].sum() for r in results) + total_emissions_kg = final_df[Col.TOTAL_EMISSIONS_KG_CO2E.value].sum() logger.info( f"Completed site_to_source for {em_scen_id}, " f"total emissions={total_emissions_kg:.0f} kg CO2e" diff --git a/utils/units.py b/utils/units.py index b30c83e..707945f 100644 --- a/utils/units.py +++ b/utils/units.py @@ -275,6 +275,7 @@ def sqft_to_sqm(sqft): "simult_h_hr_W": ("capacity", "HR Simultaneous Cap"), "awhp_cap_h_W": ("capacity", "AWHP Heating Cap"), "capacity_W": ("capacity", "Rated Capacity"), + "boiler_cap_W": ("capacity", "Boiler Cap"), # === Capacity - Cooling (W) === "awhp_cap_c_W": ("capacity_cooling", "AWHP Cooling Cap"), # === Temperature (°C) === @@ -318,6 +319,7 @@ def sqft_to_sqm(sqft): "awhp_num_c": (None, "AWHP Count (Cooling)"), "awhp_num": (None, "AWHP Count"), "awhp_num_redundant": (None, "AWHP Redundant Count"), + "boiler_num": (None, "Boiler Count"), # === Refrigerant Type (text - no conversion) === "chiller_refrigerant": (None, "Chiller Refrigerant"), "hr_wwhp_refrigerant": (None, "HR-WWHP Refrigerant"), From a21248b8da86929c52f91d8c0e23223fd13a82f8 Mon Sep 17 00:00:00 2001 From: urwahah Date: Wed, 12 Aug 2026 18:02:28 -0700 Subject: [PATCH 2/4] add new input, equip scenario --- data/input/equipment_data.JSON | 38 +++++++++++++++++ layout/input.py | 10 ++++- layout/output.py | 3 +- pages/equipment_page.py | 41 ++++++++++++++++++- src/config.py | 2 + src/energy.py | 75 +++++++++++++++++++++------------- src/equipment.py | 17 +++++--- 7 files changed, 147 insertions(+), 39 deletions(-) diff --git a/data/input/equipment_data.JSON b/data/input/equipment_data.JSON index 52e803d..72ab06c 100644 --- a/data/input/equipment_data.JSON +++ b/data/input/equipment_data.JSON @@ -1009,6 +1009,7 @@ "awhp_use_cooling": false, "awhp_sizing_priority": "heating", "backup_heating": "bo03", + "fuel_switching": false, "chiller": "ch02" }, { @@ -1026,6 +1027,7 @@ "awhp_use_cooling": false, "awhp_sizing_priority": "heating", "backup_heating": "res02", + "fuel_switching": false, "chiller": "ch02" }, { @@ -1043,6 +1045,7 @@ "awhp_use_cooling": true, "awhp_sizing_priority": "heating", "backup_heating": "bo03", + "fuel_switching": false, "chiller": "ch02" }, { @@ -1060,6 +1063,7 @@ "awhp_use_cooling": true, "awhp_sizing_priority": "heating", "backup_heating": "res02", + "fuel_switching": false, "chiller": "ch02" }, { @@ -1077,6 +1081,7 @@ "awhp_use_cooling": true, "awhp_sizing_priority": "heating", "backup_heating": "res02", + "fuel_switching": false, "chiller": "ch02" }, { @@ -1094,6 +1099,7 @@ "awhp_use_cooling": true, "awhp_sizing_priority": "heating", "backup_heating": "bo03", + "fuel_switching": false, "chiller": "ch02" }, { @@ -1111,6 +1117,7 @@ "awhp_use_cooling": true, "awhp_sizing_priority": "heating", "backup_heating": "bo03", + "fuel_switching": false, "chiller": "ch02" }, { @@ -1128,6 +1135,7 @@ "awhp_use_cooling": true, "awhp_sizing_priority": "heating", "backup_heating": "bo03", + "fuel_switching": false, "chiller": "ch02" }, { @@ -1145,6 +1153,7 @@ "awhp_use_cooling": true, "awhp_sizing_priority": "heating", "backup_heating": "bo03", + "fuel_switching": false, "chiller": "ch02" }, { @@ -1162,6 +1171,7 @@ "awhp_use_cooling": false, "awhp_sizing_priority": "heating", "backup_heating": "bo03", + "fuel_switching": false, "chiller": "ch02" }, { @@ -1179,6 +1189,7 @@ "awhp_use_cooling": false, "awhp_sizing_priority": "heating", "backup_heating": "res02", + "fuel_switching": false, "chiller": "ch02" }, { @@ -1196,6 +1207,7 @@ "awhp_use_cooling": true, "awhp_sizing_priority": "heating", "backup_heating": "res02", + "fuel_switching": false, "chiller": "ch02" }, { @@ -1213,6 +1225,7 @@ "awhp_use_cooling": true, "awhp_sizing_priority": "heating", "backup_heating": "res02", + "fuel_switching": false, "chiller": "ch02" }, { @@ -1230,6 +1243,7 @@ "awhp_use_cooling": true, "awhp_sizing_priority": "heating", "backup_heating": "res02", + "fuel_switching": false, "chiller": "ch02" }, { @@ -1247,6 +1261,25 @@ "awhp_use_cooling": true, "awhp_sizing_priority": "heating", "backup_heating": "res02", + "fuel_switching": false, + "chiller": "ch02" + }, + { + "eq_scen_id": "eq_scenario_16", + "eq_scen_name": "100% (Frac) AWHP (H+C), Fuel Switching", + "hr_wwhp": null, + "hr_wwhp_performance_model": "interpolate_HHWST", + "hr_wwhp_h_supply_t": 48.9, + "awhp": "hp01", + "awhp_performance_model": "interpolate_HHWST_fixed", + "awhp_sizing_mode": "fractional_sizing_peak_load", + "awhp_sizing_value": 1, + "awhp_redundancy": 1, + "awhp_h_supply_t": 38, + "awhp_use_cooling": true, + "awhp_sizing_priority": "heating", + "backup_heating": "bo03", + "fuel_switching": true, "chiller": "ch02" } ], @@ -1270,6 +1303,11 @@ "group_id": "hhw_supply_temps", "group_name": "HHW Supply Temps", "scenario_ids": ["eq_scenario_4", "eq_scenario_13", "eq_scenario_14", "eq_scenario_5", "eq_scenario_15"] + }, + { + "group_id": "fuel_switching", + "group_name": "Dynamic Fuel Switching", + "scenario_ids": ["eq_scenario_9", "eq_scenario_16"] } ] } \ No newline at end of file diff --git a/layout/input.py b/layout/input.py index b2f58b2..78861e9 100644 --- a/layout/input.py +++ b/layout/input.py @@ -542,9 +542,10 @@ def build_equipment_table( ("awhp_sizing_mode", "AWHP Sizing Mode"), ("awhp_sizing_value", "AWHP Sizing Value"), ("awhp_redundancy", "AWHP Redundancy"), - ("awhp_use_cooling", "AWHP Use Cooling"), + ("awhp_use_cooling", "Use AWHP for Cooling"), ("awhp_sizing_priority", "AWHP Sizing Priority"), ("backup_heating", "Backup Heating"), + ("fuel_switching", "Use Optimal Heating Fuel"), ("chiller", "Backup Cooling"), ] @@ -998,7 +999,7 @@ def edit_equipment_modal(): [ dmc.Switch( id="edit-awhp-use-cooling", - label="Use heat pump also for cooling", + label="Use AWHP for cooling", mt="xs", ), dmc.Select( @@ -1050,6 +1051,11 @@ def edit_equipment_modal(): ), ], ), + dmc.Switch( + id="edit-fuel-switching", + label="Use optimal heating fuel", + mt="xs", + ), dmc.Text( id="edit-scenario-error", size="xs", diff --git a/layout/output.py b/layout/output.py index dd80415..d4b17c1 100644 --- a/layout/output.py +++ b/layout/output.py @@ -162,9 +162,10 @@ def get_value(self, path: str): ("awhp_sizing_mode", "AWHP Sizing Mode"), ("awhp_sizing_value", "AWHP Sizing Value"), ("awhp_redundancy", "AWHP Redundancy"), - ("awhp_use_cooling", "AWHP Use Cooling"), + ("awhp_use_cooling", "Use AWHP for Cooling"), ("awhp_sizing_priority", "AWHP Sizing Priority"), ("backup_heating", "Backup Heating"), + ("fuel_switching", "Use Optimal Heating Fuel"), ("chiller", "Chiller"), ] diff --git a/pages/equipment_page.py b/pages/equipment_page.py index 954513c..1b7d25d 100644 --- a/pages/equipment_page.py +++ b/pages/equipment_page.py @@ -684,6 +684,7 @@ def reset_equipment(n_clicks, initial_data): Output("edit-awhp-sizing-priority", "value"), Output("edit-backup-heating-select", "data"), Output("edit-backup-heating-select", "value"), + Output("edit-fuel-switching", "checked"), Output("edit-chiller-select", "data"), Output("edit-chiller-select", "value"), Output("edit-scenario-error", "children"), @@ -698,7 +699,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,) * 21 + return (no_update,) * 22 if not equipment_data: return ( @@ -720,6 +721,7 @@ def open_edit_modal(edit_clicks, equipment_data, unit_mode): None, [], None, + False, [], None, "No equipment data.", @@ -730,7 +732,7 @@ def open_edit_modal(edit_clicks, equipment_data, unit_mode): triggered = callback_context.triggered if not triggered: - return (no_update,) * 21 + return (no_update,) * 22 prop_id = triggered[0]["prop_id"] id_str = prop_id.split(".")[0] @@ -757,6 +759,7 @@ def open_edit_modal(edit_clicks, equipment_data, unit_mode): None, [], None, + False, [], None, "Failed to parse button id.", @@ -787,6 +790,7 @@ def open_edit_modal(edit_clicks, equipment_data, unit_mode): None, [], None, + False, [], None, f"Scenario {eq_scen_id!r} not found.", @@ -849,6 +853,7 @@ def open_edit_modal(edit_clicks, equipment_data, unit_mode): redundancy = scenario.get("awhp_redundancy", 1) use_cooling = scenario.get("awhp_use_cooling", False) sizing_priority = scenario.get("awhp_sizing_priority") or "heating" + fuel_switching = scenario.get("fuel_switching", False) backup_heating_val = scenario.get("backup_heating") chiller_val = scenario.get("chiller") @@ -872,6 +877,7 @@ def open_edit_modal(edit_clicks, equipment_data, unit_mode): sizing_priority, backup_heating_options, backup_heating_val, + fuel_switching, chiller_options, chiller_val, "", @@ -897,6 +903,7 @@ def open_edit_modal(edit_clicks, equipment_data, unit_mode): State("edit-awhp-use-cooling", "checked"), State("edit-awhp-sizing-priority", "value"), State("edit-backup-heating-select", "value"), + State("edit-fuel-switching", "checked"), State("edit-chiller-select", "value"), State("equipment-store", "data"), State("unit-toggle", "value"), @@ -918,6 +925,7 @@ def save_edit_scenario( use_cooling, sizing_priority, backup_heating_val, + fuel_switching, chiller_val, equipment_data, unit_mode, @@ -969,6 +977,7 @@ def save_edit_scenario( redundancy = 1 use_cooling = bool(use_cooling) + fuel_switching = bool(fuel_switching) scenarios = equipment_data["equipment_scenarios"] updated = False @@ -990,6 +999,7 @@ def save_edit_scenario( new_scen["awhp_use_cooling"] = use_cooling new_scen["awhp_sizing_priority"] = sizing_priority new_scen["backup_heating"] = backup_heating_val + new_scen["fuel_switching"] = fuel_switching new_scen["chiller"] = chiller_val new_scenarios.append(new_scen) updated = True @@ -1177,8 +1187,35 @@ def update_sizing_priority(use_cooling, sizing_mode): return disabled +@callback( + Output("edit-fuel-switching", "disabled"), + Output("edit-fuel-switching", "checked", allow_duplicate=True), + Input("edit-awhp-select", "value"), + Input("edit-backup-heating-select", "value"), + State("edit-fuel-switching", "checked"), + State("equipment-store", "data"), + prevent_initial_call=True, +) +def update_fuel_switching(awhp_id, backup_heating_id, fuel_switching, equipment_data): + """Enable/disable fuel switching input and update value when AWHP or backup heating selection changes.""" + # get backup heating fuel + equipment_list = equipment_data.get("equipment", []) if equipment_data else [] + backup_heating = next((e for e in equipment_list if e.get("eq_id") == backup_heating_id), None) + backup_heating_fuel = backup_heating.get("fuel", "") + + # Disable input if AWHP is not selected, or if backup heating option is electric + awhp_selected = awhp_id and awhp_id != "None" + disabled = (not awhp_selected) or (backup_heating_fuel != "natural_gas") + + # Set value to unchecked if disabled condition is met, otherwise preserve last value + checked = False if disabled else fuel_switching + + return (disabled, checked) + + # helper to build equipment options for Selects + def _build_equipment_options( equipment_list, eq_type, unit_mode, include_none=False, none_label="None" ): diff --git a/src/config.py b/src/config.py index 6becc65..4c3338b 100644 --- a/src/config.py +++ b/src/config.py @@ -36,6 +36,7 @@ class EquipmentTableRows(Enum): "awhp_use_cooling", "awhp_sizing_priority", "backup_heating", + "fuel_switching", "chiller", ) @@ -117,6 +118,7 @@ class Columns(StrEnum): GAS_BOILER_WH = "gas_boiler_Wh" BOILER_HHW_W_NG_MODE = "boiler_hhw_W_ng_mode" GAS_BOILER_WH_NG_MODE = "gas_boiler_Wh_ng_mode" + FUEL_SWITCHING = "fuel_switching" # --- Resistance heater backup --- RES_HHW_W = "res_hhw_W" diff --git a/src/energy.py b/src/energy.py index 5946ef6..b33e7e5 100644 --- a/src/energy.py +++ b/src/energy.py @@ -535,7 +535,7 @@ def loads_to_site_energy( Col.AWHP_NUM.value, Col.AWHP_NUM_R.value, Col.ELEC_AWHP_H_WH.value, - Col.HHW_REM_W_NG_MODE.value, # for fuel switching + Col.HHW_REM_W_NG_MODE.value, # Boiler Col.BOILER_NUM.value, Col.BOILER_HHW_W.value, @@ -543,8 +543,9 @@ def loads_to_site_energy( Col.BOILER_EQ_CALC.value, Col.BOILER_CAP_W.value, Col.GAS_BOILER_WH.value, - Col.BOILER_HHW_W_NG_MODE.value, # for fuel switching - Col.GAS_BOILER_WH_NG_MODE.value, # for fuel switching + Col.BOILER_HHW_W_NG_MODE.value, + Col.GAS_BOILER_WH_NG_MODE.value, + Col.FUEL_SWITCHING.value, # Resistance heater Col.RES_HHW_W.value, Col.ELEC_RES_WH.value, @@ -783,10 +784,12 @@ def loads_to_site_energy( 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 - fuel_switching = True - if fuel_switching: + if scen.fuel_switching: + df[Col.FUEL_SWITCHING.value] = True df[Col.HHW_REM_W_NG_MODE.value] = df[Col.HHW_REM_W.value].to_numpy() # store remaining HHW load before it gets updated (in case there is heat recovery preceding) + else: + df[Col.FUEL_SWITCHING.value] = False # add refrigerant information awhp_refrigerant = awhp_h.refrigerant if awhp_h.refrigerant else "Unknown" @@ -842,7 +845,7 @@ def loads_to_site_energy( boiler_served_W = df[Col.HHW_REM_W].to_numpy() gas_Wh = boiler_served_W / eff - if fuel_switching: + if scen.fuel_switching: # create intermediate columns to store boiler HHW and gas usage if AWHP did not operate (i.e., NG mode) df[Col.BOILER_HHW_W_NG_MODE.value] = df[Col.HHW_REM_W_NG_MODE.value] gas_Wh_NG_mode = df[Col.HHW_REM_W_NG_MODE.value] / eff @@ -1089,6 +1092,7 @@ def _finalize_columns(df: pd.DataFrame, detail: bool) -> list[str]: Col.BOILER_CAP_W.value, Col.GAS_BOILER_WH.value, Col.GAS_BOILER_WH_NG_MODE.value, + Col.FUEL_SWITCHING.value, Col.RES_HHW_W.value, Col.ELEC_RES_WH.value, Col.AWHP_NUM_C.value, @@ -1204,33 +1208,45 @@ def site_to_source( # because they're matched by month+hour pattern. ## fuel switching logic - fuel_switching = True - if fuel_switching: - emissions_intensity_HP = ( - merged[Col.ELEC_EMISSIONS_RATE_G_PER_KWH.value] / merged[Col.AWHP_COP_H.value] - ) - emissions_intensity_NG = gas_emissions_rate / merged[Col.BOILER_EFF.value] - NG_mode = ( + emissions_intensity_HP = ( + merged[Col.ELEC_EMISSIONS_RATE_G_PER_KWH.value] / merged[Col.AWHP_COP_H.value] + ) + emissions_intensity_NG = gas_emissions_rate / merged[Col.BOILER_EFF.value] + NG_mode = ( + merged[Col.FUEL_SWITCHING.value].to_numpy() # only evaluate if fuel switching is true + & ( emissions_intensity_HP > emissions_intensity_NG ) # disable HP if electricity emissions intensity is higher + ) - # update AWHP and electricity total - merged.loc[NG_mode, Col.ELEC_WH.value] -= merged[Col.ELEC_AWHP_H_WH.value][NG_mode] - merged.loc[NG_mode, Col.ELEC_AWHP_H_WH.value] = 0.0 - merged.loc[NG_mode, Col.AWHP_HHW_W.value] = 0.0 + # print number of fuel switching hours + df_fuel_switching = merged + df_fuel_switching["NG_mode"] = NG_mode + df_fuel_switching = ( + df_fuel_switching[[Col.EQ_SCEN_ID.value, Col.EQ_SCEN_NAME.value, "NG_mode"]] + .groupby([Col.EQ_SCEN_ID.value, Col.EQ_SCEN_NAME.value]) + .agg(NG_mode_hours=("NG_mode", "sum"), total_hours=("NG_mode", "size")) + .reset_index() + ) + logger.debug(f"Fuel switching operation:\n {df_fuel_switching}") - # update boiler and gas total - merged.loc[NG_mode, Col.GAS_WH.value] += ( - merged[Col.GAS_BOILER_WH_NG_MODE.value][NG_mode] - - merged[Col.GAS_BOILER_WH.value][NG_mode] - ) - # boilers are the only gas consuming equipment, but this logic included for consistency - merged.loc[NG_mode, Col.BOILER_HHW_W.value] = merged[Col.BOILER_HHW_W_NG_MODE.value][ - NG_mode - ] - merged.loc[NG_mode, Col.GAS_BOILER_WH.value] = merged[Col.GAS_BOILER_WH_NG_MODE.value][ - NG_mode - ] + # update AWHP and electricity total + merged.loc[NG_mode, Col.ELEC_WH.value] -= merged[Col.ELEC_AWHP_H_WH.value][NG_mode] + merged.loc[NG_mode, Col.ELEC_AWHP_H_WH.value] = 0.0 + merged.loc[NG_mode, Col.AWHP_HHW_W.value] = 0.0 + + # update boiler and gas total + merged.loc[NG_mode, Col.GAS_WH.value] += ( + merged[Col.GAS_BOILER_WH_NG_MODE.value][NG_mode] + - merged[Col.GAS_BOILER_WH.value][NG_mode] + ) + # boilers are the only gas consuming equipment, but this logic included for consistency + merged.loc[NG_mode, Col.BOILER_HHW_W.value] = merged[Col.BOILER_HHW_W_NG_MODE.value][ + NG_mode + ] + merged.loc[NG_mode, Col.GAS_BOILER_WH.value] = merged[Col.GAS_BOILER_WH_NG_MODE.value][ + NG_mode + ] ## boiler sizing, after gas has been updated # determine peak boiler load for each equipment scenario @@ -1259,6 +1275,7 @@ def site_to_source( Col.BOILER_HHW_W_NG_MODE.value, Col.GAS_BOILER_WH_NG_MODE.value, Col.BOILER_EQ_CALC.value, + Col.FUEL_SWITCHING.value, "boiler_peak_hhw_W", ], axis=1, diff --git a/src/equipment.py b/src/equipment.py index c390273..822660a 100644 --- a/src/equipment.py +++ b/src/equipment.py @@ -11,6 +11,7 @@ # --- Models --- class PerformanceCurves(BaseModel): """Equipment performance curves: coefficient of performance (COP), capacity, and outdoor air temperature constraints.""" + cop: list[float] | None = None capacity_W: list[float] | None = None constraints: dict[str, float] | None = None @@ -21,7 +22,8 @@ class Performance(BaseModel): Leaving supply water temperatures, associated performance curves, and supply water temperature constraints. Outdoor air temperature curve for AWHPs, capacity curve for WWHPs, constant efficiency for boilers/chillers. """ - t_out_C: list[float] | None = None + + t_out_C: list[float] | None = None capacity_W: list[float] | None = None leaving_supply_t: dict[str, PerformanceCurves] | None = None efficiency: float | None = None @@ -29,21 +31,27 @@ class Performance(BaseModel): class Emissions(BaseModel): - """"Equipment emissions data.""" + """ "Equipment emissions data.""" + co2_kg_per_mwh: float + class Dimensions(BaseModel): """Equipment physical dimensions in metres.""" + length: float | None = None height: float | None = None width: float | None = None + class Electrical(BaseModel): """Equipment electrical characteristics: minimum circuit amperage (MCA), voltage, and phase.""" + mca: float | None = None voltage: float | None = None phase: int | None = None + class Equipment(BaseModel): eq_id: str eq_type: str @@ -95,10 +103,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 + awhp_sizing_priority: Literal["heating", "cooling", "larger"] | None = None backup_heating: str | None = None + fuel_switching: bool chiller: str | None = None From 466e65e9c5b0658ba927b31c5d9191d59369f9cd Mon Sep 17 00:00:00 2001 From: urwahah Date: Mon, 17 Aug 2026 18:00:16 -0700 Subject: [PATCH 3/4] update scen, add boiler, fix awhp sizing --- app.py | 4 +-- data/input/equipment_data.JSON | 65 ++++++++++++++++++++++++++++------ src/energy.py | 53 ++++++++++++++++++--------- 3 files changed, 92 insertions(+), 30 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 diff --git a/data/input/equipment_data.JSON b/data/input/equipment_data.JSON index 72ab06c..f0cf068 100644 --- a/data/input/equipment_data.JSON +++ b/data/input/equipment_data.JSON @@ -817,6 +817,31 @@ } } }, + { + "eq_id": "bo05", + "eq_type": "backup_heating", + "eq_subtype": "hot_water", + "eq_calc_type": "specific", + "model": "GenericBoiler", + "fuel": "natural_gas", + "capacity_W": 190500, + "dimensions": { + "length": 1.06, + "width": 0.833, + "height": 2.01 + }, + "operating_weight_g": 305000, + "electrical": { + "mca": 10, + "voltage": 120, + "phase": 1 + }, + "performance": { + "heating": { + "efficiency": 0.6 + } + } + }, { "eq_id": "ch01", "eq_type": "chiller", @@ -1248,37 +1273,55 @@ }, { "eq_scen_id": "eq_scenario_15", - "eq_scen_name": "High HHWST HR WWHP+100% AWHP (H+C)+Elec Backup", - "hr_wwhp": "hr03", + "eq_scen_name": "Gas Boiler+AC Chiller", + "hr_wwhp": null, "hr_wwhp_performance_model": "interpolate_HHWST", - "hr_wwhp_h_supply_t": 60, - "awhp": "hp01", + "hr_wwhp_h_supply_t": 48.9, + "awhp": null, "awhp_performance_model": "interpolate_HHWST_fixed", "awhp_sizing_mode": "integer_sizing_peak_load", "awhp_sizing_value": 1, "awhp_redundancy": 1, - "awhp_h_supply_t": 52, - "awhp_use_cooling": true, + "awhp_h_supply_t": 38, + "awhp_use_cooling": false, "awhp_sizing_priority": "heating", - "backup_heating": "res02", + "backup_heating": "bo02", "fuel_switching": false, "chiller": "ch02" }, { "eq_scen_id": "eq_scenario_16", - "eq_scen_name": "100% (Frac) AWHP (H+C), Fuel Switching", + "eq_scen_name": "50% AWHP (H+C)+Gas Backup", "hr_wwhp": null, "hr_wwhp_performance_model": "interpolate_HHWST", "hr_wwhp_h_supply_t": 48.9, "awhp": "hp01", "awhp_performance_model": "interpolate_HHWST_fixed", - "awhp_sizing_mode": "fractional_sizing_peak_load", + "awhp_sizing_mode": "integer_sizing_peak_load", + "awhp_sizing_value": 0.5, + "awhp_redundancy": 1, + "awhp_h_supply_t": 38, + "awhp_use_cooling": true, + "awhp_sizing_priority": "heating", + "backup_heating": "bo02", + "fuel_switching": false, + "chiller": "ch02" + }, + { + "eq_scen_id": "eq_scenario_17", + "eq_scen_name": "100% AWHP (H+C)+Fuel Switching", + "hr_wwhp": null, + "hr_wwhp_performance_model": "interpolate_HHWST", + "hr_wwhp_h_supply_t": 48.9, + "awhp": "hp01", + "awhp_performance_model": "interpolate_HHWST_fixed", + "awhp_sizing_mode": "integer_sizing_peak_load", "awhp_sizing_value": 1, "awhp_redundancy": 1, "awhp_h_supply_t": 38, "awhp_use_cooling": true, "awhp_sizing_priority": "heating", - "backup_heating": "bo03", + "backup_heating": "bo02", "fuel_switching": true, "chiller": "ch02" } @@ -1307,7 +1350,7 @@ { "group_id": "fuel_switching", "group_name": "Dynamic Fuel Switching", - "scenario_ids": ["eq_scenario_9", "eq_scenario_16"] + "scenario_ids": ["eq_scenario_15", "eq_scenario_16", "eq_scenario_4", "eq_scenario_17"] } ] } \ No newline at end of file diff --git a/src/energy.py b/src/energy.py index b33e7e5..a46f314 100644 --- a/src/energy.py +++ b/src/energy.py @@ -565,6 +565,11 @@ def loads_to_site_energy( # ---- scenario ---- scen = library.get_scenario(scenario_id) + if scen.fuel_switching: + df[Col.FUEL_SWITCHING.value] = True + else: + df[Col.FUEL_SWITCHING.value] = False + # ========================= # Phase 1 - HR WWHP (optional) # ========================= @@ -718,13 +723,13 @@ def loads_to_site_energy( # Determine reference capacity if sizing_priority == "heating": - sizing_load = "hhw_W" + sizing_load = Col.HHW_REM_W.value cap_ref = _awhp_reference_capacity( awhp_h, awhp_h_performance, awhp_h_supply_t, "heating" ) elif sizing_priority == "cooling": - sizing_load = "chw_W" + sizing_load = Col.CHW_REM_W.value cap_ref = _awhp_reference_capacity( awhp_c, awhp_c_performance, awhp_c_supply_t, "cooling" ) @@ -734,16 +739,20 @@ def loads_to_site_energy( "fractional_sizing_peak_load", ]: cap_ref = { - "hhw_W": _awhp_reference_capacity( + Col.HHW_REM_W.value: _awhp_reference_capacity( awhp_h, awhp_h_performance, awhp_h_supply_t, "heating" ), - "chw_W": _awhp_reference_capacity( + Col.CHW_REM_W.value: _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"], + Col.HHW_REM_W.value: float(df[Col.HHW_REM_W.value].max()) + * sizing_value + / cap_ref[Col.HHW_REM_W.value], + Col.CHW_REM_W.value: float(df[Col.CHW_REM_W.value].max()) + * sizing_value + / cap_ref[Col.CHW_REM_W.value], } sizing_load = max(num, key=num.get) cap_ref = cap_ref[sizing_load] @@ -785,11 +794,8 @@ def loads_to_site_energy( elec_h_Wh = served_h_W / awhp_cop_h if scen.fuel_switching: - df[Col.FUEL_SWITCHING.value] = True df[Col.HHW_REM_W_NG_MODE.value] = df[Col.HHW_REM_W.value].to_numpy() # store remaining HHW load before it gets updated (in case there is heat recovery preceding) - else: - df[Col.FUEL_SWITCHING.value] = False # add refrigerant information awhp_refrigerant = awhp_h.refrigerant if awhp_h.refrigerant else "Unknown" @@ -860,12 +866,22 @@ def loads_to_site_energy( df[Col.BOILER_EQ_CALC.value] = backup_heating.eq_calc_type df[Col.BOILER_CAP_W.value] = backup_heating.capacity_W - boiler_coverage = ( - (np.nansum(boiler_served_W) / np.nansum(df[Col.HHW_W.value])) * 100 - if np.nansum(df[Col.HHW_W.value]) > 0 - else 0 - ) - logger.debug(f"Phase 3 complete: Boiler covers {boiler_coverage:.1f}% of heating load") + if scen.fuel_switching: + logger.debug("Phase 3 complete") + # boiler coverage calculation would be incorrect for fuel switching so skipped here + else: + boiler_coverage = ( + ( + np.nansum(np.nansum(df[Col.BOILER_HHW_W.value])) + / np.nansum(df[Col.HHW_W.value]) + ) + * 100 + if np.nansum(df[Col.HHW_W.value]) > 0 + else 0 + ) + logger.debug( + f"Phase 3 complete: Boiler covers {boiler_coverage:.1f}% of heating load" + ) # ========================= # Phase 4 - Electric resistance (if heating remains) @@ -1223,8 +1239,10 @@ def site_to_source( df_fuel_switching = merged df_fuel_switching["NG_mode"] = NG_mode df_fuel_switching = ( - df_fuel_switching[[Col.EQ_SCEN_ID.value, Col.EQ_SCEN_NAME.value, "NG_mode"]] - .groupby([Col.EQ_SCEN_ID.value, Col.EQ_SCEN_NAME.value]) + df_fuel_switching[ + [Col.EQ_SCEN_ID.value, Col.EQ_SCEN_NAME.value, Col.FUEL_SWITCHING.value, "NG_mode"] + ] + .groupby([Col.EQ_SCEN_ID.value, Col.EQ_SCEN_NAME.value, Col.FUEL_SWITCHING.value]) .agg(NG_mode_hours=("NG_mode", "sum"), total_hours=("NG_mode", "size")) .reset_index() ) @@ -1277,6 +1295,7 @@ def site_to_source( Col.BOILER_EQ_CALC.value, Col.FUEL_SWITCHING.value, "boiler_peak_hhw_W", + "NG_mode", ], axis=1, ) From 89b43561bb6eb695ca5267826e27f5cd5c129041 Mon Sep 17 00:00:00 2001 From: urwahah Date: Wed, 2 Sep 2026 18:18:00 -0700 Subject: [PATCH 4/4] address review comments --- data/input/equipment_data.JSON | 25 +++++++++++++++++++++---- pages/equipment_page.py | 2 +- src/energy.py | 3 +-- src/equipment.py | 2 +- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/data/input/equipment_data.JSON b/data/input/equipment_data.JSON index f0cf068..e5ee3f2 100644 --- a/data/input/equipment_data.JSON +++ b/data/input/equipment_data.JSON @@ -1273,7 +1273,24 @@ }, { "eq_scen_id": "eq_scenario_15", - "eq_scen_name": "Gas Boiler+AC Chiller", + "eq_scen_name": "High HHWST HR WWHP+100% AWHP (H+C)+Elec Backup", + "hr_wwhp": "hr03", + "hr_wwhp_performance_model": "interpolate_HHWST", + "hr_wwhp_h_supply_t": 60, + "awhp": "hp01", + "awhp_performance_model": "interpolate_HHWST_fixed", + "awhp_sizing_mode": "integer_sizing_peak_load", + "awhp_sizing_value": 1, + "awhp_redundancy": 1, + "awhp_h_supply_t": 52, + "awhp_use_cooling": true, + "awhp_sizing_priority": "heating", + "backup_heating": "res02", + "chiller": "ch02" + }, + { + "eq_scen_id": "eq_scenario_16", + "eq_scen_name": "90% Eff. Gas Boiler+AC Chiller", "hr_wwhp": null, "hr_wwhp_performance_model": "interpolate_HHWST", "hr_wwhp_h_supply_t": 48.9, @@ -1290,7 +1307,7 @@ "chiller": "ch02" }, { - "eq_scen_id": "eq_scenario_16", + "eq_scen_id": "eq_scenario_17", "eq_scen_name": "50% AWHP (H+C)+Gas Backup", "hr_wwhp": null, "hr_wwhp_performance_model": "interpolate_HHWST", @@ -1308,7 +1325,7 @@ "chiller": "ch02" }, { - "eq_scen_id": "eq_scenario_17", + "eq_scen_id": "eq_scenario_18", "eq_scen_name": "100% AWHP (H+C)+Fuel Switching", "hr_wwhp": null, "hr_wwhp_performance_model": "interpolate_HHWST", @@ -1350,7 +1367,7 @@ { "group_id": "fuel_switching", "group_name": "Dynamic Fuel Switching", - "scenario_ids": ["eq_scenario_15", "eq_scenario_16", "eq_scenario_4", "eq_scenario_17"] + "scenario_ids": ["eq_scenario_16", "eq_scenario_17", "eq_scenario_4", "eq_scenario_18"] } ] } \ No newline at end of file diff --git a/pages/equipment_page.py b/pages/equipment_page.py index 1b7d25d..b206e7e 100644 --- a/pages/equipment_page.py +++ b/pages/equipment_page.py @@ -1201,7 +1201,7 @@ def update_fuel_switching(awhp_id, backup_heating_id, fuel_switching, equipment_ # get backup heating fuel equipment_list = equipment_data.get("equipment", []) if equipment_data else [] backup_heating = next((e for e in equipment_list if e.get("eq_id") == backup_heating_id), None) - backup_heating_fuel = backup_heating.get("fuel", "") + backup_heating_fuel = backup_heating.get("fuel", "") if backup_heating else "" # Disable input if AWHP is not selected, or if backup heating option is electric awhp_selected = awhp_id and awhp_id != "None" diff --git a/src/energy.py b/src/energy.py index a46f314..e41c0d0 100644 --- a/src/energy.py +++ b/src/energy.py @@ -1236,7 +1236,7 @@ def site_to_source( ) # print number of fuel switching hours - df_fuel_switching = merged + df_fuel_switching = merged.copy() df_fuel_switching["NG_mode"] = NG_mode df_fuel_switching = ( df_fuel_switching[ @@ -1295,7 +1295,6 @@ def site_to_source( Col.BOILER_EQ_CALC.value, Col.FUEL_SWITCHING.value, "boiler_peak_hhw_W", - "NG_mode", ], axis=1, ) diff --git a/src/equipment.py b/src/equipment.py index 822660a..0a4e630 100644 --- a/src/equipment.py +++ b/src/equipment.py @@ -105,7 +105,7 @@ class EquipmentScenario(DotAccessMixin, BaseModel): awhp_use_cooling: bool awhp_sizing_priority: Literal["heating", "cooling", "larger"] | None = None backup_heating: str | None = None - fuel_switching: bool + fuel_switching: bool = False chiller: str | None = None