From 28687c788fe5eed456f445da1833ddd2690b50ba Mon Sep 17 00:00:00 2001 From: Hugh Runyan Date: Mon, 6 Jul 2026 13:50:56 -0700 Subject: [PATCH 1/2] Use a city's measured gas-capture efficiency when available (city DST) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The simple city path built the with-capture landfill without a gas_capture_efficiency, so the engine always filled the 0.6 model default and a city's measured "Methane Capture Efficiency (%)" was silently ignored. - load_csv_new now normalizes that column robustly: null -> None (falls back to the model default), a value > 1 is read as a percentage (60 -> 0.60), and a value <= 1 as a fraction (0.25 -> 0.25) — correct whether the source stores 25 or 0.25. Negative/non-numeric -> None. - _calculate_divs passes the measured efficiency (or None) to the with-capture landfill; None still falls back to Landfill's default. model-output-change: cities whose record carries a real capture efficiency other than 60% will now produce different baseline emissions. Almost all rows are null, so most cities are unaffected. Not a breaking change. Co-Authored-By: Claude Opus 4.8 --- SWEET_python/city_params.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/SWEET_python/city_params.py b/SWEET_python/city_params.py index d4e2ddf..443173a 100644 --- a/SWEET_python/city_params.py +++ b/SWEET_python/city_params.py @@ -638,8 +638,30 @@ def load_csv_new(self, db: pd.DataFrame, scenario: int = 0, dst: bool = False) - if non_compostable_not_targeted_total.isna().all(): non_compostable_not_targeted_total = pd.Series(0, index=years) - gas_capture_efficiency = city_data["Methane Capture Efficiency (%)"].values[0] / 100 - gas_capture_efficiency = pd.Series(gas_capture_efficiency, index=years) + def _normalize_capture_efficiency(raw): + # "Methane Capture Efficiency (%)" is usually null, but a few rows + # carry a real value that may be a fraction (0.25) or a percentage + # (25). Return a fraction in [0, 1], or None when there's no usable + # value (the landfill then falls back to the model default). + try: + value = float(raw) + except (TypeError, ValueError): + return None + if pd.isna(value) or value < 0: + return None + if value > 1: # percentage form, e.g. 60 -> 0.60 + value = value / 100.0 + return min(value, 1.0) + + _measured_capture_eff = _normalize_capture_efficiency( + city_data["Methane Capture Efficiency (%)"].values[0] + ) + # None -> leave unset so the with-capture landfill fills the model default. + gas_capture_efficiency = ( + pd.Series(_measured_capture_eff, index=years) + if _measured_capture_eff is not None + else None + ) mef_compost = city_data["MEF: Compost"].values[0] @@ -4111,6 +4133,9 @@ def _calculate_divs(self, advanced_baseline=False, advanced_dst=False) -> None: landfill_index=0, fraction_of_waste=city_parameters.split_fractions.landfill_w_capture, gas_capture=True, + # Use the city's measured capture efficiency when the source data + # provides one; None falls back to the model default in Landfill. + gas_capture_efficiency=city_parameters.gas_capture_efficiency, fraction_of_waste_vector=pd.Series(city_parameters.split_fractions.landfill_w_capture, index=years), ) landfill_wo_capture = Landfill( From 5b7317c330a549f2b8b29f27c23ab660189a3168 Mon Sep 17 00:00:00 2001 From: Hugh Runyan Date: Tue, 7 Jul 2026 07:40:00 -0700 Subject: [PATCH 2/2] City DST uses reported gas-capture efficiency instead of hardcoded 60% MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit load_andre_params (the map-data pipeline's city builder) hardcoded the with-capture landfill's gas-capture efficiency to 0.6 for every city, discarding the gas_capture_efficiency_percent the pipeline already selects from the DB. It now reads that source value (percentage form, e.g. 50 -> 0.50) via a new _normalize_gas_capture_efficiency helper, falling back to the 0.6 model default when the source is null / NaN / non-numeric. The value flows through _calculate_divs into the with-capture landfill and out to cities_for_map's "Methane Capture Efficiency (%)" column (previously a flat 60). model-output-change: cities whose source reports a real efficiency (currently few — most rows are null) shift accordingly. Not a breaking change; downstream contracts unchanged. Adds DB-free regression tests (tests/test_gas_capture_efficiency.py, 13 cases) and a July changelog entry. Co-Authored-By: Claude Opus 4.8 --- SWEET_python/city_params.py | 27 ++++++++++++++++- changelog/2026-07.md | 23 ++++++++++++++ changelog/README.md | 1 + tests/test_gas_capture_efficiency.py | 45 ++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 changelog/2026-07.md create mode 100644 tests/test_gas_capture_efficiency.py diff --git a/SWEET_python/city_params.py b/SWEET_python/city_params.py index 443173a..2152503 100644 --- a/SWEET_python/city_params.py +++ b/SWEET_python/city_params.py @@ -30,6 +30,24 @@ import time +def _normalize_gas_capture_efficiency(raw, default: float = 0.6) -> float: + """Coerce a source gas-capture-efficiency value to a fraction in [0, 1]. + + The source column ``gas_capture_efficiency_percent`` is a percentage, e.g. + ``50`` -> ``0.50``; a value already given as a fraction (``<= 1``) is used + as-is. Missing / NaN / non-numeric -> ``default`` (the model default). + """ + try: + value = float(raw) + except (TypeError, ValueError): + return default + if pd.isna(value): + return default + if value > 1: + value = value / 100.0 + return min(max(value, 0.0), 1.0) + + # The way this model is set up is based on the unit of a City, corresponding to the City class. # Cities can have multiple sets of CityParameters, one for each scenario. # Sets of CityParameters can have one or more landfills, dumpsites, waste to energy, etc. @@ -1397,7 +1415,14 @@ def calculate_component_fractions( if non_compostable_not_targeted_total.isna().all(): non_compostable_not_targeted_total = pd.Series(0, index=years) - gas_capture_efficiency = pd.Series(0.6, index=years) + # Use the city's source-reported gas-capture efficiency when present + # (gas_capture_efficiency_percent, a %). Missing -> model default 0.6. + gas_capture_efficiency = pd.Series( + _normalize_gas_capture_efficiency( + row.get("gas_capture_efficiency_percent") + ), + index=years, + ) waste_mass = pd.Series(waste_mass, index=years) diff --git a/changelog/2026-07.md b/changelog/2026-07.md new file mode 100644 index 0000000..3f234a2 --- /dev/null +++ b/changelog/2026-07.md @@ -0,0 +1,23 @@ +# SWEET_python Changelog — July 2026 + +**Highlights:** The city DST now uses each city's *reported* gas-capture +efficiency instead of assuming 60% for every city. + +## Changed + +- **City DST reads the source gas-capture efficiency.** `City.load_andre_params` + (the builder the map-data pipeline uses) previously hardcoded the with-capture + landfill's gas-capture efficiency to `0.6` for every city, discarding the + `gas_capture_efficiency_percent` value the pipeline already selects from the + database. It now reads that source value — a percentage, e.g. `50` → `0.50` — + and falls back to the `0.6` model default only when the source is missing. + The value flows through `_calculate_divs` into the with-capture landfill and + out to `cities_for_map_*.csv`'s `Methane Capture Efficiency (%)` column (which + was a flat `60` before). Adds a DB-free normalizer helper + (`_normalize_gas_capture_efficiency`, handling percentage-vs-fraction form and + NaN/non-numeric input) and regression tests + (`tests/test_gas_capture_efficiency.py`). + + **Model-output change:** cities whose source data reports a real efficiency + (currently a small number — most rows are null) will see their landfill-methane + estimates shift. Not a breaking change; downstream contracts are unchanged. diff --git a/changelog/README.md b/changelog/README.md index 084c972..d31885e 100644 --- a/changelog/README.md +++ b/changelog/README.md @@ -15,6 +15,7 @@ The project does not publish semantic version tags, so releases are tracked by Newest first: +- [2026-07](2026-07.md) — City DST reads the source gas-capture efficiency instead of hardcoding 60% (model-output change) - [2026-06](2026-06.md) — New single-site and city-level ADST modeling modules, min-cost max-flow rewrite of the city DST diversion allocator, physical-k fix for cold/dry sites, no more spurious negative food-waste mass - [2026-05](2026-05.md) — SDST models from a landfill's actual open year (1950–2050), Central Asia/Afghanistan disposal-default fix, auto-Jira issue tooling, professional-comment cleanup - [2026-04](2026-04.md) — SDST baseline/scenario oxidation hardening against pandas dtype bugs, new PM2.5/PM10 particulate emissions from flared methane diff --git a/tests/test_gas_capture_efficiency.py b/tests/test_gas_capture_efficiency.py new file mode 100644 index 0000000..a99a32b --- /dev/null +++ b/tests/test_gas_capture_efficiency.py @@ -0,0 +1,45 @@ +"""Regression tests for reading the source gas-capture efficiency in the +city DST pipeline path (`City.load_andre_params`). + +Change: `load_andre_params` previously hardcoded the landfill gas-capture +efficiency to 0.6 for every city, discarding the source +`gas_capture_efficiency_percent` value that the map-data pipeline already +selects from the database. It now reads that source value (a percentage, e.g. +50 -> 0.50) via `_normalize_gas_capture_efficiency`, falling back to the 0.6 +model default when the source is missing / NaN / non-numeric. The value flows +through `_calculate_divs` into the with-capture landfill and out to +`cities_for_map_*.csv`'s "Methane Capture Efficiency (%)" column. + +In the live DB this column is almost always null (one city currently reports a +real 50%). These unit tests pin the normalizer's behaviour and are DB-free. +""" + +import pytest + +from SWEET_python.city_params import _normalize_gas_capture_efficiency as normalize + + +@pytest.mark.parametrize( + "raw, expected", + [ + (50, 0.50), # the real source value currently in the DB (percent form) + (50.0, 0.50), + ("50", 0.50), # numeric string (some DB drivers return object dtype) + (25, 0.25), # another percentage + (0.5, 0.50), # already a fraction -> used as-is + (0, 0.0), # an explicit zero is a real reported value + (150, 1.0), # implausible high percent -> clamped to 1.0 + ], +) +def test_source_values_are_normalized(raw, expected): + assert normalize(raw) == pytest.approx(expected) + + +@pytest.mark.parametrize("raw", [None, float("nan"), "", "n/a", "unknown"]) +def test_missing_or_nonnumeric_falls_back_to_default(raw): + # Missing / NaN / non-numeric -> the 0.6 model default (the common case). + assert normalize(raw) == pytest.approx(0.6) + + +def test_custom_default_is_respected(): + assert normalize(None, default=0.45) == pytest.approx(0.45)