From 78800ba1d2602f4c51195320d040e1367a937e41 Mon Sep 17 00:00:00 2001 From: Toby Kramer <49641232+t-kramer@users.noreply.github.com> Date: Mon, 14 Sep 2026 10:37:12 -0700 Subject: [PATCH] Revert "Average emissions factors #52" --- data/input/metadata_index.json | 6 -- layout/charts.py | 2 +- layout/input.py | 76 ++++++--------- pages/emissions_page.py | 163 +++++++-------------------------- src/config.py | 19 +--- src/emissions.py | 24 ++--- src/energy.py | 162 +++++++++++--------------------- src/metadata.py | 12 +-- tests/conftest.py | 4 +- utils/units.py | 5 +- 10 files changed, 136 insertions(+), 337 deletions(-) diff --git a/data/input/metadata_index.json b/data/input/metadata_index.json index 76a81fe..b36d172 100644 --- a/data/input/metadata_index.json +++ b/data/input/metadata_index.json @@ -63,12 +63,6 @@ ] }, "emissions": { - "elec_emission_source" : [ - "Marginal (Cambium, Long-run)", - "Marginal (Cambium, Short-run)", - "Average (Cambium)", - "Constant (User-provided)" - ], "year": [ 2025, 2030, diff --git a/layout/charts.py b/layout/charts.py index 523ce35..2e9704b 100644 --- a/layout/charts.py +++ b/layout/charts.py @@ -54,7 +54,7 @@ def emissions_bar_chart(): dmc.MultiSelect( id="emission-em-scen-dropdown", data=_emission_scen_seed(), # will be overwritten dynamically - value=["em_scenario_a", "em_scenario_b", "em_scenario_c", "em_scenario_d"], + value=["em_scenario_a", "em_scenario_b", "em_scenario_c"], placeholder="Emission Scenarios", searchable=True, clearable=True, diff --git a/layout/input.py b/layout/input.py index 3af1f1d..e1e3517 100644 --- a/layout/input.py +++ b/layout/input.py @@ -1163,7 +1163,7 @@ def build_emissions_table(emission_data, active_ids=None, view_mode="simple", un view_mode: One of "simple", "advanced", or "differences" unit_mode: "SI" or "IP" for unit conversion """ - from utils.units import get_converter, get_display_unit + from utils.units import get_unit_converter, get_unit_label emission_df = pd.DataFrame(emission_data) if isinstance(emission_data, list) else emission_data @@ -1181,22 +1181,16 @@ def build_emissions_table(emission_data, active_ids=None, view_mode="simple", un # Sort for stable column order emission_df = emission_df.sort_values("em_scen_id").reset_index(drop=True) - # Get unit label for emission rates (dynamic based on unit_mode) - ng_emission_rate_unit = get_display_unit("gas_emission_factor", unit_mode) - elec_emission_rate_unit = get_display_unit("emissions_rate", unit_mode) + # Get unit label for NG emission rate (dynamic based on unit_mode) + ng_emission_rate_unit = get_unit_label("emissions_rate", unit_mode) # Rows to display (property name, label) # Note: em_scen_id is excluded as it's shown in the header row_config = [ - ("em_scen_name", "Scenario"), - ("elec_emission_source", "Grid emissions source"), - ( - "elec_avg_emission_rate_gCO2e_per_kWh", - f"Constant grid emissions rate ({elec_emission_rate_unit})", - ), ("grid_scenario", "Grid Scenario"), ("gea_grid_region", "GEA Grid Region"), ("emission_type", "Emission Type"), + ("shortrun_weighting", "Short-run weighting"), ("annual_refrig_leakage_percent", "Refrigerant leakage (frac)"), ( "ng_emission_rate_gCO2e_per_kWh", @@ -1325,9 +1319,8 @@ def build_emissions_table(emission_data, active_ids=None, view_mode="simple", un # ---------- Property rows ---------- diff_row_style = TABLE_STYLE.diff_row_style - # Get converter for emission rate values - ng_emission_rate_converter = get_converter("gas_emission_factor", unit_mode) - elec_emission_rate_converter = get_converter("emissions_rate", unit_mode) + # Get converter for NG emission rate values + ng_emission_rate_converter = get_unit_converter("emissions_rate", unit_mode) for field, label in available_rows: is_diff_row = field in diff_fields @@ -1342,19 +1335,13 @@ def build_emissions_table(emission_data, active_ids=None, view_mode="simple", un for idx, scen_id in enumerate(scen_ids): raw_value = emission_df.iloc[idx].get(field, "") - # Apply unit conversion for emission rates + # Apply unit conversion for NG emission rate if field == "ng_emission_rate_gCO2e_per_kWh" and raw_value is not None: try: converted = ng_emission_rate_converter(float(raw_value)) display_value = f"{converted:.2f}" except (ValueError, TypeError): display_value = format_table_value(raw_value, field_name=field) - elif field == "elec_avg_emission_rate_gCO2e_per_kWh" and raw_value is not None: - try: - converted = elec_emission_rate_converter(float(raw_value)) - display_value = f"{converted:.2f}" - except (ValueError, TypeError): - display_value = format_table_value(raw_value, field_name=field) else: display_value = format_table_value(raw_value, field_name=field) @@ -1923,36 +1910,6 @@ def _options(values): ], grow=True, ), - dmc.SimpleGrid( - cols=2, - spacing="md", - children=[ - dmc.Select( - id="edit-em-elec-source", - label="Grid emissions source", - placeholder="Select grid emissions source", - data=_options(emissions_index["elec_emission_source"]), - searchable=True, - clearable=False, - ), - dmc.Stack( - [ - dmc.Text( - id="edit-em-elec-emission-rate-label", - children="Constant grid emissions rate (g/kWh)", - size="sm", - fw=500, - ), - dmc.NumberInput( - id="edit-em-elec-avg-emission-rate", - min=0, - step=1, - ), - ], - gap=4, - ), - ], - ), dmc.SimpleGrid( cols=2, spacing="md", @@ -1979,6 +1936,12 @@ def _options(values): cols=2, spacing="md", children=[ + dmc.TextInput( + id="edit-em-time-zone", + label="Time zone", + placeholder="e.g. America/Los_Angeles", + disabled=True, + ), dmc.Select( id="edit-em-emission-type", label="Emission type", @@ -1987,6 +1950,19 @@ def _options(values): searchable=False, clearable=False, ), + ], + ), + dmc.SimpleGrid( + cols=2, + spacing="md", + children=[ + dmc.NumberInput( + id="edit-em-shortrun-weighting", + label="Short-run weighting", + min=0, + max=1, + step=0.1, + ), dmc.Select( id="edit-em-year", label="Year", diff --git a/pages/emissions_page.py b/pages/emissions_page.py index d9e9e67..33c94b3 100644 --- a/pages/emissions_page.py +++ b/pages/emissions_page.py @@ -100,10 +100,6 @@ def layout(): "label": "Combustion vs pre-combustion", "value": "emission_types", }, - { - "label": "Different electricity emission factors", - "value": "emission_sources", - }, ], value=None, placeholder="Select a scenario group", @@ -239,7 +235,6 @@ def handle_emission_group_selection(group_id, metadata_data, selected_ids, store - year: Varies years (2025, 2035, 2045), others reset to defaults - refrigerant_leakage: Varies leakage (0.01, 0.05, 0.1), others reset to defaults - emission_types: Varies emission type ("Includes pre-combustion" vs "Combustion only"), others reset to defaults - - emission_sources: Varies emission source (average and marginal) and associated values, others reset to defaults """ # When dropdown is cleared, clear the stored group to allow re-selecting if not group_id: @@ -262,12 +257,8 @@ def handle_emission_group_selection(group_id, metadata_data, selected_ids, store default_ids = ["em_scenario_a", "em_scenario_b"] if group_id != "emission_types": # only two scenarios for comparing emission types default_ids.append("em_scenario_c") - if group_id == "emission_sources": # fourth scenario for comparing emission sources - default_ids.append("em_scenario_d") # Get defaults - default_elec_emission_source = EmissionScenarioDefaults.ELEC_EMISSION_SOURCE.value - default_elec_avg_emission_rate = EmissionScenarioDefaults.ELEC_AVG_EMISSION_RATE_G_KWH.value default_year = EmissionScenarioDefaults.YEAR.value default_leakage = EmissionScenarioDefaults.REFRIGERANT_LEAKAGE.value default_emission_type = EmissionScenarioDefaults.EMISSION_TYPE.value @@ -281,37 +272,24 @@ def handle_emission_group_selection(group_id, metadata_data, selected_ids, store EmissionScenarioDefaults.NG_EMISSION_RATE_G_KWH_COMBUSTION.value, default_ng_emission_rate, ] - elec_emission_source_values = [ - "Average (Cambium)", - "Constant (User-provided)", - "Marginal (Cambium, Long-run)", - "Marginal (Cambium, Short-run)", - ] - elec_avg_emission_rate_values = [None, 0, None, None] - emission_sources_names = [ - "Cambium Average Emissions", - "Constant (Zero) Emissions", - "Long-run Marginal Emissions", - "Short-run Marginal Emissions", - ] # Create base scenario template from first existing scenario or defaults base_scenario = ( existing_scenarios[0].copy() if existing_scenarios else { - "elec_emission_source": default_elec_emission_source, - "elec_avg_emission_rate_gCO2e_per_kWh": default_elec_avg_emission_rate, "grid_scenario": "MidCase", "gea_grid_region": None, + "time_zone": "America/Los_Angeles", "emission_type": default_emission_type, + "shortrun_weighting": 0, "annual_refrig_leakage_percent": default_leakage, "ng_emission_rate_gCO2e_per_kWh": default_ng_emission_rate, "year": default_year, } ) - # Reset to default 2/3/4 scenarios (a, b, optionally c & d) with group-specific values + # Reset to default 2/3 scenarios (a, b, optionally c) with group-specific values updated_scenarios = [] for idx, scen_id in enumerate(default_ids): scen = {**base_scenario, "em_scen_id": scen_id, "em_scen_name": ""} @@ -319,53 +297,27 @@ def handle_emission_group_selection(group_id, metadata_data, selected_ids, store if group_id == "year": # Vary year, reset others to defaults scen["year"] = year_values[idx % len(year_values)] - scen["em_scen_name"] = str(scen["year"]) - scen["annual_refrig_leakage_percent"] = default_leakage scen["emission_type"] = default_emission_type scen["ng_emission_rate_gCO2e_per_kWh"] = default_ng_emission_rate - scen["elec_emission_source"] = default_elec_emission_source - scen["elec_avg_emission_rate_gCO2e_per_kWh"] = default_elec_avg_emission_rate + scen["em_scen_name"] = str(scen["year"]) elif group_id == "refrigerant_leakage": # Vary leakage, reset others to defaults scen["annual_refrig_leakage_percent"] = leakage_values[idx % len(leakage_values)] - pct_leakage = scen["annual_refrig_leakage_percent"] * 100 - scen["em_scen_name"] = f"{pct_leakage:.0f}% leakage" - scen["year"] = default_year scen["emission_type"] = default_emission_type scen["ng_emission_rate_gCO2e_per_kWh"] = default_ng_emission_rate - scen["elec_emission_source"] = default_elec_emission_source - scen["elec_avg_emission_rate_gCO2e_per_kWh"] = default_elec_avg_emission_rate + pct_leakage = scen["annual_refrig_leakage_percent"] * 100 + scen["em_scen_name"] = f"{pct_leakage:.0f}% leakage" elif group_id == "emission_types": # Set emission type, reset others to defaults scen["emission_type"] = emission_types[idx % len(emission_types)] scen["ng_emission_rate_gCO2e_per_kWh"] = ng_emission_rate_values[ idx % len(ng_emission_rate_values) ] - scen["em_scen_name"] = scen["emission_type"] - - scen["year"] = default_year - scen["annual_refrig_leakage_percent"] = default_leakage - scen["elec_emission_source"] = default_elec_emission_source - scen["elec_avg_emission_rate_gCO2e_per_kWh"] = default_elec_avg_emission_rate - elif group_id == "emission_sources": - # Vary source, avg emissions rate, and short-run weighting, reset others to defaults - scen["elec_emission_source"] = elec_emission_source_values[ - idx % len(elec_emission_source_values) - ] - scen["elec_avg_emission_rate_gCO2e_per_kWh"] = elec_avg_emission_rate_values[ - idx % len(elec_avg_emission_rate_values) - ] - scen["em_scen_name"] = emission_sources_names[idx % len(emission_sources_names)] - if scen["elec_emission_source"] == "Constant (User-provided)": - scen["grid_scenario"] = None - scen["gea_grid_region"] = None - scen["year"] = default_year scen["annual_refrig_leakage_percent"] = default_leakage - scen["emission_type"] = default_emission_type - scen["ng_emission_rate_gCO2e_per_kWh"] = default_ng_emission_rate + scen["em_scen_name"] = scen["emission_type"] updated_scenarios.append(scen) @@ -603,11 +555,11 @@ def remove_emission_scenario(remove_clicks, metadata_data, selected_em_ids): Output("emissions-edit-modal", "opened"), Output("edit-em-scenario-id-input", "value"), Output("edit-em-scenario-name-input", "value"), - Output("edit-em-elec-source", "value"), - Output("edit-em-elec-avg-emission-rate", "value"), Output("edit-em-grid-scenario", "value"), Output("edit-em-gea-grid-region", "value"), + Output("edit-em-time-zone", "value"), Output("edit-em-emission-type", "value"), + Output("edit-em-shortrun-weighting", "value"), Output("edit-em-year", "value"), Output("edit-em-refrig-leakage", "value"), Output("edit-em-ng-emission-rate", "value"), @@ -627,13 +579,13 @@ def open_edit_emission_modal(edit_clicks, metadata_data, unit_mode): "", "", "", - None, "", "", "", None, None, None, + None, "No emission data.", ) @@ -654,13 +606,13 @@ def open_edit_emission_modal(edit_clicks, metadata_data, unit_mode): "", "", "", - None, "", "", "", None, None, None, + None, "Failed to parse button id.", ) @@ -673,53 +625,44 @@ def open_edit_emission_modal(edit_clicks, metadata_data, unit_mode): "", "", "", - None, "", "", "", None, None, None, + None, f"Scenario {em_scen_id!r} not found.", ) - # Convert emissions rates for display based on unit mode - from utils.units import get_converter - + # Convert NG emissions rate for display based on unit mode ng_emission_rate_base = scen.get("ng_emission_rate_gCO2e_per_kWh") if ng_emission_rate_base is not None and unit_mode == "IP": - ng_converter = get_converter("gas_emission_factor", "IP") + from utils.units import get_unit_converter + + ng_converter = get_unit_converter("emissions_rate", "IP") ng_emission_rate_display = ng_converter(float(ng_emission_rate_base)) else: ng_emission_rate_display = ng_emission_rate_base - elec_avg_emission_rate_base = scen.get("elec_avg_emission_rate_gCO2e_per_kWh") - if elec_avg_emission_rate_base is not None and unit_mode == "IP": - elec_converter = get_converter("emissions_rate", "IP") - elec_avg_emission_rate_display = elec_converter(float(elec_avg_emission_rate_base)) - else: - elec_avg_emission_rate_display = elec_avg_emission_rate_base - # Round refrigerant leakage to 2 decimal places for display refrig_leakage = scen.get("annual_refrig_leakage_percent") if refrig_leakage is not None: refrig_leakage = round(float(refrig_leakage), 2) - # Round emission rate display to 2 decimal places + # Round NG emission rate display to 2 decimal places if ng_emission_rate_display is not None: ng_emission_rate_display = round(float(ng_emission_rate_display), 2) - if elec_avg_emission_rate_display is not None: - elec_avg_emission_rate_display = round(float(elec_avg_emission_rate_display), 2) return ( True, scen.get("em_scen_id"), scen.get("em_scen_name", ""), - scen.get("elec_emission_source", ""), - elec_avg_emission_rate_display, scen.get("grid_scenario", ""), scen.get("gea_grid_region", ""), + scen.get("time_zone", ""), scen.get("emission_type", ""), + scen.get("shortrun_weighting"), str(scen.get("year")) if scen.get("year") is not None else "", refrig_leakage, ng_emission_rate_display, @@ -734,11 +677,11 @@ def open_edit_emission_modal(edit_clicks, metadata_data, unit_mode): Input("edit-em-scenario-save-btn", "n_clicks"), State("edit-em-scenario-id-input", "value"), State("edit-em-scenario-name-input", "value"), - State("edit-em-elec-source", "value"), - State("edit-em-elec-avg-emission-rate", "value"), State("edit-em-grid-scenario", "value"), State("edit-em-gea-grid-region", "value"), + State("edit-em-time-zone", "value"), State("edit-em-emission-type", "value"), + State("edit-em-shortrun-weighting", "value"), State("edit-em-year", "value"), State("edit-em-refrig-leakage", "value"), State("edit-em-ng-emission-rate", "value"), @@ -750,11 +693,11 @@ def save_edit_emission( n_clicks, scen_id, scen_name, - elec_emission_source, - elec_avg_emission_rate, grid_scenario, gea_grid_region, + time_zone, emission_type, + shortrun_weighting, year, refrig_leakage, ng_emission_rate, @@ -771,6 +714,11 @@ def save_edit_emission( return True, no_update, "Scenario ID is missing." # basic type cleaning + try: + shortrun_weighting = float(shortrun_weighting) if shortrun_weighting is not None else 0.0 + except (TypeError, ValueError): + shortrun_weighting = 0.0 + try: year = int(year) if year is not None and year != "" else 2025 except (TypeError, ValueError): @@ -786,14 +734,6 @@ def save_edit_emission( except (TypeError, ValueError): ng_emission_rate = 0.0 - try: - elec_avg_emission_rate = ( - float(elec_avg_emission_rate) if elec_avg_emission_rate is not None else None - ) - except (TypeError, ValueError): - elec_avg_emission_rate = None - # default avg emission rate is None to generate an error if calculation is run without a valid input - # Convert NG emissions rate back to base units (g/kWh) if in IP mode unit_mode = unit_mode or "SI" if unit_mode == "IP" and ng_emission_rate > 0: @@ -803,13 +743,6 @@ def save_edit_emission( # g/kWh = (lb/kBTU) / (g_to_lb / Wh_to_BTU) ng_emission_rate = ng_emission_rate / (g_to_lb / Wh_to_BTU) - if unit_mode == "IP" and elec_avg_emission_rate is not None: - from utils.units import g_to_lb - - # IP unit is lb/kWh, convert back to g/kWh - # g/kWh = (lb/kWh) / (g_to_lb) - elec_avg_emission_rate = elec_avg_emission_rate / (g_to_lb) - scenarios = metadata_data.get("emission_settings", []) updated = False new_scenarios = [] @@ -818,11 +751,11 @@ def save_edit_emission( if scen.get("em_scen_id") == scen_id: new_scen = scen.copy() new_scen["em_scen_name"] = scen_name or scen.get("em_scen_name", "") - new_scen["elec_emission_source"] = elec_emission_source - new_scen["elec_avg_emission_rate_gCO2e_per_kWh"] = elec_avg_emission_rate new_scen["grid_scenario"] = grid_scenario new_scen["gea_grid_region"] = gea_grid_region + new_scen["time_zone"] = time_zone new_scen["emission_type"] = emission_type + new_scen["shortrun_weighting"] = shortrun_weighting new_scen["year"] = year new_scen["annual_refrig_leakage_percent"] = refrig_leakage new_scen["ng_emission_rate_gCO2e_per_kWh"] = ng_emission_rate @@ -853,18 +786,15 @@ def cancel_edit_emission_modal(n_clicks): @callback( Output("edit-em-ng-emission-rate-label", "children"), - Output("edit-em-elec-emission-rate-label", "children"), Input("unit-toggle", "value"), ) -def update_emissions_rate_label(unit_mode): - """Update NG/elec emissions rate label based on unit mode.""" - from utils.units import get_display_unit +def update_ng_emissions_rate_label(unit_mode): + """Update NG emissions rate label based on unit mode.""" + from utils.units import get_unit_label unit_mode = unit_mode or "SI" - ng_unit = get_display_unit("gas_emission_factor", unit_mode) - elec_unit = get_display_unit("emissions_rate", unit_mode) - - return f"Gas emissions rate ({ng_unit})", f"Constant grid emissions rate ({elec_unit})" + ng_unit = get_unit_label("emissions_rate", unit_mode) + return f"Gas emissions rate ({ng_unit})" @callback( @@ -1006,26 +936,3 @@ def update_ng_rate_on_emission_type_change(emission_type, unit_mode): ) return ng_emission_rate - - -@callback( - Output("edit-em-elec-avg-emission-rate", "disabled"), - Output("edit-em-grid-scenario", "value", allow_duplicate=True), - Output("edit-em-gea-grid-region", "value", allow_duplicate=True), - Input("edit-em-elec-source", "value"), - State("edit-em-grid-scenario", "value"), - State("edit-em-gea-grid-region", "value"), - prevent_initial_call=True, -) -def update_emission_inputs_on_source_change(emission_source, grid_scenario, grid_region): - """Update emissions inputs when emission source changes to fixed user-provided value.""" - - # disable average emissions rate input - disable_avg = emission_source != "Constant (User-provided)" - - # blank out cambium-related inputs - if not disable_avg: - grid_scenario = None - grid_region = None - - return disable_avg, grid_scenario, grid_region diff --git a/src/config.py b/src/config.py index f79c319..4c3338b 100644 --- a/src/config.py +++ b/src/config.py @@ -44,22 +44,12 @@ class EquipmentTableRows(Enum): class EmissionTableRows(Enum): """Row configurations for emission table view modes.""" - SIMPLE = ( - "em_scen_name", - "elec_emission_source", - "elec_avg_emission_rate_gCO2e_per_kWh", - "grid_scenario", - "gea_grid_region", - "emission_type", - "year", - ) + SIMPLE = ("grid_scenario", "gea_grid_region", "emission_type", "year") ADVANCED = ( - "em_scen_name", - "elec_emission_source", - "elec_avg_emission_rate_gCO2e_per_kWh", "grid_scenario", "gea_grid_region", "emission_type", + "shortrun_weighting", "annual_refrig_leakage_percent", "ng_emission_rate_gCO2e_per_kWh", "year", @@ -69,8 +59,6 @@ class EmissionTableRows(Enum): class EmissionScenarioDefaults(Enum): """Default values for emission scenario parameters.""" - ELEC_EMISSION_SOURCE: str = "Marginal (Cambium, Long-run)" - ELEC_AVG_EMISSION_RATE_G_KWH: float = None YEAR: int = 2025 REFRIGERANT_LEAKAGE: float = 0.02 EMISSION_TYPE: str = "Includes pre-combustion" @@ -170,9 +158,6 @@ class Columns(StrEnum): SRMER_CO2E_C = "srmer_co2e_c" SRMER_CO2E_P = "srmer_co2e_p" SRMER_CO2E = "srmer_co2e" - AER_LOAD_CO2E_C = "aer_load_co2e_c" - AER_LOAD_CO2E_P = "aer_load_co2e_p" - AER_LOAD_CO2E = "aer_load_co2e" SHORTRUN_WEIGHTING = "shortrun_weighting" ELEC_EMISSIONS_RATE_G_PER_KWH = "elec_emissions_rate_gCO2e_per_kWh" diff --git a/src/emissions.py b/src/emissions.py index dd286e8..2c39029 100644 --- a/src/emissions.py +++ b/src/emissions.py @@ -10,11 +10,11 @@ class EmissionScenario(DotAccessMixin, BaseModel): em_scen_id: str em_scen_name: str - elec_emission_source: str - elec_avg_emission_rate_gCO2e_per_kWh: float | None = None - grid_scenario: str | None = None + grid_scenario: str gea_grid_region: str | None = None + time_zone: str emission_type: str + shortrun_weighting: float annual_refrig_leakage_percent: float ng_emission_rate_gCO2e_per_kWh: float year: int @@ -24,7 +24,7 @@ class StandardEmissions: """ Unified interface for emissions data. Canonical schema: - emission_scenario | gea_grid_region | year | timestamp | lrmer_co2e_c | lrmer_co2e_p | srmer_co2e_c | srmer_co2e_p | aer_load_co2e_c | aer_load_co2e_p + emission_scenario | gea_grid_region | time_zone | year | timestamp | lrmer_co2e_c | lrmer_co2e_p | srmer_co2e_c | srmer_co2e_p """ def __init__(self, df: pd.DataFrame): @@ -36,13 +36,12 @@ def _validate(df: pd.DataFrame) -> pd.DataFrame: "emission_scenario", "gea_grid_region", "year", + "time_zone", "timestamp", "lrmer_co2e_c", "lrmer_co2e_p", "srmer_co2e_c", "srmer_co2e_p", - "aer_load_co2e_c", - "aer_load_co2e_p", ] missing = [c for c in required if c not in df.columns] if missing: @@ -55,14 +54,7 @@ def _validate(df: pd.DataFrame) -> pd.DataFrame: df = df.sort_values("timestamp").set_index("timestamp") # enforce numeric - for col in [ - "lrmer_co2e_c", - "lrmer_co2e_p", - "srmer_co2e_c", - "srmer_co2e_p", - "aer_load_co2e_c", - "aer_load_co2e_p", - ]: + for col in ["lrmer_co2e_c", "lrmer_co2e_p", "srmer_co2e_c", "srmer_co2e_p"]: df[col] = pd.to_numeric(df[col], errors="coerce") if df[col].isnull().any(): raise ValueError(f"Invalid numeric values in column {col}") @@ -115,6 +107,7 @@ def get_emissions_data( "em_scen_id": scenario.em_scen_id, "emission_scenario": scenario.grid_scenario, "gea_grid_region": scenario.gea_grid_region, + "time_zone": scenario.time_zone, "emission_type": scenario.emission_type, "year": df["year"], "timestamp": df["timestamp"], @@ -124,9 +117,6 @@ def get_emissions_data( "srmer_co2e_c": df["srmer_co2e_c"], "srmer_co2e_p": df["srmer_co2e_p"], "srmer_co2e": df["srmer_co2e"], - "aer_load_co2e_c": df["aer_load_co2e_c"], - "aer_load_co2e_p": df["aer_load_co2e_p"], - "aer_load_co2e": df["aer_load_co2e"], } ) diff --git a/src/energy.py b/src/energy.py index cd85c28..e41c0d0 100644 --- a/src/energy.py +++ b/src/energy.py @@ -1149,7 +1149,12 @@ def site_to_source( f"Processing emission scenario: {em_scen_id}, year={metadata[em_scen_id].year}" ) + emissions_data = get_emissions_data(metadata[em_scen_id]) + logger.debug(f"Loaded {len(emissions_data.df)} emission data rows") + em_scen = metadata[em_scen_id] + + shortrun_weighting = float(em_scen.shortrun_weighting) annual_refrig_leakage_percent = float(em_scen.annual_refrig_leakage_percent) gas_emissions_rate = float(em_scen.ng_emission_rate_gCO2e_per_kWh) @@ -1161,117 +1166,62 @@ def site_to_source( base[Col.HOUR.value] = base.index.hour base[Col.DOY.value] = base.index.dayofyear - if em_scen.elec_emission_source in [ - "Marginal (Cambium, Long-run)", - "Marginal (Cambium, Short-run)", - "Average (Cambium)", - ]: - emissions_data = get_emissions_data(metadata[em_scen_id]) - logger.debug( - f"Cambium grid emissions: Loaded {len(emissions_data.df)} emission data rows" - ) - - # collapse emissions to month-hour averages - emissions_data.df[Col.MONTH.value] = emissions_data.df.index.month - emissions_data.df[Col.HOUR.value] = emissions_data.df.index.hour - group_cols = [Col.MONTH.value, Col.HOUR.value] - - if em_scen.elec_emission_source == "Average (Cambium)": - # all rates are in gCO2e/kWh - if em_scen.emission_type == "Combustion only": - emissions_data.df[Col.ELEC_EMISSIONS_RATE_G_PER_KWH] = emissions_data.df[ - Col.AER_LOAD_CO2E_C.value - ] - elif em_scen.emission_type == "Includes pre-combustion": - emissions_data.df[Col.ELEC_EMISSIONS_RATE_G_PER_KWH] = ( - emissions_data.df[Col.AER_LOAD_CO2E_C.value] - + emissions_data.df[Col.AER_LOAD_CO2E_P.value] - ) - else: - raise ValueError(f"Invalid emissions_type: {em_scen.emission_type}") - - df_em = ( - emissions_data.df.groupby(group_cols)[ - [ - Col.ELEC_EMISSIONS_RATE_G_PER_KWH, - Col.AER_LOAD_CO2E_C.value, - Col.AER_LOAD_CO2E_P.value, - Col.AER_LOAD_CO2E.value, - ] - ] - .mean() - .reset_index() + # collapse emissions to month-hour averages + emissions_data.df[Col.MONTH.value] = emissions_data.df.index.month + emissions_data.df[Col.HOUR.value] = emissions_data.df.index.hour + emissions_data.df[Col.SHORTRUN_WEIGHTING.value] = shortrun_weighting + group_cols = [Col.MONTH.value, Col.HOUR.value] + + # all rates are in gCO2e/kWh + if em_scen.emission_type == "Combustion only": + emissions_data.df[Col.ELEC_EMISSIONS_RATE_G_PER_KWH] = ( + emissions_data.df[Col.LRMER_CO2E_C.value] * (1 - shortrun_weighting) + ) + (emissions_data.df[Col.SRMER_CO2E_C.value] * shortrun_weighting) + elif em_scen.emission_type == "Includes pre-combustion": + emissions_data.df[Col.ELEC_EMISSIONS_RATE_G_PER_KWH] = ( + ( + emissions_data.df[Col.LRMER_CO2E_C.value] + + emissions_data.df[Col.LRMER_CO2E_P.value] ) - else: - if em_scen.elec_emission_source == "Marginal (Cambium, Long-run)": - shortrun_weighting = 0 - elif em_scen.elec_emission_source == "Marginal (Cambium, Short-run)": - shortrun_weighting = 1 - emissions_data.df[Col.SHORTRUN_WEIGHTING.value] = shortrun_weighting - - # all rates are in gCO2e/kWh - if em_scen.emission_type == "Combustion only": - emissions_data.df[Col.ELEC_EMISSIONS_RATE_G_PER_KWH] = ( - emissions_data.df[Col.LRMER_CO2E_C.value] * (1 - shortrun_weighting) - ) + (emissions_data.df[Col.SRMER_CO2E_C.value] * shortrun_weighting) - elif em_scen.emission_type == "Includes pre-combustion": - emissions_data.df[Col.ELEC_EMISSIONS_RATE_G_PER_KWH] = ( - ( - emissions_data.df[Col.LRMER_CO2E_C.value] - + emissions_data.df[Col.LRMER_CO2E_P.value] - ) - * (1 - shortrun_weighting) - ) + ( - ( - emissions_data.df[Col.SRMER_CO2E_C.value] - + emissions_data.df[Col.SRMER_CO2E_P.value] - ) - * shortrun_weighting - ) - else: - raise ValueError(f"Invalid emissions_type: {em_scen.emission_type}") - - df_em = ( - emissions_data.df.groupby(group_cols)[ - [ - Col.ELEC_EMISSIONS_RATE_G_PER_KWH, - Col.LRMER_CO2E_C.value, - Col.LRMER_CO2E_P.value, - Col.LRMER_CO2E.value, - Col.SRMER_CO2E_C.value, - Col.SRMER_CO2E_P.value, - Col.SRMER_CO2E.value, - Col.SHORTRUN_WEIGHTING.value, - ] - ] - .mean() - .reset_index() + * (1 - shortrun_weighting) + ) + ( + ( + emissions_data.df[Col.SRMER_CO2E_C.value] + + emissions_data.df[Col.SRMER_CO2E_P.value] ) + * shortrun_weighting + ) + else: + raise ValueError(f"Invalid emissions_type: {em_scen.emission_type}") + + df_em = ( + emissions_data.df.groupby(group_cols)[ + [ + Col.ELEC_EMISSIONS_RATE_G_PER_KWH, + Col.LRMER_CO2E_C.value, + Col.LRMER_CO2E_P.value, + Col.LRMER_CO2E.value, + Col.SRMER_CO2E_C.value, + Col.SRMER_CO2E_P.value, + Col.SRMER_CO2E.value, + Col.SHORTRUN_WEIGHTING.value, + ] + ] + .mean() + .reset_index() + ) - # expand loads with this year's emissions - merged = base.merge(df_em, on=[Col.MONTH.value, Col.HOUR.value], how="left") - - nan_count = merged[Col.ELEC_EMISSIONS_RATE_G_PER_KWH].isna().sum() - if nan_count > 0: - logger.warning(f"Merge produced {nan_count} rows with missing emission rates") - - # Note: Keep original load data year (already extracted above) for timestamp - # reconstruction. This avoids Feb 29 errors when leap year load data is - # used with non-leap emission scenario years. Emissions are still correct - # because they're matched by month+hour pattern. - - elif em_scen.elec_emission_source == "Constant (User-provided)": - elec_avg_emission_rate = em_scen.elec_avg_emission_rate_gCO2e_per_kWh - if elec_avg_emission_rate is not None and elec_avg_emission_rate >= 0: - logger.debug(f"Constant grid emissions rate: {elec_avg_emission_rate} gCO2e/kWh") + # expand loads with this year's emissions + merged = base.merge(df_em, on=[Col.MONTH.value, Col.HOUR.value], how="left") - merged = base.copy() - merged[Col.ELEC_EMISSIONS_RATE_G_PER_KWH] = float(elec_avg_emission_rate) - else: - raise ValueError("Provide a valid input value for constant grid emissions rate.") + nan_count = merged[Col.ELEC_EMISSIONS_RATE_G_PER_KWH].isna().sum() + if nan_count > 0: + logger.warning(f"Merge produced {nan_count} rows with missing emission rates") - else: - raise ValueError(f"Invalid elec_emissions_source: {em_scen.elec_emission_source}") + # Note: Keep original load data year (already extracted above) for timestamp + # reconstruction. This avoids Feb 29 errors when leap year load data is + # used with non-leap emission scenario years. Emissions are still correct + # because they're matched by month+hour pattern. ## fuel switching logic emissions_intensity_HP = ( diff --git a/src/metadata.py b/src/metadata.py index f4ca6e4..76740e9 100644 --- a/src/metadata.py +++ b/src/metadata.py @@ -97,11 +97,11 @@ def create(cls, **overrides: Any) -> "Metadata": EmissionScenario( em_scen_id="em_scenario_a", em_scen_name="2025", - elec_emission_source="Marginal (Cambium, Long-run)", - elec_avg_emission_rate_gCO2e_per_kWh=None, grid_scenario="MidCase", gea_grid_region=None, + time_zone="America/Los_Angeles", emission_type="Includes pre-combustion", + shortrun_weighting=0, annual_refrig_leakage_percent=0.05, ng_emission_rate_gCO2e_per_kWh=239.2, year=2025, @@ -109,11 +109,11 @@ def create(cls, **overrides: Any) -> "Metadata": EmissionScenario( em_scen_id="em_scenario_b", em_scen_name="2035", - elec_emission_source="Marginal (Cambium, Long-run)", - elec_avg_emission_rate_gCO2e_per_kWh=None, grid_scenario="MidCase", gea_grid_region=None, + time_zone="America/Los_Angeles", emission_type="Includes pre-combustion", + shortrun_weighting=0, annual_refrig_leakage_percent=0.05, ng_emission_rate_gCO2e_per_kWh=239.2, year=2035, @@ -121,11 +121,11 @@ def create(cls, **overrides: Any) -> "Metadata": EmissionScenario( em_scen_id="em_scenario_c", em_scen_name="2045", - elec_emission_source="Marginal (Cambium, Long-run)", - elec_avg_emission_rate_gCO2e_per_kWh=None, grid_scenario="MidCase", gea_grid_region=None, + time_zone="America/Los_Angeles", emission_type="Includes pre-combustion", + shortrun_weighting=0, annual_refrig_leakage_percent=0.05, ng_emission_rate_gCO2e_per_kWh=239.2, year=2045, diff --git a/tests/conftest.py b/tests/conftest.py index 953b9c2..80ec79b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -54,11 +54,11 @@ def emission_scenario(): """Create a sample emission scenario for testing.""" return EmissionScenario( em_scen_id="test_em_1", - elec_emission_source="Marginal (Cambium, Long-run)", - elec_avg_emission_rate_gCO2e_per_kWh=None, grid_scenario="MidCase", gea_grid_region="CAMX", + time_zone="America/Los_Angeles", emission_type="Includes pre-combustion", + shortrun_weighting=0.0, annual_refrig_leakage_percent=0.05, ng_emission_rate_gCO2e_per_kWh=239.2, year=2025, diff --git a/utils/units.py b/utils/units.py index 7ad45be..707945f 100644 --- a/utils/units.py +++ b/utils/units.py @@ -185,7 +185,7 @@ def sqft_to_sqm(sqft): "emissions_rate": { "base": "g CO₂e/kWh", "SI": {"unit": "g CO₂e/kWh", "func": lambda x: x}, - "IP": {"unit": "lb CO₂e/kWh", "func": lambda x: x * g_to_lb}, + "IP": {"unit": "lb CO₂e/kBTU", "func": lambda x: x * g_to_lb / Wh_to_BTU}, }, # --- Gas emission factor --- "gas_emission_factor": { @@ -298,8 +298,6 @@ def sqft_to_sqm(sqft): "lrmer_co2e_p": ("emissions_rate", "LRMER Pre-combustion"), "srmer_co2e_c": ("emissions_rate", "SRMER Combustion"), "srmer_co2e_p": ("emissions_rate", "SRMER Pre-combustion"), - "aer_load_co2e_c": ("emissions_rate", "AER Load Combustion"), - "aer_load_co2e_p": ("emissions_rate", "AER Load Pre-combustion"), "ng_emission_rate_gCO2e_per_kWh": ("gas_emission_factor", "Gas Emissions Rate"), # === Refrigerant Mass (kg) === "hr_wwhp_refrigerant_weight_kg": ("mass", "HR-WWHP Refrig Weight"), @@ -329,7 +327,6 @@ def sqft_to_sqm(sqft): # === Emission Scenario Parameters === "lrmer_co2e": (None, "LRMER CO₂e"), "srmer_co2e": (None, "SRMER CO₂e"), - "aer_load_co2e": (None, "AER Load CO₂e"), "shortrun_weighting": (None, "Short-run Weighting"), "year": (None, "Year"), # === Scenario Identifiers ===