From 99efd93c22058e952dfc7d1df183cdc28e06ce6f Mon Sep 17 00:00:00 2001 From: urwahah Date: Mon, 7 Sep 2026 13:38:20 -0700 Subject: [PATCH 01/11] average emissions factor calculation --- app.py | 4 +- src/energy.py | 122 ++++++++++++++++++++++++++++---------------------- 2 files changed, 70 insertions(+), 56 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/energy.py b/src/energy.py index e41c0d0..c56ebb5 100644 --- a/src/energy.py +++ b/src/energy.py @@ -1149,9 +1149,6 @@ 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) @@ -1166,62 +1163,79 @@ def site_to_source( base[Col.HOUR.value] = base.index.hour base[Col.DOY.value] = base.index.dayofyear - # 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] - ) - * (1 - shortrun_weighting) - ) + ( - ( - emissions_data.df[Col.SRMER_CO2E_C.value] - + emissions_data.df[Col.SRMER_CO2E_P.value] - ) - * shortrun_weighting + elec_emissions_source = "average" # em_scen.elec_emissions_source + if elec_emissions_source == "marginal": # replace direct with em_scen.elec_emissions_source + emissions_data = get_emissions_data(metadata[em_scen_id]) + logger.debug( + f"Marginal electricity emissions: Loaded {len(emissions_data.df)} emission data rows" ) - 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, + + # 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] + ) + * (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() - ) + .mean() + .reset_index() + ) + + # expand loads with this year's emissions + merged = base.merge(df_em, on=[Col.MONTH.value, Col.HOUR.value], how="left") - # 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") - 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. - # 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 elec_emissions_source == "average": + # elec_emissions_rate = float(em_scen.elec_emission_rate_gCO2e_per_kWh) + elec_emissions_rate = 500 + logger.debug(f"Average electricity emissions: {elec_emissions_rate} gCO2e/kWh") + + merged = base.copy() + merged[Col.ELEC_EMISSIONS_RATE_G_PER_KWH] = elec_emissions_rate + else: + raise ValueError(f"Invalid elec_emissions_source: {em_scen.elec_emissions_source}") ## fuel switching logic emissions_intensity_HP = ( From bd81e225aff43462d3b9fc10cedf5cde9da1bf8f Mon Sep 17 00:00:00 2001 From: urwahah Date: Mon, 7 Sep 2026 15:52:02 -0700 Subject: [PATCH 02/11] add emissions source and avg factor inputs --- data/input/metadata_index.json | 4 ++ layout/input.py | 55 +++++++++++++++++++--- pages/emissions_page.py | 83 +++++++++++++++++++++++++++++----- src/config.py | 13 +++++- src/emissions.py | 2 + src/energy.py | 22 +++++---- src/metadata.py | 6 +++ tests/conftest.py | 2 + utils/units.py | 2 +- 9 files changed, 159 insertions(+), 30 deletions(-) diff --git a/data/input/metadata_index.json b/data/input/metadata_index.json index b36d172..d3c3ddf 100644 --- a/data/input/metadata_index.json +++ b/data/input/metadata_index.json @@ -63,6 +63,10 @@ ] }, "emissions": { + "elec_emission_source" : [ + "Marginal", + "Average" + ], "year": [ 2025, 2030, diff --git a/layout/input.py b/layout/input.py index f484767..5e5842b 100644 --- a/layout/input.py +++ b/layout/input.py @@ -1103,7 +1103,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_unit_converter, get_unit_label + from utils.units import get_converter, get_display_unit emission_df = pd.DataFrame(emission_data) if isinstance(emission_data, list) else emission_data @@ -1121,12 +1121,18 @@ 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 NG emission rate (dynamic based on unit_mode) - ng_emission_rate_unit = get_unit_label("emissions_rate", unit_mode) + # 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) # Rows to display (property name, label) # Note: em_scen_id is excluded as it's shown in the header row_config = [ + ("elec_emission_source", "Grid emissions source"), + ( + "elec_avg_emission_rate_gCO2e_per_kWh", + f"Average grid emissions rate ({elec_emission_rate_unit})", + ), ("grid_scenario", "Grid Scenario"), ("gea_grid_region", "GEA Grid Region"), ("emission_type", "Emission Type"), @@ -1256,8 +1262,9 @@ 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 NG emission rate values - ng_emission_rate_converter = get_unit_converter("emissions_rate", unit_mode) + # 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) for field, label in available_rows: is_diff_row = field in diff_fields @@ -1272,13 +1279,19 @@ 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 NG emission rate + # Apply unit conversion for emission rates 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) @@ -1847,6 +1860,36 @@ 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="Average 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", diff --git a/pages/emissions_page.py b/pages/emissions_page.py index 42c7841..64fe92f 100644 --- a/pages/emissions_page.py +++ b/pages/emissions_page.py @@ -261,6 +261,8 @@ def handle_emission_group_selection(group_id, metadata_data, selected_ids, store default_ids.append("em_scenario_c") # 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 @@ -280,6 +282,8 @@ def handle_emission_group_selection(group_id, metadata_data, selected_ids, store 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", @@ -557,6 +561,8 @@ 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"), @@ -573,7 +579,7 @@ def remove_emission_scenario(remove_clicks, metadata_data, selected_em_ids): ) def open_edit_emission_modal(edit_clicks, metadata_data, unit_mode): if not any(edit_clicks or []): - return (no_update,) * 12 + return (no_update,) * 14 if not metadata_data or "emission_settings" not in metadata_data: return ( @@ -581,6 +587,8 @@ def open_edit_emission_modal(edit_clicks, metadata_data, unit_mode): "", "", "", + None, + "", "", "", "", @@ -595,7 +603,7 @@ def open_edit_emission_modal(edit_clicks, metadata_data, unit_mode): triggered = callback_context.triggered if not triggered: - return (no_update,) * 12 + return (no_update,) * 14 prop_id = triggered[0]["prop_id"] id_str = prop_id.split(".")[0] @@ -608,6 +616,8 @@ def open_edit_emission_modal(edit_clicks, metadata_data, unit_mode): "", "", "", + None, + "", "", "", "", @@ -627,6 +637,8 @@ def open_edit_emission_modal(edit_clicks, metadata_data, unit_mode): "", "", "", + None, + "", "", "", "", @@ -637,29 +649,40 @@ def open_edit_emission_modal(edit_clicks, metadata_data, unit_mode): f"Scenario {em_scen_id!r} not found.", ) - # Convert NG emissions rate for display based on unit mode + # Convert emissions rates for display based on unit mode + from utils.units import get_converter + ng_emission_rate_base = scen.get("ng_emission_rate_gCO2e_per_kWh") if ng_emission_rate_base is not None and unit_mode == "IP": - from utils.units import get_unit_converter - - ng_converter = get_unit_converter("emissions_rate", "IP") + ng_converter = get_converter("gas_emission_factor", "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 NG emission rate display to 2 decimal places + # Round 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", ""), @@ -679,6 +702,8 @@ 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"), @@ -695,6 +720,8 @@ def save_edit_emission( n_clicks, scen_id, scen_name, + elec_emission_source, + elec_avg_emission_rate, grid_scenario, gea_grid_region, time_zone, @@ -736,6 +763,14 @@ 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: @@ -745,6 +780,13 @@ 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 = [] @@ -753,6 +795,8 @@ 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 @@ -788,15 +832,18 @@ 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_ng_emissions_rate_label(unit_mode): - """Update NG emissions rate label based on unit mode.""" - from utils.units import get_unit_label +def update_emissions_rate_label(unit_mode): + """Update NG/elec emissions rate label based on unit mode.""" + from utils.units import get_display_unit unit_mode = unit_mode or "SI" - ng_unit = get_unit_label("emissions_rate", unit_mode) - return f"Gas emissions rate ({ng_unit})" + 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"Average grid emissions rate ({elec_unit})" @callback( @@ -984,3 +1031,15 @@ 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"), + Input("edit-em-elec-source", "value"), + prevent_initial_call=True, +) +def update_avg_emission_on_source_change(emission_source): + """Enable/disable average grid emissions input when emission source changes.""" + disable_avg = emission_source != "Average" + + return disable_avg diff --git a/src/config.py b/src/config.py index 4c3338b..f9cceae 100644 --- a/src/config.py +++ b/src/config.py @@ -44,8 +44,17 @@ class EquipmentTableRows(Enum): class EmissionTableRows(Enum): """Row configurations for emission table view modes.""" - SIMPLE = ("grid_scenario", "gea_grid_region", "emission_type", "year") + SIMPLE = ( + "elec_emission_source", + "elec_avg_emission_rate_gCO2e_per_kWh", + "grid_scenario", + "gea_grid_region", + "emission_type", + "year", + ) ADVANCED = ( + "elec_emission_source", + "elec_avg_emission_rate_gCO2e_per_kWh", "grid_scenario", "gea_grid_region", "emission_type", @@ -59,6 +68,8 @@ class EmissionTableRows(Enum): class EmissionScenarioDefaults(Enum): """Default values for emission scenario parameters.""" + ELEC_EMISSION_SOURCE: str = "Marginal" + ELEC_AVG_EMISSION_RATE_G_KWH: float = None YEAR: int = 2025 REFRIGERANT_LEAKAGE: float = 0.02 EMISSION_TYPE: str = "Includes pre-combustion" diff --git a/src/emissions.py b/src/emissions.py index 2c39029..52a4ffe 100644 --- a/src/emissions.py +++ b/src/emissions.py @@ -10,6 +10,8 @@ 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 gea_grid_region: str | None = None time_zone: str diff --git a/src/energy.py b/src/energy.py index c56ebb5..15502f0 100644 --- a/src/energy.py +++ b/src/energy.py @@ -1163,11 +1163,10 @@ def site_to_source( base[Col.HOUR.value] = base.index.hour base[Col.DOY.value] = base.index.dayofyear - elec_emissions_source = "average" # em_scen.elec_emissions_source - if elec_emissions_source == "marginal": # replace direct with em_scen.elec_emissions_source + if em_scen.elec_emission_source == "Marginal": emissions_data = get_emissions_data(metadata[em_scen_id]) logger.debug( - f"Marginal electricity emissions: Loaded {len(emissions_data.df)} emission data rows" + f"Marginal grid emissions: Loaded {len(emissions_data.df)} emission data rows" ) # collapse emissions to month-hour averages @@ -1227,15 +1226,18 @@ def site_to_source( # used with non-leap emission scenario years. Emissions are still correct # because they're matched by month+hour pattern. - elif elec_emissions_source == "average": - # elec_emissions_rate = float(em_scen.elec_emission_rate_gCO2e_per_kWh) - elec_emissions_rate = 500 - logger.debug(f"Average electricity emissions: {elec_emissions_rate} gCO2e/kWh") + elif em_scen.elec_emission_source == "Average": + elec_avg_emission_rate = em_scen.elec_avg_emission_rate_gCO2e_per_kWh + if elec_avg_emission_rate is not None: + logger.debug(f"Average grid emissions: {elec_avg_emission_rate} gCO2e/kWh") + + 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 average grid emissions rate.") - merged = base.copy() - merged[Col.ELEC_EMISSIONS_RATE_G_PER_KWH] = elec_emissions_rate else: - raise ValueError(f"Invalid elec_emissions_source: {em_scen.elec_emissions_source}") + raise ValueError(f"Invalid elec_emissions_source: {em_scen.elec_emission_source}") ## fuel switching logic emissions_intensity_HP = ( diff --git a/src/metadata.py b/src/metadata.py index 76740e9..4b36e68 100644 --- a/src/metadata.py +++ b/src/metadata.py @@ -97,6 +97,8 @@ def create(cls, **overrides: Any) -> "Metadata": EmissionScenario( em_scen_id="em_scenario_a", em_scen_name="2025", + elec_emission_source="Marginal", + elec_avg_emission_rate_gCO2e_per_kWh=None, grid_scenario="MidCase", gea_grid_region=None, time_zone="America/Los_Angeles", @@ -109,6 +111,8 @@ def create(cls, **overrides: Any) -> "Metadata": EmissionScenario( em_scen_id="em_scenario_b", em_scen_name="2035", + elec_emission_source="Marginal", + elec_avg_emission_rate_gCO2e_per_kWh=None, grid_scenario="MidCase", gea_grid_region=None, time_zone="America/Los_Angeles", @@ -121,6 +125,8 @@ def create(cls, **overrides: Any) -> "Metadata": EmissionScenario( em_scen_id="em_scenario_c", em_scen_name="2045", + elec_emission_source="Marginal", + elec_avg_emission_rate_gCO2e_per_kWh=None, grid_scenario="MidCase", gea_grid_region=None, time_zone="America/Los_Angeles", diff --git a/tests/conftest.py b/tests/conftest.py index 80ec79b..511abb3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -54,6 +54,8 @@ def emission_scenario(): """Create a sample emission scenario for testing.""" return EmissionScenario( em_scen_id="test_em_1", + elec_emission_source="Marginal", + elec_avg_emission_rate_gCO2e_per_kWh=None, grid_scenario="MidCase", gea_grid_region="CAMX", time_zone="America/Los_Angeles", diff --git a/utils/units.py b/utils/units.py index 707945f..9efe09d 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/kBTU", "func": lambda x: x * g_to_lb / Wh_to_BTU}, + "IP": {"unit": "lb CO₂e/kWh", "func": lambda x: x * g_to_lb}, }, # --- Gas emission factor --- "gas_emission_factor": { From 3490220eab796d0643ae70eabf05f6d709beda28 Mon Sep 17 00:00:00 2001 From: urwahah Date: Mon, 7 Sep 2026 16:31:20 -0700 Subject: [PATCH 03/11] add scenario group --- pages/emissions_page.py | 58 ++++++++++++++++++++++++++++++++++++----- src/config.py | 1 + 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/pages/emissions_page.py b/pages/emissions_page.py index 64fe92f..7e23c0c 100644 --- a/pages/emissions_page.py +++ b/pages/emissions_page.py @@ -102,6 +102,10 @@ def layout(): "label": "Combustion vs pre-combustion", "value": "emission_types", }, + { + "label": "Average vs marginal emissions", + "value": "emission_sources", + }, ], value=None, placeholder="Select a scenario group", @@ -237,6 +241,7 @@ 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 for emissions rate and short-run weighting, others reset to defaults """ # When dropdown is cleared, clear the stored group to allow re-selecting if not group_id: @@ -259,6 +264,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 @@ -266,6 +273,7 @@ def handle_emission_group_selection(group_id, metadata_data, selected_ids, store default_year = EmissionScenarioDefaults.YEAR.value default_leakage = EmissionScenarioDefaults.REFRIGERANT_LEAKAGE.value default_emission_type = EmissionScenarioDefaults.EMISSION_TYPE.value + default_shortrun_weighting = EmissionScenarioDefaults.SHORTRUN_WEIGHTING.value default_ng_emission_rate = EmissionScenarioDefaults.NG_EMISSION_RATE_G_KWH.value # Define the variation values and default IDs @@ -276,6 +284,15 @@ 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", "Average", "Marginal", "Marginal"] + elec_avg_emission_rate_values = [400, 0, None, None] + shortun_weighting_values = [0, 0, 1, 0] + emission_sources_names = [ + "Average Emissions (Typical)", + "Average Emissions (Zero)", + "Marginal Emissions (Short-run)", + "Marginal Emissions (Long-run)", + ] # Create base scenario template from first existing scenario or defaults base_scenario = ( @@ -288,14 +305,14 @@ def handle_emission_group_selection(group_id, metadata_data, selected_ids, store "gea_grid_region": None, "time_zone": "America/Los_Angeles", "emission_type": default_emission_type, - "shortrun_weighting": 0, + "shortrun_weighting": default_shortrun_weighting, "annual_refrig_leakage_percent": default_leakage, "ng_emission_rate_gCO2e_per_kWh": default_ng_emission_rate, "year": default_year, } ) - # Reset to default 2/3 scenarios (a, b, optionally c) with group-specific values + # Reset to default 2/3/4 scenarios (a, b, optionally c & d) 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": ""} @@ -303,27 +320,56 @@ 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["em_scen_name"] = str(scen["year"]) + scen["elec_emission_source"] = default_elec_emission_source + scen["elec_avg_emission_rate_gCO2e_per_kWh"] = default_elec_avg_emission_rate + scen["shortrun_weighting"] = default_shortrun_weighting 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 - pct_leakage = scen["annual_refrig_leakage_percent"] * 100 - scen["em_scen_name"] = f"{pct_leakage:.0f}% leakage" + scen["elec_emission_source"] = default_elec_emission_source + scen["elec_avg_emission_rate_gCO2e_per_kWh"] = default_elec_avg_emission_rate + scen["shortrun_weighting"] = default_shortrun_weighting 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["em_scen_name"] = scen["emission_type"] + scen["elec_emission_source"] = default_elec_emission_source + scen["elec_avg_emission_rate_gCO2e_per_kWh"] = default_elec_avg_emission_rate + scen["shortrun_weighting"] = default_shortrun_weighting + 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["shortrun_weighting"] = shortun_weighting_values[ + idx % len(shortun_weighting_values) + ] + scen["em_scen_name"] = emission_sources_names[idx % len(emission_sources_names)] + + 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 updated_scenarios.append(scen) diff --git a/src/config.py b/src/config.py index f9cceae..de1f2f1 100644 --- a/src/config.py +++ b/src/config.py @@ -73,6 +73,7 @@ class EmissionScenarioDefaults(Enum): YEAR: int = 2025 REFRIGERANT_LEAKAGE: float = 0.02 EMISSION_TYPE: str = "Includes pre-combustion" + SHORTRUN_WEIGHTING: float = 0.0 NG_EMISSION_RATE_G_KWH: float = 239.2 NG_EMISSION_RATE_G_KWH_COMBUSTION: float = 181 From 82fe77564765a25a52998432a5d8e2d323f2f7a3 Mon Sep 17 00:00:00 2001 From: urwahah Date: Mon, 7 Sep 2026 16:35:11 -0700 Subject: [PATCH 04/11] expose emission scenario name --- layout/input.py | 1 + src/config.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/layout/input.py b/layout/input.py index 5e5842b..3b3b2b1 100644 --- a/layout/input.py +++ b/layout/input.py @@ -1128,6 +1128,7 @@ def build_emissions_table(emission_data, active_ids=None, view_mode="simple", un # 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", diff --git a/src/config.py b/src/config.py index de1f2f1..709c19c 100644 --- a/src/config.py +++ b/src/config.py @@ -45,6 +45,7 @@ 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", @@ -53,6 +54,7 @@ class EmissionTableRows(Enum): "year", ) ADVANCED = ( + "em_scen_name", "elec_emission_source", "elec_avg_emission_rate_gCO2e_per_kWh", "grid_scenario", From 2793c95b5c7504457ffb1eee192236e398e47d6c Mon Sep 17 00:00:00 2001 From: urwahah Date: Tue, 8 Sep 2026 20:06:28 -0700 Subject: [PATCH 05/11] revise emission source options, remove shortrun weighting input --- data/input/metadata_index.json | 6 +++-- layout/input.py | 8 ------ pages/emissions_page.py | 47 +++++++++++----------------------- src/config.py | 4 +-- src/emissions.py | 1 - src/energy.py | 14 +++++++--- src/metadata.py | 9 +++---- tests/conftest.py | 3 +-- 8 files changed, 34 insertions(+), 58 deletions(-) diff --git a/data/input/metadata_index.json b/data/input/metadata_index.json index d3c3ddf..cc1b7db 100644 --- a/data/input/metadata_index.json +++ b/data/input/metadata_index.json @@ -64,8 +64,10 @@ }, "emissions": { "elec_emission_source" : [ - "Marginal", - "Average" + "Marginal (Cambium, Long-run)", + "Marginal (Cambium, Short-run)", + "Average (Cambium)", + "Average (User-provided)" ], "year": [ 2025, diff --git a/layout/input.py b/layout/input.py index 3b3b2b1..6106406 100644 --- a/layout/input.py +++ b/layout/input.py @@ -1137,7 +1137,6 @@ def build_emissions_table(emission_data, active_ids=None, view_mode="simple", un ("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", f"Gas emissions rate ({ng_emission_rate_unit})"), ("year", "Year"), @@ -1937,13 +1936,6 @@ def _options(values): 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 7e23c0c..b42fba8 100644 --- a/pages/emissions_page.py +++ b/pages/emissions_page.py @@ -241,7 +241,7 @@ 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 for emissions rate and short-run weighting, 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: @@ -273,7 +273,6 @@ def handle_emission_group_selection(group_id, metadata_data, selected_ids, store default_year = EmissionScenarioDefaults.YEAR.value default_leakage = EmissionScenarioDefaults.REFRIGERANT_LEAKAGE.value default_emission_type = EmissionScenarioDefaults.EMISSION_TYPE.value - default_shortrun_weighting = EmissionScenarioDefaults.SHORTRUN_WEIGHTING.value default_ng_emission_rate = EmissionScenarioDefaults.NG_EMISSION_RATE_G_KWH.value # Define the variation values and default IDs @@ -284,14 +283,18 @@ 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", "Average", "Marginal", "Marginal"] - elec_avg_emission_rate_values = [400, 0, None, None] - shortun_weighting_values = [0, 0, 1, 0] + elec_emission_source_values = [ + "Average (Cambium)", + "Average (User-provided)", + "Marginal (Cambium, Long-run)", + "Marginal (Cambium, Short-run)", + ] + elec_avg_emission_rate_values = [None, 0, None, None] emission_sources_names = [ - "Average Emissions (Typical)", + "Average Emissions (Cambium)", "Average Emissions (Zero)", - "Marginal Emissions (Short-run)", - "Marginal Emissions (Long-run)", + "Marginal Emissions (Cambium Long-run)", + "Marginal Emissions (Cambium Short-run)", ] # Create base scenario template from first existing scenario or defaults @@ -305,7 +308,6 @@ def handle_emission_group_selection(group_id, metadata_data, selected_ids, store "gea_grid_region": None, "time_zone": "America/Los_Angeles", "emission_type": default_emission_type, - "shortrun_weighting": default_shortrun_weighting, "annual_refrig_leakage_percent": default_leakage, "ng_emission_rate_gCO2e_per_kWh": default_ng_emission_rate, "year": default_year, @@ -327,7 +329,6 @@ def handle_emission_group_selection(group_id, metadata_data, selected_ids, store 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["shortrun_weighting"] = default_shortrun_weighting elif group_id == "refrigerant_leakage": # Vary leakage, reset others to defaults scen["annual_refrig_leakage_percent"] = leakage_values[idx % len(leakage_values)] @@ -339,7 +340,6 @@ def handle_emission_group_selection(group_id, metadata_data, selected_ids, store 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["shortrun_weighting"] = default_shortrun_weighting elif group_id == "emission_types": # Set emission type, reset others to defaults scen["emission_type"] = emission_types[idx % len(emission_types)] @@ -352,7 +352,6 @@ def handle_emission_group_selection(group_id, metadata_data, selected_ids, store 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 - scen["shortrun_weighting"] = default_shortrun_weighting 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[ @@ -361,9 +360,6 @@ def handle_emission_group_selection(group_id, metadata_data, selected_ids, store scen["elec_avg_emission_rate_gCO2e_per_kWh"] = elec_avg_emission_rate_values[ idx % len(elec_avg_emission_rate_values) ] - scen["shortrun_weighting"] = shortun_weighting_values[ - idx % len(shortun_weighting_values) - ] scen["em_scen_name"] = emission_sources_names[idx % len(emission_sources_names)] scen["year"] = default_year @@ -613,7 +609,6 @@ def remove_emission_scenario(remove_clicks, metadata_data, selected_em_ids): 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"), @@ -625,7 +620,7 @@ def remove_emission_scenario(remove_clicks, metadata_data, selected_em_ids): ) def open_edit_emission_modal(edit_clicks, metadata_data, unit_mode): if not any(edit_clicks or []): - return (no_update,) * 14 + return (no_update,) * 13 if not metadata_data or "emission_settings" not in metadata_data: return ( @@ -641,7 +636,6 @@ def open_edit_emission_modal(edit_clicks, metadata_data, unit_mode): None, None, None, - None, "No emission data.", ) @@ -649,7 +643,7 @@ def open_edit_emission_modal(edit_clicks, metadata_data, unit_mode): triggered = callback_context.triggered if not triggered: - return (no_update,) * 14 + return (no_update,) * 13 prop_id = triggered[0]["prop_id"] id_str = prop_id.split(".")[0] @@ -670,7 +664,6 @@ def open_edit_emission_modal(edit_clicks, metadata_data, unit_mode): None, None, None, - None, "Failed to parse button id.", ) @@ -691,7 +684,6 @@ def open_edit_emission_modal(edit_clicks, metadata_data, unit_mode): None, None, None, - None, f"Scenario {em_scen_id!r} not found.", ) @@ -733,7 +725,6 @@ def open_edit_emission_modal(edit_clicks, metadata_data, unit_mode): 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, @@ -754,7 +745,6 @@ def open_edit_emission_modal(edit_clicks, metadata_data, unit_mode): 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"), @@ -772,7 +762,6 @@ def save_edit_emission( gea_grid_region, time_zone, emission_type, - shortrun_weighting, year, refrig_leakage, ng_emission_rate, @@ -789,11 +778,6 @@ 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): @@ -847,7 +831,6 @@ def save_edit_emission( 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 @@ -1085,7 +1068,7 @@ def update_ng_rate_on_emission_type_change(emission_type, unit_mode): prevent_initial_call=True, ) def update_avg_emission_on_source_change(emission_source): - """Enable/disable average grid emissions input when emission source changes.""" - disable_avg = emission_source != "Average" + """Enable/disable fixed average grid emissions input when emission source changes.""" + disable_avg = emission_source != "Average (User-provided)" return disable_avg diff --git a/src/config.py b/src/config.py index 709c19c..b8cc134 100644 --- a/src/config.py +++ b/src/config.py @@ -60,7 +60,6 @@ class EmissionTableRows(Enum): "grid_scenario", "gea_grid_region", "emission_type", - "shortrun_weighting", "annual_refrig_leakage_percent", "ng_emission_rate_gCO2e_per_kWh", "year", @@ -70,12 +69,11 @@ class EmissionTableRows(Enum): class EmissionScenarioDefaults(Enum): """Default values for emission scenario parameters.""" - ELEC_EMISSION_SOURCE: str = "Marginal" + 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" - SHORTRUN_WEIGHTING: float = 0.0 NG_EMISSION_RATE_G_KWH: float = 239.2 NG_EMISSION_RATE_G_KWH_COMBUSTION: float = 181 diff --git a/src/emissions.py b/src/emissions.py index 52a4ffe..269cbf9 100644 --- a/src/emissions.py +++ b/src/emissions.py @@ -16,7 +16,6 @@ class EmissionScenario(DotAccessMixin, BaseModel): 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 diff --git a/src/energy.py b/src/energy.py index 15502f0..775aa3e 100644 --- a/src/energy.py +++ b/src/energy.py @@ -1150,8 +1150,6 @@ def site_to_source( ) 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) @@ -1163,12 +1161,20 @@ def site_to_source( base[Col.HOUR.value] = base.index.hour base[Col.DOY.value] = base.index.dayofyear - if em_scen.elec_emission_source == "Marginal": + if em_scen.elec_emission_source in [ + "Marginal (Cambium, Long-run)", + "Marginal (Cambium, Short-run)", + ]: emissions_data = get_emissions_data(metadata[em_scen_id]) logger.debug( f"Marginal grid emissions: Loaded {len(emissions_data.df)} emission data rows" ) + 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 + # 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 @@ -1226,7 +1232,7 @@ def site_to_source( # 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 == "Average": + elif em_scen.elec_emission_source == "Average (User-provided)": elec_avg_emission_rate = em_scen.elec_avg_emission_rate_gCO2e_per_kWh if elec_avg_emission_rate is not None: logger.debug(f"Average grid emissions: {elec_avg_emission_rate} gCO2e/kWh") diff --git a/src/metadata.py b/src/metadata.py index 4b36e68..591701c 100644 --- a/src/metadata.py +++ b/src/metadata.py @@ -97,13 +97,12 @@ def create(cls, **overrides: Any) -> "Metadata": EmissionScenario( em_scen_id="em_scenario_a", em_scen_name="2025", - elec_emission_source="Marginal", + 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, @@ -111,13 +110,12 @@ def create(cls, **overrides: Any) -> "Metadata": EmissionScenario( em_scen_id="em_scenario_b", em_scen_name="2035", - elec_emission_source="Marginal", + 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, @@ -125,13 +123,12 @@ def create(cls, **overrides: Any) -> "Metadata": EmissionScenario( em_scen_id="em_scenario_c", em_scen_name="2045", - elec_emission_source="Marginal", + 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 511abb3..9f00bb8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -54,13 +54,12 @@ def emission_scenario(): """Create a sample emission scenario for testing.""" return EmissionScenario( em_scen_id="test_em_1", - elec_emission_source="Marginal", + 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, From 6c145a0cf4c780dad58881dfa86d3e2c7c74fecb Mon Sep 17 00:00:00 2001 From: urwahah Date: Tue, 8 Sep 2026 20:43:01 -0700 Subject: [PATCH 06/11] add cambium average emissions calculation --- pages/emissions_page.py | 8 +-- src/config.py | 3 ++ src/emissions.py | 16 +++++- src/energy.py | 112 +++++++++++++++++++++++++--------------- utils/units.py | 3 ++ 5 files changed, 94 insertions(+), 48 deletions(-) diff --git a/pages/emissions_page.py b/pages/emissions_page.py index b42fba8..650f15a 100644 --- a/pages/emissions_page.py +++ b/pages/emissions_page.py @@ -291,10 +291,10 @@ def handle_emission_group_selection(group_id, metadata_data, selected_ids, store ] elec_avg_emission_rate_values = [None, 0, None, None] emission_sources_names = [ - "Average Emissions (Cambium)", - "Average Emissions (Zero)", - "Marginal Emissions (Cambium Long-run)", - "Marginal Emissions (Cambium Short-run)", + "Cambium Average Emissions", + "Zero Emissions", + "Long-run Marginal Emissions", + "Short-run Marginal Emissions", ] # Create base scenario template from first existing scenario or defaults diff --git a/src/config.py b/src/config.py index b8cc134..f79c319 100644 --- a/src/config.py +++ b/src/config.py @@ -170,6 +170,9 @@ 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 269cbf9..2e3345d 100644 --- a/src/emissions.py +++ b/src/emissions.py @@ -25,7 +25,7 @@ class StandardEmissions: """ Unified interface for emissions data. Canonical schema: - emission_scenario | gea_grid_region | time_zone | year | timestamp | lrmer_co2e_c | lrmer_co2e_p | srmer_co2e_c | srmer_co2e_p + emission_scenario | gea_grid_region | time_zone | year | timestamp | lrmer_co2e_c | lrmer_co2e_p | srmer_co2e_c | srmer_co2e_p | aer_load_co2e_c | aer_load_co2e_p """ def __init__(self, df: pd.DataFrame): @@ -43,6 +43,8 @@ def _validate(df: pd.DataFrame) -> pd.DataFrame: "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,7 +57,14 @@ 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"]: + for col in [ + "lrmer_co2e_c", + "lrmer_co2e_p", + "srmer_co2e_c", + "srmer_co2e_p", + "aer_load_co2e_c", + "aer_load_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}") @@ -118,6 +127,9 @@ 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 775aa3e..cb4a115 100644 --- a/src/energy.py +++ b/src/energy.py @@ -1164,61 +1164,89 @@ def site_to_source( 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"Marginal grid emissions: Loaded {len(emissions_data.df)} emission data rows" + f"Cambium grid emissions: Loaded {len(emissions_data.df)} emission data rows" ) - 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 - # 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] - ) - * (1 - shortrun_weighting) - ) + ( - ( - emissions_data.df[Col.SRMER_CO2E_C.value] - + emissions_data.df[Col.SRMER_CO2E_P.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] ) - * 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.AER_LOAD_CO2E_C.value, + Col.AER_LOAD_CO2E_P.value, + Col.AER_LOAD_CO2E.value, + ] + ] + .mean() + .reset_index() ) 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, + 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() - ) + .mean() + .reset_index() + ) # expand loads with this year's emissions merged = base.merge(df_em, on=[Col.MONTH.value, Col.HOUR.value], how="left") @@ -1235,7 +1263,7 @@ def site_to_source( elif em_scen.elec_emission_source == "Average (User-provided)": elec_avg_emission_rate = em_scen.elec_avg_emission_rate_gCO2e_per_kWh if elec_avg_emission_rate is not None: - logger.debug(f"Average grid emissions: {elec_avg_emission_rate} gCO2e/kWh") + logger.debug(f"Fixed average grid emissions: {elec_avg_emission_rate} gCO2e/kWh") merged = base.copy() merged[Col.ELEC_EMISSIONS_RATE_G_PER_KWH] = float(elec_avg_emission_rate) diff --git a/utils/units.py b/utils/units.py index 9efe09d..7ad45be 100644 --- a/utils/units.py +++ b/utils/units.py @@ -298,6 +298,8 @@ 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"), @@ -327,6 +329,7 @@ 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 === From c74b2b7a64530d5e4814304e91440721fc5128d0 Mon Sep 17 00:00:00 2001 From: urwahah Date: Wed, 9 Sep 2026 11:09:40 -0700 Subject: [PATCH 07/11] add 4th emission scenario to chart --- app.py | 4 ++-- layout/charts.py | 2 +- 2 files changed, 3 insertions(+), 3 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/layout/charts.py b/layout/charts.py index 2e9704b..523ce35 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"], + value=["em_scenario_a", "em_scenario_b", "em_scenario_c", "em_scenario_d"], placeholder="Emission Scenarios", searchable=True, clearable=True, From 6fca317cc72d1d4507665cbdc1d6700c7efd2dd2 Mon Sep 17 00:00:00 2001 From: urwahah Date: Wed, 9 Sep 2026 11:13:17 -0700 Subject: [PATCH 08/11] revise constant source name, add check for positive rate --- data/input/metadata_index.json | 2 +- pages/emissions_page.py | 6 +++--- src/energy.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/data/input/metadata_index.json b/data/input/metadata_index.json index cc1b7db..79590e3 100644 --- a/data/input/metadata_index.json +++ b/data/input/metadata_index.json @@ -67,7 +67,7 @@ "Marginal (Cambium, Long-run)", "Marginal (Cambium, Short-run)", "Average (Cambium)", - "Average (User-provided)" + "Average (Constant)" ], "year": [ 2025, diff --git a/pages/emissions_page.py b/pages/emissions_page.py index 650f15a..6deb5cd 100644 --- a/pages/emissions_page.py +++ b/pages/emissions_page.py @@ -285,14 +285,14 @@ def handle_emission_group_selection(group_id, metadata_data, selected_ids, store ] elec_emission_source_values = [ "Average (Cambium)", - "Average (User-provided)", + "Average (Constant)", "Marginal (Cambium, Long-run)", "Marginal (Cambium, Short-run)", ] elec_avg_emission_rate_values = [None, 0, None, None] emission_sources_names = [ "Cambium Average Emissions", - "Zero Emissions", + "Constant (Zero) Emissions", "Long-run Marginal Emissions", "Short-run Marginal Emissions", ] @@ -1069,6 +1069,6 @@ def update_ng_rate_on_emission_type_change(emission_type, unit_mode): ) def update_avg_emission_on_source_change(emission_source): """Enable/disable fixed average grid emissions input when emission source changes.""" - disable_avg = emission_source != "Average (User-provided)" + disable_avg = emission_source != "Average (Constant)" return disable_avg diff --git a/src/energy.py b/src/energy.py index cb4a115..9b376d2 100644 --- a/src/energy.py +++ b/src/energy.py @@ -1260,9 +1260,9 @@ def site_to_source( # 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 == "Average (User-provided)": + elif em_scen.elec_emission_source == "Average (Constant)": elec_avg_emission_rate = em_scen.elec_avg_emission_rate_gCO2e_per_kWh - if elec_avg_emission_rate is not None: + if elec_avg_emission_rate is not None and elec_avg_emission_rate >= 0: logger.debug(f"Fixed average grid emissions: {elec_avg_emission_rate} gCO2e/kWh") merged = base.copy() From 34b083d73476442ff41699c6c028cbeef81dc366 Mon Sep 17 00:00:00 2001 From: urwahah Date: Wed, 9 Sep 2026 11:32:05 -0700 Subject: [PATCH 09/11] remove timezone input from emissions data/scenarios --- layout/input.py | 12 ------------ pages/emissions_page.py | 13 ++----------- src/emissions.py | 5 +---- src/metadata.py | 3 --- tests/conftest.py | 1 - 5 files changed, 3 insertions(+), 31 deletions(-) diff --git a/layout/input.py b/layout/input.py index 6106406..e249472 100644 --- a/layout/input.py +++ b/layout/input.py @@ -1916,12 +1916,6 @@ 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", @@ -1930,12 +1924,6 @@ def _options(values): searchable=False, clearable=False, ), - ], - ), - dmc.SimpleGrid( - cols=2, - spacing="md", - children=[ dmc.Select( id="edit-em-year", label="Year", diff --git a/pages/emissions_page.py b/pages/emissions_page.py index 6deb5cd..d003207 100644 --- a/pages/emissions_page.py +++ b/pages/emissions_page.py @@ -306,7 +306,6 @@ def handle_emission_group_selection(group_id, metadata_data, selected_ids, store "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, "annual_refrig_leakage_percent": default_leakage, "ng_emission_rate_gCO2e_per_kWh": default_ng_emission_rate, @@ -607,7 +606,6 @@ def remove_emission_scenario(remove_clicks, metadata_data, selected_em_ids): 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-year", "value"), Output("edit-em-refrig-leakage", "value"), @@ -620,7 +618,7 @@ def remove_emission_scenario(remove_clicks, metadata_data, selected_em_ids): ) def open_edit_emission_modal(edit_clicks, metadata_data, unit_mode): if not any(edit_clicks or []): - return (no_update,) * 13 + return (no_update,) * 12 if not metadata_data or "emission_settings" not in metadata_data: return ( @@ -632,7 +630,6 @@ def open_edit_emission_modal(edit_clicks, metadata_data, unit_mode): "", "", "", - "", None, None, None, @@ -643,7 +640,7 @@ def open_edit_emission_modal(edit_clicks, metadata_data, unit_mode): triggered = callback_context.triggered if not triggered: - return (no_update,) * 13 + return (no_update,) * 12 prop_id = triggered[0]["prop_id"] id_str = prop_id.split(".")[0] @@ -660,7 +657,6 @@ def open_edit_emission_modal(edit_clicks, metadata_data, unit_mode): "", "", "", - "", None, None, None, @@ -680,7 +676,6 @@ def open_edit_emission_modal(edit_clicks, metadata_data, unit_mode): "", "", "", - "", None, None, None, @@ -723,7 +718,6 @@ def open_edit_emission_modal(edit_clicks, metadata_data, unit_mode): elec_avg_emission_rate_display, scen.get("grid_scenario", ""), scen.get("gea_grid_region", ""), - scen.get("time_zone", ""), scen.get("emission_type", ""), str(scen.get("year")) if scen.get("year") is not None else "", refrig_leakage, @@ -743,7 +737,6 @@ def open_edit_emission_modal(edit_clicks, metadata_data, unit_mode): 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-year", "value"), State("edit-em-refrig-leakage", "value"), @@ -760,7 +753,6 @@ def save_edit_emission( elec_avg_emission_rate, grid_scenario, gea_grid_region, - time_zone, emission_type, year, refrig_leakage, @@ -829,7 +821,6 @@ def save_edit_emission( 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["year"] = year new_scen["annual_refrig_leakage_percent"] = refrig_leakage diff --git a/src/emissions.py b/src/emissions.py index 2e3345d..cd9269d 100644 --- a/src/emissions.py +++ b/src/emissions.py @@ -14,7 +14,6 @@ class EmissionScenario(DotAccessMixin, BaseModel): elec_avg_emission_rate_gCO2e_per_kWh: float | None = None grid_scenario: str gea_grid_region: str | None = None - time_zone: str emission_type: str annual_refrig_leakage_percent: float ng_emission_rate_gCO2e_per_kWh: float @@ -25,7 +24,7 @@ class StandardEmissions: """ Unified interface for emissions data. Canonical schema: - emission_scenario | gea_grid_region | time_zone | 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 | year | timestamp | lrmer_co2e_c | lrmer_co2e_p | srmer_co2e_c | srmer_co2e_p | aer_load_co2e_c | aer_load_co2e_p """ def __init__(self, df: pd.DataFrame): @@ -37,7 +36,6 @@ def _validate(df: pd.DataFrame) -> pd.DataFrame: "emission_scenario", "gea_grid_region", "year", - "time_zone", "timestamp", "lrmer_co2e_c", "lrmer_co2e_p", @@ -117,7 +115,6 @@ 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"], diff --git a/src/metadata.py b/src/metadata.py index 591701c..f4ca6e4 100644 --- a/src/metadata.py +++ b/src/metadata.py @@ -101,7 +101,6 @@ def create(cls, **overrides: Any) -> "Metadata": 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", annual_refrig_leakage_percent=0.05, ng_emission_rate_gCO2e_per_kWh=239.2, @@ -114,7 +113,6 @@ def create(cls, **overrides: Any) -> "Metadata": 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", annual_refrig_leakage_percent=0.05, ng_emission_rate_gCO2e_per_kWh=239.2, @@ -127,7 +125,6 @@ def create(cls, **overrides: Any) -> "Metadata": 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", annual_refrig_leakage_percent=0.05, ng_emission_rate_gCO2e_per_kWh=239.2, diff --git a/tests/conftest.py b/tests/conftest.py index 9f00bb8..953b9c2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -58,7 +58,6 @@ def emission_scenario(): 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", annual_refrig_leakage_percent=0.05, ng_emission_rate_gCO2e_per_kWh=239.2, From 669fea9072305fe7eaccf90592ce545f8afee555 Mon Sep 17 00:00:00 2001 From: urwahah Date: Wed, 9 Sep 2026 11:44:03 -0700 Subject: [PATCH 10/11] blank cambium grid inputs when set to constant emissions --- pages/emissions_page.py | 17 ++++++++++++++--- src/emissions.py | 2 +- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/pages/emissions_page.py b/pages/emissions_page.py index d003207..c547922 100644 --- a/pages/emissions_page.py +++ b/pages/emissions_page.py @@ -1055,11 +1055,22 @@ def update_ng_rate_on_emission_type_change(emission_type, unit_mode): @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_avg_emission_on_source_change(emission_source): - """Enable/disable fixed average grid emissions input when emission source changes.""" +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 != "Average (Constant)" - return disable_avg + # 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/emissions.py b/src/emissions.py index cd9269d..dd286e8 100644 --- a/src/emissions.py +++ b/src/emissions.py @@ -12,7 +12,7 @@ class EmissionScenario(DotAccessMixin, BaseModel): em_scen_name: str elec_emission_source: str elec_avg_emission_rate_gCO2e_per_kWh: float | None = None - grid_scenario: str + grid_scenario: str | None = None gea_grid_region: str | None = None emission_type: str annual_refrig_leakage_percent: float From 483da4fb37e0834974beb371199eb79cbde9d770 Mon Sep 17 00:00:00 2001 From: urwahah Date: Wed, 9 Sep 2026 15:03:33 -0700 Subject: [PATCH 11/11] add "constant" to fixed emission factor name --- data/input/metadata_index.json | 2 +- layout/input.py | 4 ++-- pages/emissions_page.py | 11 +++++++---- src/energy.py | 6 +++--- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/data/input/metadata_index.json b/data/input/metadata_index.json index 79590e3..76a81fe 100644 --- a/data/input/metadata_index.json +++ b/data/input/metadata_index.json @@ -67,7 +67,7 @@ "Marginal (Cambium, Long-run)", "Marginal (Cambium, Short-run)", "Average (Cambium)", - "Average (Constant)" + "Constant (User-provided)" ], "year": [ 2025, diff --git a/layout/input.py b/layout/input.py index 35ddd73..3af1f1d 100644 --- a/layout/input.py +++ b/layout/input.py @@ -1192,7 +1192,7 @@ def build_emissions_table(emission_data, active_ids=None, view_mode="simple", un ("elec_emission_source", "Grid emissions source"), ( "elec_avg_emission_rate_gCO2e_per_kWh", - f"Average grid emissions rate ({elec_emission_rate_unit})", + f"Constant grid emissions rate ({elec_emission_rate_unit})", ), ("grid_scenario", "Grid Scenario"), ("gea_grid_region", "GEA Grid Region"), @@ -1939,7 +1939,7 @@ def _options(values): [ dmc.Text( id="edit-em-elec-emission-rate-label", - children="Average grid emissions rate (g/kWh)", + children="Constant grid emissions rate (g/kWh)", size="sm", fw=500, ), diff --git a/pages/emissions_page.py b/pages/emissions_page.py index 1ffad74..d9e9e67 100644 --- a/pages/emissions_page.py +++ b/pages/emissions_page.py @@ -101,7 +101,7 @@ def layout(): "value": "emission_types", }, { - "label": "Average vs marginal emissions", + "label": "Different electricity emission factors", "value": "emission_sources", }, ], @@ -283,7 +283,7 @@ def handle_emission_group_selection(group_id, metadata_data, selected_ids, store ] elec_emission_source_values = [ "Average (Cambium)", - "Average (Constant)", + "Constant (User-provided)", "Marginal (Cambium, Long-run)", "Marginal (Cambium, Short-run)", ] @@ -358,6 +358,9 @@ def handle_emission_group_selection(group_id, metadata_data, selected_ids, store 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 @@ -861,7 +864,7 @@ def update_emissions_rate_label(unit_mode): 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"Average grid emissions rate ({elec_unit})" + return f"Gas emissions rate ({ng_unit})", f"Constant grid emissions rate ({elec_unit})" @callback( @@ -1018,7 +1021,7 @@ def update_emission_inputs_on_source_change(emission_source, grid_scenario, grid """Update emissions inputs when emission source changes to fixed user-provided value.""" # disable average emissions rate input - disable_avg = emission_source != "Average (Constant)" + disable_avg = emission_source != "Constant (User-provided)" # blank out cambium-related inputs if not disable_avg: diff --git a/src/energy.py b/src/energy.py index 9b376d2..cd85c28 100644 --- a/src/energy.py +++ b/src/energy.py @@ -1260,15 +1260,15 @@ def site_to_source( # 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 == "Average (Constant)": + 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"Fixed average grid emissions: {elec_avg_emission_rate} gCO2e/kWh") + logger.debug(f"Constant grid emissions rate: {elec_avg_emission_rate} gCO2e/kWh") 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 average grid emissions rate.") + raise ValueError("Provide a valid input value for constant grid emissions rate.") else: raise ValueError(f"Invalid elec_emissions_source: {em_scen.elec_emission_source}")