From 8a55ed41a198173e015ebdf3e38ad5d0ec0d859e Mon Sep 17 00:00:00 2001 From: urwahah Date: Fri, 3 Apr 2026 12:15:25 -0700 Subject: [PATCH 1/2] add backup heating/cooling units calc, update equip selection names --- data/input/equipment_data.JSON | 128 ++++++++++++++++++++++----------- pages/equipment_page.py | 38 +++++++--- src/energy.py | 49 ++++++++++++- src/equipment.py | 1 + 4 files changed, 163 insertions(+), 53 deletions(-) diff --git a/data/input/equipment_data.JSON b/data/input/equipment_data.JSON index 464e3b1..7b52105 100644 --- a/data/input/equipment_data.JSON +++ b/data/input/equipment_data.JSON @@ -4,6 +4,7 @@ "eq_id": "hr01", "eq_type": "hr_heat_pump", "eq_subtype": "water_to_water", + "eq_calc_type": "specific", "model": "GenericHR_WWHP", "nominal_tons": 300, "fuel": "electricity", @@ -32,7 +33,9 @@ "eq_id": "hr02", "eq_type": "hr_heat_pump", "eq_subtype": "water_to_water_scroll", - "model": "GenericIMC", + "eq_calc_type": "specific", + "eq_manufacturer": "IMC", + "model": "GenericHR_WWHP", "nominal_tons": 30, "fuel": "electricity", "refrigerant": "R-454B", @@ -60,7 +63,9 @@ "eq_id": "hr03", "eq_type": "hr_heat_pump", "eq_subtype": "water_to_water_scroll", - "model": "GenericIMC", + "eq_calc_type": "specific", + "eq_manufacturer": "IMC", + "model": "GenericHR_WWHP", "nominal_tons": 60, "fuel": "electricity", "refrigerant": "R-454B", @@ -88,7 +93,9 @@ "eq_id": "hr04", "eq_type": "hr_heat_pump", "eq_subtype": "water_to_water_scroll", - "model": "GenericIMC", + "eq_calc_type": "specific", + "eq_manufacturer": "IMC", + "model": "GenericHR_WWHP", "nominal_tons": 250, "fuel": "electricity", "refrigerant": "R-515B", @@ -116,7 +123,9 @@ "eq_id": "hr05", "eq_type": "hr_heat_pump", "eq_subtype": "water_to_water_scroll", - "model": "GenericIMC", + "eq_calc_type": "specific", + "eq_manufacturer": "IMC", + "model": "GenericHR_WWHP", "nominal_tons": 500, "fuel": "electricity", "refrigerant": "R-515B", @@ -144,6 +153,7 @@ "eq_id": "hp01", "eq_type": "heat_pump", "eq_subtype": "air_to_water", + "eq_calc_type": "specific", "model": "GenericAWHP", "nominal_tons": 60, "fuel": "electricity", @@ -199,7 +209,9 @@ "eq_id": "hp02", "eq_type": "heat_pump", "eq_subtype": "air_to_water", - "model": "GenericIMC", + "eq_calc_type": "specific", + "eq_manufacturer": "IMC", + "model": "GenericAWHP", "nominal_tons": 30, "fuel": "electricity", "refrigerant": "R-454B", @@ -252,7 +264,9 @@ "eq_id": "hp03", "eq_type": "heat_pump", "eq_subtype": "air_to_water", - "model": "GenericIMC", + "eq_calc_type": "specific", + "eq_manufacturer": "IMC", + "model": "GenericAWHP", "nominal_tons": 60, "fuel": "electricity", "refrigerant": "R-454B", @@ -305,6 +319,7 @@ "eq_id": "hp04", "eq_type": "heat_pump", "eq_subtype": "air_to_water", + "eq_calc_type": "specific", "eq_manufacturer": "Aermec", "model": "NRBHA800", "nominal_tons": 60, @@ -369,6 +384,7 @@ "eq_id": "hp05", "eq_type": "heat_pump", "eq_subtype": "air_to_water", + "eq_calc_type": "specific", "eq_manufacturer": "Aermec", "model": "NRBHA1600", "nominal_tons": 120, @@ -433,6 +449,7 @@ "eq_id": "hp06", "eq_type": "heat_pump", "eq_subtype": "air_to_water", + "eq_calc_type": "specific", "eq_manufacturer": "Trane", "model": "AXM", "nominal_tons": 60, @@ -505,6 +522,7 @@ "eq_id": "hp07", "eq_type": "heat_pump", "eq_subtype": "air_to_water", + "eq_calc_type": "specific", "eq_manufacturer": "Trane", "model": "ACX", "nominal_tons": 120, @@ -577,60 +595,64 @@ "eq_id": "bo01", "eq_type": "backup_heating", "eq_subtype": "hot_water", - "model": "GenericBoiler-90%Eff", + "eq_calc_type": "generic", + "model": "GenericBoiler", "fuel": "natural_gas", - "capacity_W": 500000, "performance": { "heating": { - "efficiency": 0.9 + "efficiency": 0.8 } - }, - "emissions": { - "co2_kg_per_mwh": 200 } }, { "eq_id": "bo02", "eq_type": "backup_heating", "eq_subtype": "hot_water", - "model": "GenericBoiler-80%Eff", + "eq_calc_type": "specific", + "model": "GenericBoiler", "fuel": "natural_gas", - "capacity_W": 500000, + "capacity_W": 150000, "performance": { "heating": { - "efficiency": 0.8 + "efficiency": 0.9 } - }, - "emissions": { - "co2_kg_per_mwh": 200 } }, { "eq_id": "bo03", "eq_type": "backup_heating", "eq_subtype": "hot_water", - "model": "GenericBoiler-70%Eff", + "eq_calc_type": "specific", + "model": "GenericBoiler", "fuel": "natural_gas", - "capacity_W": 500000, + "capacity_W": 150000, + "performance": { + "heating": { + "efficiency": 0.8 + } + } + }, + { + "eq_id": "bo04", + "eq_type": "backup_heating", + "eq_subtype": "hot_water", + "eq_calc_type": "specific", + "model": "GenericBoiler", + "fuel": "natural_gas", + "capacity_W": 150000, "performance": { "heating": { "efficiency": 0.7 } - }, - "emissions": { - "co2_kg_per_mwh": 200 } }, { "eq_id": "ch01", "eq_type": "chiller", - "model": "GenericChiller", "eq_subtype": "air_cooled", + "eq_calc_type": "generic", + "model": "GenericChiller", "fuel": "electricity", - "refrigerant": "R-410A", - "refrigerant_weight_g": 90700, - "refrigerant_gwp": 2088, - "capacity_W": 150000, "performance": { "cooling": { "efficiency": 2.84 @@ -641,7 +663,10 @@ "eq_id": "ch02", "eq_type": "chiller", "eq_subtype": "air_cooled", - "model": "IMC-AirCooledChiller-100Ton", + "eq_calc_type": "specific", + "eq_manufacturer": "IMC", + "model": "GenericChiller", + "nominal_tons": 100, "fuel": "electricity", "refrigerant": "R-410A", "refrigerant_weight_g": 45400, @@ -657,7 +682,10 @@ "eq_id": "ch03", "eq_type": "chiller", "eq_subtype": "air_cooled", - "model": "IMC-AirCooledChiller-200Ton", + "eq_calc_type": "specific", + "eq_manufacturer": "IMC", + "model": "GenericChiller", + "nominal_tons": 200, "fuel": "electricity", "refrigerant": "R-410A", "refrigerant_weight_g": 90700, @@ -674,8 +702,22 @@ "eq_type": "backup_heating", "model": "GenericResHeater", "eq_subtype": "electric", + "eq_calc_type": "generic", "fuel": "electricity", - "capacity_W": 50000, + "performance": { + "heating": { + "efficiency": 1.0 + } + } + }, + { + "eq_id": "res02", + "eq_type": "backup_heating", + "model": "GenericResHeater", + "eq_calc_type": "specific", + "eq_subtype": "electric", + "fuel": "electricity", + "capacity_W": 150000, "performance": { "heating": { "efficiency": 1.0 @@ -696,7 +738,7 @@ "awhp_redundancy": 1, "awhp_h_supply_t": 38, "awhp_use_cooling": false, - "backup_heating": "bo02", + "backup_heating": "bo03", "chiller": "ch03" }, { @@ -710,7 +752,7 @@ "awhp_redundancy": 1, "awhp_h_supply_t": 38, "awhp_use_cooling": false, - "backup_heating": "res01", + "backup_heating": "res02", "chiller": "ch03" }, { @@ -724,7 +766,7 @@ "awhp_redundancy": 1, "awhp_h_supply_t": 38, "awhp_use_cooling": true, - "backup_heating": "bo02", + "backup_heating": "bo03", "chiller": "ch03" }, { @@ -738,7 +780,7 @@ "awhp_redundancy": 1, "awhp_h_supply_t": 38, "awhp_use_cooling": true, - "backup_heating": "res01", + "backup_heating": "res02", "chiller": "ch03" }, { @@ -752,7 +794,7 @@ "awhp_redundancy": 1, "awhp_h_supply_t": 38, "awhp_use_cooling": true, - "backup_heating": "res01", + "backup_heating": "res02", "chiller": "ch03" }, { @@ -766,7 +808,7 @@ "awhp_redundancy": 1, "awhp_h_supply_t": 38, "awhp_use_cooling": true, - "backup_heating": "bo02", + "backup_heating": "bo03", "chiller": "ch03" }, { @@ -780,7 +822,7 @@ "awhp_redundancy": 1, "awhp_h_supply_t": 38, "awhp_use_cooling": true, - "backup_heating": "bo02", + "backup_heating": "bo03", "chiller": "ch03" }, { @@ -794,7 +836,7 @@ "awhp_redundancy": 1, "awhp_h_supply_t": 38, "awhp_use_cooling": true, - "backup_heating": "bo02", + "backup_heating": "bo03", "chiller": "ch03" }, { @@ -808,7 +850,7 @@ "awhp_redundancy": 1, "awhp_h_supply_t": 38, "awhp_use_cooling": true, - "backup_heating": "bo02", + "backup_heating": "bo03", "chiller": "ch03" }, { @@ -822,7 +864,7 @@ "awhp_redundancy": 1, "awhp_h_supply_t": 38, "awhp_use_cooling": false, - "backup_heating": "bo02", + "backup_heating": "bo03", "chiller": "ch03" }, { @@ -836,7 +878,7 @@ "awhp_redundancy": 1, "awhp_h_supply_t": 38, "awhp_use_cooling": false, - "backup_heating": "res01", + "backup_heating": "res02", "chiller": "ch03" }, { @@ -850,7 +892,7 @@ "awhp_redundancy": 1, "awhp_h_supply_t": 38, "awhp_use_cooling": true, - "backup_heating": "res01", + "backup_heating": "res02", "chiller": "ch03" } ], diff --git a/pages/equipment_page.py b/pages/equipment_page.py index 48a1b08..8ba47e9 100644 --- a/pages/equipment_page.py +++ b/pages/equipment_page.py @@ -998,14 +998,36 @@ def update_temp_labels_on_unit_change(unit_mode): def _build_equipment_options( equipment_list, eq_type, include_none=False, none_label="None" ): - options = [ - { - "label": f"{eq.get('model', '')} ({eq.get('eq_subtype', '')})", - "value": eq.get("eq_id"), - } - for eq in (equipment_list or []) - if eq.get("eq_type") == eq_type - ] + options = [] + for eq in (equipment_list or []): + if eq.get("eq_type") == eq_type: + + if eq.get("eq_manufacturer"): + label = f"{eq.get('eq_manufacturer', '')} {eq.get('model', '')}" + + else: + label = f"{eq.get('model', '')}" + + if eq_type == "backup_heating": + eff = eq.get('performance', '')['heating']['efficiency'] + label += f" {eff*100:.0f}% Eff" + + if eq.get("eq_calc_type") == "specific": + if eq_type in ["hr_heat_pump", "heat_pump", "chiller"]: + label += f" ({eq.get('nominal_tons', '')} Ton)" + + if eq_type == "backup_heating": + label += f" ({eq.get('capacity_W', '')/1000:.0f} kW)" + + else: + label += " (Infinite cap.)" + + options.append( + { + "label": label, + "value": eq.get("eq_id"), + } + ) if include_none: options = [{"label": none_label, "value": "None"}] + options return options diff --git a/src/energy.py b/src/energy.py index cfd9cf8..f4b1c29 100644 --- a/src/energy.py +++ b/src/energy.py @@ -731,6 +731,19 @@ 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") + df[Col.BOILER_HHW_W.value] = boiler_served_W df[Col.GAS_BOILER_WH.value] = gas_Wh df[Col.BOILER_EFF.value] = eff @@ -768,6 +781,20 @@ def loads_to_site_energy( logger.warning( f"Both gas and electric backup heating equipment are being used." ) + + resheater_peak_served_W = np.nanmax(elec_res_Wh) + # sizing logic + if backup_heating.eq_calc_type == "generic": + # generic equipment - do not account for space, electric capacity, etc. + resheater_num = 0 + else: + # specific equipment model + resheater_cap = backup_heating.capacity_W + resheater_num = np.ceil(resheater_peak_served_W / resheater_cap) + resheater_num = max(resheater_num, 1) # ensure at least one unit + + logger.debug(f"Electric resistance heater sizing: {resheater_num:.0f} units") + df[Col.RES_HHW_W.value] = remaining_h_W df[Col.ELEC_RES_WH.value] = elec_res_Wh df[Col.ELEC_WH.value] += elec_res_Wh @@ -861,13 +888,30 @@ def loads_to_site_energy( # scalar COP path served_W = df[Col.CHW_REM_W.value].to_numpy() elec_Wh = served_W / chiller_cop + + chl_peak_served_W = np.nanmax(served_W) + # sizing logic + if chl.eq_calc_type == "generic": + # generic equipment - do not account for refrigerant, space, electric capacity, etc. + chl_num = 0 + else: + # specific equipment model + # assumes fixed capacity, edit for curves later + chl_cap = chl.capacity_W + chl_num = np.ceil(chl_peak_served_W / chl_cap) + chl_num = max(chl_num, 1) # ensure at least one unit + + logger.debug(f"Chiller sizing: {chl_num:.0f} units") # add refrigerant information chiller_refrigerant = chl.refrigerant if chl.refrigerant else "Unknown" num_hours = len(df) # Use actual data length (handles leap years) chiller_refrigerant_weight_kg = ( - chl.refrigerant_weight_g * 0.001 / num_hours + chl.refrigerant_weight_g + * 0.001 + * chl_num + / num_hours if chl.refrigerant_weight_g else 0.0 ) @@ -878,7 +922,8 @@ def loads_to_site_energy( else 0.0 ) - if chiller_refrigerant_gwp_kg == 0: + # this will produce a warning if generic equipment is used + if chiller_refrigerant_gwp_kg == 0: if chiller_refrigerant_weight_kg == 0: logger.warning("Chiller refrigerant charge is 0.") else: diff --git a/src/equipment.py b/src/equipment.py index 78a9d00..e489977 100644 --- a/src/equipment.py +++ b/src/equipment.py @@ -31,6 +31,7 @@ class Equipment(BaseModel): eq_id: str eq_type: str eq_subtype: str | None = None + eq_calc_type: str | None = None eq_manufacturer: str | None = None model: str nominal_tons: int | None = None From 348a566ca222df57a883d1a8e23baf918eb91d9e Mon Sep 17 00:00:00 2001 From: urwahah Date: Thu, 9 Apr 2026 12:45:30 -0700 Subject: [PATCH 2/2] add unit conversion --- data/input/equipment_data.JSON | 28 +++++++++++++------------- pages/equipment_page.py | 36 +++++++++++++++++++++++++--------- src/equipment.py | 2 +- 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/data/input/equipment_data.JSON b/data/input/equipment_data.JSON index 7b52105..27c62d6 100644 --- a/data/input/equipment_data.JSON +++ b/data/input/equipment_data.JSON @@ -6,7 +6,7 @@ "eq_subtype": "water_to_water", "eq_calc_type": "specific", "model": "GenericHR_WWHP", - "nominal_tons": 300, + "nominal_capacity_W": 1054800, "fuel": "electricity", "refrigerant": "R-454B", "refrigerant_weight_g": 200000, @@ -36,7 +36,7 @@ "eq_calc_type": "specific", "eq_manufacturer": "IMC", "model": "GenericHR_WWHP", - "nominal_tons": 30, + "nominal_capacity_W": 105480, "fuel": "electricity", "refrigerant": "R-454B", "refrigerant_weight_g": 17700, @@ -66,7 +66,7 @@ "eq_calc_type": "specific", "eq_manufacturer": "IMC", "model": "GenericHR_WWHP", - "nominal_tons": 60, + "nominal_capacity_W": 210960, "fuel": "electricity", "refrigerant": "R-454B", "refrigerant_weight_g": 29000, @@ -96,7 +96,7 @@ "eq_calc_type": "specific", "eq_manufacturer": "IMC", "model": "GenericHR_WWHP", - "nominal_tons": 250, + "nominal_capacity_W": 879000, "fuel": "electricity", "refrigerant": "R-515B", "refrigerant_weight_g": 300800, @@ -126,7 +126,7 @@ "eq_calc_type": "specific", "eq_manufacturer": "IMC", "model": "GenericHR_WWHP", - "nominal_tons": 500, + "nominal_capacity_W": 1758000, "fuel": "electricity", "refrigerant": "R-515B", "refrigerant_weight_g": 453600, @@ -155,7 +155,7 @@ "eq_subtype": "air_to_water", "eq_calc_type": "specific", "model": "GenericAWHP", - "nominal_tons": 60, + "nominal_capacity_W": 210960, "fuel": "electricity", "refrigerant": "R-454B", "refrigerant_weight_g": 54000, @@ -212,7 +212,7 @@ "eq_calc_type": "specific", "eq_manufacturer": "IMC", "model": "GenericAWHP", - "nominal_tons": 30, + "nominal_capacity_W": 105480, "fuel": "electricity", "refrigerant": "R-454B", "refrigerant_weight_g": 40000, @@ -267,7 +267,7 @@ "eq_calc_type": "specific", "eq_manufacturer": "IMC", "model": "GenericAWHP", - "nominal_tons": 60, + "nominal_capacity_W": 210960, "fuel": "electricity", "refrigerant": "R-454B", "refrigerant_weight_g": 80000, @@ -322,7 +322,7 @@ "eq_calc_type": "specific", "eq_manufacturer": "Aermec", "model": "NRBHA800", - "nominal_tons": 60, + "nominal_capacity_W": 210960, "fuel": "electricity", "refrigerant": "R-410A", "refrigerant_weight_g": 29030, @@ -387,7 +387,7 @@ "eq_calc_type": "specific", "eq_manufacturer": "Aermec", "model": "NRBHA1600", - "nominal_tons": 120, + "nominal_capacity_W": 421920, "fuel": "electricity", "refrigerant": "R-410A", "refrigerant_weight_g": 55792, @@ -452,7 +452,7 @@ "eq_calc_type": "specific", "eq_manufacturer": "Trane", "model": "AXM", - "nominal_tons": 60, + "nominal_capacity_W": 210960, "fuel": "electricity", "refrigerant": "R-454B", "refrigerant_weight_g": 78018, @@ -525,7 +525,7 @@ "eq_calc_type": "specific", "eq_manufacturer": "Trane", "model": "ACX", - "nominal_tons": 120, + "nominal_capacity_W": 421920, "fuel": "electricity", "refrigerant": "R-454B", "refrigerant_weight_g": 104326, @@ -666,7 +666,7 @@ "eq_calc_type": "specific", "eq_manufacturer": "IMC", "model": "GenericChiller", - "nominal_tons": 100, + "nominal_capacity_W": 351600, "fuel": "electricity", "refrigerant": "R-410A", "refrigerant_weight_g": 45400, @@ -685,7 +685,7 @@ "eq_calc_type": "specific", "eq_manufacturer": "IMC", "model": "GenericChiller", - "nominal_tons": 200, + "nominal_capacity_W": 703200, "fuel": "electricity", "refrigerant": "R-410A", "refrigerant_weight_g": 90700, diff --git a/pages/equipment_page.py b/pages/equipment_page.py index 8ba47e9..465e0da 100644 --- a/pages/equipment_page.py +++ b/pages/equipment_page.py @@ -694,16 +694,16 @@ def open_edit_modal(edit_clicks, equipment_data, unit_mode): # --- Build select options from equipment list --- hr_hp_options = _build_equipment_options( - equipment_list, "hr_heat_pump", include_none=True + equipment_list, "hr_heat_pump", unit_mode, include_none=True ) awhp_options = _build_equipment_options( - equipment_list, "heat_pump", include_none=True + equipment_list, "heat_pump", unit_mode, include_none=True ) backup_heating_options = _build_equipment_options( - equipment_list, "backup_heating", include_none=False + equipment_list, "backup_heating", unit_mode, include_none=False ) chiller_options = _build_equipment_options( - equipment_list, "chiller", include_none=False + equipment_list, "chiller", unit_mode, include_none=False ) # --- Scenario current values --- @@ -996,8 +996,15 @@ def update_temp_labels_on_unit_change(unit_mode): def _build_equipment_options( - equipment_list, eq_type, include_none=False, none_label="None" + equipment_list, eq_type, unit_mode, include_none=False, none_label="None" ): + from utils.units import format_with_auto_scale + unit_mode = unit_mode or "SI" + if unit_mode == 'IP': + decimals = 0 + else: + decimals = 1 + options = [] for eq in (equipment_list or []): if eq.get("eq_type") == eq_type: @@ -1014,13 +1021,24 @@ def _build_equipment_options( if eq.get("eq_calc_type") == "specific": if eq_type in ["hr_heat_pump", "heat_pump", "chiller"]: - label += f" ({eq.get('nominal_tons', '')} Ton)" - + capacity = "nominal_capacity_W" + category = "power_cooling" + if eq_type == "backup_heating": - label += f" ({eq.get('capacity_W', '')/1000:.0f} kW)" + capacity = "capacity_W" + category = "power" + + label_cap = format_with_auto_scale( + eq.get(capacity, ''), + category, + unit_mode, + decimals = decimals + ) else: - label += " (Infinite cap.)" + label_cap = "Infinite cap." + + label += f" ({label_cap})" options.append( { diff --git a/src/equipment.py b/src/equipment.py index e489977..4ae0e82 100644 --- a/src/equipment.py +++ b/src/equipment.py @@ -34,7 +34,7 @@ class Equipment(BaseModel): eq_calc_type: str | None = None eq_manufacturer: str | None = None model: str - nominal_tons: int | None = None + nominal_capacity_W: int | None = None fuel: str refrigerant: str | None = None refrigerant_weight_g: float | None = None