Skip to content
Merged
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
12 changes: 12 additions & 0 deletions SWEET_python/city_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -8276,6 +8276,18 @@ def _apply_open_close_window(
waste_masses_df_scenario, "scenario"
)
else:
# Before the implementation year nothing has changed, so the scenario
# landfill must receive exactly the baseline waste -- mass *and*
# composition. The two trace-reconciled branches above already splice
# this (waste_masses_df_scenario.loc[:implement_year-1] =
# waste_masses_df_baseline...); this blank/custom-site branch omitted
# it, so a scenario that changed the waste composition wrongly
# back-dated the new composition onto deposits from before
# implement_year. Mirror the sibling branches (and
# advanced_dst.run_advanced_dst) by forcing baseline pre-implement.
generated_waste_masses_scenario.loc[:implement_year - 1, :] = (
generated_waste_masses_baseline.loc[:implement_year - 1, :]
)
waste_masses_df_baseline = _apply_open_close_window(
generated_waste_masses_baseline, "baseline"
)
Expand Down
21 changes: 20 additions & 1 deletion changelog/2026-08.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
# SWEET_python Changelog — August 2026

**Highlights:** The single-site advanced DST (`advanced_dst`) gains two optional inputs — `depth` and `k_override` — that restore the last two site-DST (`City.sdst_v1_5`) levers the adst model had no equivalent for. Both default to "derive as before," so existing adst calls are byte-for-byte unaffected; they are opt-in and not a model-output change for current callers. Separately, a variable-name bug in `City.sdst_v1_5` was silently discarding the `/sdst` flaring efficiency and forcing flare destruction to the 0.98 default on every run. Sites that set a non-default flaring efficiency now model the value the user supplied. This is a model-output change for any sdst run with a non-0.98 flaring efficiency. Also, the annual model (`estimate_emissions2`) now applies cover oxidation by emission year rather than deposit year, so a mid-life oxidation change (e.g. biocover) affects methane emitted from that year on — fixing biocover having no effect on already-closed landfills (WasteMAP #719). This is a model-output change for scenarios that vary oxidation over time; constant-oxidation runs are unchanged.
**Highlights:** The single-site advanced DST (`advanced_dst`) gains two optional inputs — `depth` and `k_override` — that restore the last two site-DST (`City.sdst_v1_5`) levers the adst model had no equivalent for. Both default to "derive as before," so existing adst calls are byte-for-byte unaffected; they are opt-in and not a model-output change for current callers. Separately, a variable-name bug in `City.sdst_v1_5` was silently discarding the `/sdst` flaring efficiency and forcing flare destruction to the 0.98 default on every run. Sites that set a non-default flaring efficiency now model the value the user supplied. This is a model-output change for any sdst run with a non-0.98 flaring efficiency. Also, the annual model (`estimate_emissions2`) now applies cover oxidation by emission year rather than deposit year, so a mid-life oxidation change (e.g. biocover) affects methane emitted from that year on — fixing biocover having no effect on already-closed landfills (WasteMAP #719). This is a model-output change for scenarios that vary oxidation over time; constant-oxidation runs are unchanged. Finally, `City.sdst_v1_5`'s blank/custom-site path now holds the scenario equal to the baseline for every year before the implementation year even when the scenario changes the waste composition — it was back-dating the new composition onto pre-implementation deposits, so baseline and scenario diverged before the change was even implemented. This is a model-output change only for a composition-changing scenario on the custom-site path; composition-stable runs and every baseline are byte-for-byte unchanged.

## Added
- `AdvancedDSTRequest.depth` (`Optional[Variant[float]]`, metres): a controlled or open dump (landfill type 1 or 2) deeper than 5 m has its methane correction factor raised to 0.8, matching `City.sdst_v1_5`'s deep-dump rule (deep dumps decompose more anaerobically). Implemented in `dst_common.mcf_series`, which now accepts optional per-variant `baseline_depth`/`scenario_depth`; a `None` depth (the default) leaves MCF at the per-type value, and the rule never applies to engineered landfills (type 0). Restores the `/sdst` "Depth" control for adst. ([#40](https://github.com/RMI/SWEET_python/pull/40))
- `AdvancedDSTRequest.k_override` (`Optional[Variant[YearlyFloat]]`): a per-year decomposition rate `k` that, when supplied, is applied uniformly to every biodegradable component and bypasses the derived (temperature/precipitation/composition) k — mirroring `City.sdst_v1_5`'s `ks_overrides`. Implemented via the new `dst_common.uniform_decomposition_rates` helper; spliced at `implement_year` like every other adst scenario input. Restores the `/sdst` "Degradation rate (k)" control for adst. ([#40](https://github.com/RMI/SWEET_python/pull/40))

## Fixed
- `City.sdst_v1_5` now holds the scenario equal to the baseline for every year
**before the implementation year**, even when the scenario changes the waste
composition. The method models the baseline and scenario as two landfills; the
scenario waste-mass series splices the *total mass* at `implement_year` but
applied the *scenario composition* to every year. On the blank/custom-site path
(`baseline_data=None` — a "Custom Location" in `/sdst`) a scenario that changed
composition therefore back-dated the new composition onto deposits from before
the change was implemented, and the two series diverged pre-implement (~6% on
the scenario series in a representative dump→landfill case). The two
TRACE-reconciled branches already spliced the scenario back to the baseline
pre-implement (`waste_masses_df_scenario.loc[:implement_year-1, :] =
waste_masses_df_baseline...`), as do `advanced_dst.run_advanced_dst` and
`advanced_dst_city.run_advanced_dst_city`; only the `else` (custom-site) branch
omitted it. It now applies the same splice, before the open/close window.
**Model-output change:** a composition-changing scenario on the custom-site path
now produces different (correct) results; a scenario that does not change
composition — and every baseline — is byte-for-byte unchanged. Adds
`tests/test_sdst_v1_5_preimplement_composition.py`.
([#47](https://github.com/RMI/SWEET_python/pull/47))
- `model_v2.SWEET.estimate_emissions2` now applies cover oxidation by **emission
year**, not deposit year. The method builds `(deposit_year, emission_year)`
methane matrices and sums over deposit years; gas capture and flaring already
Expand Down
2 changes: 1 addition & 1 deletion changelog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The project does not publish semantic version tags, so releases are tracked by

Newest first:

- [2026-08](2026-08.md) — Single-site adst gains optional `depth` (deep-dump MCF bump) and `k_override` (caller-supplied decomposition rate) inputs, restoring the last two site-DST levers; `/sdst` flaring efficiency reaches the model again after a variable-name bug silently forced flare destruction to 0.98; annual model applies cover oxidation by emission year not deposit year, fixing biocover having no effect on closed landfills (WasteMAP #719) (model-output change)
- [2026-08](2026-08.md) — Single-site adst gains optional `depth` (deep-dump MCF bump) and `k_override` (caller-supplied decomposition rate) inputs, restoring the last two site-DST levers; `/sdst` flaring efficiency reaches the model again after a variable-name bug silently forced flare destruction to 0.98; annual model applies cover oxidation by emission year not deposit year, fixing biocover having no effect on closed landfills (WasteMAP #719); `City.sdst_v1_5` custom-site path holds the scenario equal to the baseline before the implementation year even when composition changes (was back-dating the new composition onto pre-implementation deposits) (model-output change)
- [2026-07](2026-07.md) — All ten waste types eligible for combustion (metal/glass/other added); methane-only model treats combustion as landfill diversion (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
Expand Down
178 changes: 178 additions & 0 deletions tests/test_sdst_v1_5_preimplement_composition.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
"""Regression test: sdst scenario == baseline before the implementation year.

``City.sdst_v1_5`` models the baseline and the scenario as two independent
landfills. Before the scenario's implementation year nothing has been changed, so
the scenario landfill must receive exactly the same waste as the baseline — the
same total mass *and* the same composition — and therefore emit exactly the same
methane in every year before ``implement_year``.

For real (TRACE-reconciled) sites the engine already enforced this by splicing the
scenario waste back to the baseline pre-implement. The blank/custom-site branch
(``baseline_data=None`` — a "Custom Location" in the /sdst UI) omitted that splice:
it applied the *scenario* waste composition to every year, so a scenario that
changed the waste composition wrongly back-dated the new composition onto deposits
from before the implementation year. This test drives that exact path (a custom
site with a different scenario composition) and asserts the pre-implement
scenario emissions equal the baseline. It fails on the pre-fix engine.
"""

import pandas as pd
import pytest

from SWEET_python.city_params import City
from SWEET_python.class_defs import Variant

COMPONENT_ORDER = [
"food",
"green",
"wood",
"paper_cardboard",
"textiles",
"plastic",
"metal",
"glass",
"rubber",
"other",
]

OPEN_YEAR = 2000
CLOSE_YEAR = 2050
IMPLEMENT_YEAR = 2025
MODEL_YEAR_MAX = 2050

# Baseline and scenario compositions differ; both sum to 1.0.
BASELINE_FRACTIONS = [0.5, 0.1, 0.05, 0.1, 0.05, 0.1, 0.02, 0.03, 0.0, 0.05]
SCENARIO_FRACTIONS = [0.3, 0.1, 0.05, 0.2, 0.05, 0.15, 0.05, 0.05, 0.0, 0.05]


def _run_sdst(baseline_fractions, scenario_fractions):
"""Drive City.sdst_v1_5 for a blank/custom single site (baseline_data=None)."""
years = pd.Index(range(OPEN_YEAR, MODEL_YEAR_MAX + 1))

def _expand(values):
return pd.DataFrame(
[list(values)] * len(years), index=years, columns=COMPONENT_ORDER, dtype=float
)

waste_mass_year = Variant[int](baseline=2025, scenario=2025)
city = City("preimplement_composition_test")
city.cityparams_obj_for_blank_site(
country="BRA",
population=None,
precipitation=500.0,
temperature=10.0,
waste_fractions=Variant(baseline=list(baseline_fractions), scenario=list(scenario_fractions)),
waste_mass_year=waste_mass_year,
growth_rate_override=0.0,
)

city.sdst_v1_5(
precipitation=500.0,
new_waste_fractions={"baseline": _expand(baseline_fractions), "scenario": _expand(scenario_fractions)},
new_landfill_types=Variant(baseline=[2], scenario=[0]),
new_gas_efficiency=Variant(baseline=[0.0], scenario=[0.6]),
new_landfill_open_close_dates=Variant(
baseline=[(OPEN_YEAR, CLOSE_YEAR)], scenario=[(OPEN_YEAR, CLOSE_YEAR)]
),
scenario=1,
landfill_split_timeline=Variant(
baseline={year: [1.0] for year in years}, scenario={year: [1.0] for year in years}
),
new_landfill_latlons=None,
new_landfill_areas=None,
new_covertypes=None,
new_coverthicknesses=None,
waste_burning=Variant(baseline=0.0, scenario=0.0),
new_landfill_flaring=Variant(baseline=[0.98], scenario=[0.98]),
fancy_ox=None,
new_waste_mass=Variant(baseline=10000.0, scenario=10000.0),
waste_mass_year=waste_mass_year,
depths=Variant(baseline=[3.0], scenario=[3.0]),
ks_overrides=Variant(baseline=0.2, scenario=0.2),
biocover={"baseline": 0.0, "scenario": 0.0},
oxidation_override=None,
baseline_data=None,
implement_year=IMPLEMENT_YEAR,
growth_rate_override=0.0,
country_growth_defaults=[1.0, 1.0],
)
return (
city.baseline_parameters.total_emissions["total"],
city.scenario_parameters[0].total_emissions["total"],
)


def test_scenario_matches_baseline_before_implement_year():
baseline, scenario = _run_sdst(BASELINE_FRACTIONS, SCENARIO_FRACTIONS)

for year in range(OPEN_YEAR, IMPLEMENT_YEAR):
assert scenario.loc[year] == pytest.approx(baseline.loc[year], abs=1e-9), (
f"scenario emissions in {year} (< implement year {IMPLEMENT_YEAR}) must "
f"equal baseline: got scenario={scenario.loc[year]}, baseline={baseline.loc[year]}"
)


def test_scenario_diverges_from_baseline_after_implement_year():
# Sanity check that the scenario really is a different scenario (dump -> landfill
# with gas capture), so the pre-implement equality above is a real constraint,
# not a degenerate baseline==scenario run.
baseline, scenario = _run_sdst(BASELINE_FRACTIONS, SCENARIO_FRACTIONS)

assert scenario.loc[2050] < baseline.loc[2050]


def test_pre_implement_equality_holds_when_only_composition_changes():
# Even with no landfill-type/gas change at all, a pure composition change must
# not affect pre-implement emissions.
years = pd.Index(range(OPEN_YEAR, MODEL_YEAR_MAX + 1))

def _expand(values):
return pd.DataFrame(
[list(values)] * len(years), index=years, columns=COMPONENT_ORDER, dtype=float
)

waste_mass_year = Variant[int](baseline=2025, scenario=2025)
city = City("preimplement_composition_only")
city.cityparams_obj_for_blank_site(
country="BRA",
population=None,
precipitation=500.0,
temperature=10.0,
waste_fractions=Variant(baseline=list(BASELINE_FRACTIONS), scenario=list(SCENARIO_FRACTIONS)),
waste_mass_year=waste_mass_year,
growth_rate_override=0.0,
)
city.sdst_v1_5(
precipitation=500.0,
new_waste_fractions={"baseline": _expand(BASELINE_FRACTIONS), "scenario": _expand(SCENARIO_FRACTIONS)},
new_landfill_types=Variant(baseline=[0], scenario=[0]),
new_gas_efficiency=Variant(baseline=[0.0], scenario=[0.0]),
new_landfill_open_close_dates=Variant(
baseline=[(OPEN_YEAR, CLOSE_YEAR)], scenario=[(OPEN_YEAR, CLOSE_YEAR)]
),
scenario=1,
landfill_split_timeline=Variant(
baseline={year: [1.0] for year in years}, scenario={year: [1.0] for year in years}
),
new_landfill_latlons=None,
new_landfill_areas=None,
new_covertypes=None,
new_coverthicknesses=None,
waste_burning=Variant(baseline=0.0, scenario=0.0),
new_landfill_flaring=None,
fancy_ox=None,
new_waste_mass=Variant(baseline=10000.0, scenario=10000.0),
waste_mass_year=waste_mass_year,
depths=Variant(baseline=[3.0], scenario=[3.0]),
ks_overrides=Variant(baseline=0.2, scenario=0.2),
biocover={"baseline": 0.0, "scenario": 0.0},
oxidation_override=None,
baseline_data=None,
implement_year=IMPLEMENT_YEAR,
growth_rate_override=0.0,
country_growth_defaults=[1.0, 1.0],
)
baseline = city.baseline_parameters.total_emissions["total"]
scenario = city.scenario_parameters[0].total_emissions["total"]
for year in range(OPEN_YEAR, IMPLEMENT_YEAR):
assert scenario.loc[year] == pytest.approx(baseline.loc[year], abs=1e-9)