Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 53 additions & 3 deletions SWEET_python/city_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -638,8 +656,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]

Expand Down Expand Up @@ -1375,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)

Expand Down Expand Up @@ -4111,6 +4158,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(
Expand Down
23 changes: 23 additions & 0 deletions changelog/2026-07.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions changelog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 45 additions & 0 deletions tests/test_gas_capture_efficiency.py
Original file line number Diff line number Diff line change
@@ -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)