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
37 changes: 4 additions & 33 deletions SWEET_python/city_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -4725,40 +4725,11 @@ def dst_baseline_blank(
textiles=pd.Series(ks.get("textiles", 0.0), index=years),
)

# Determine waste split fractions using .get() method
dumpsite_frac = defaults_2019.fraction_open_dumped_country.get(
iso3, defaults_2019.fraction_open_dumped.get(region, 0)
)
landfill_wo_capture_frac = defaults_2019.fraction_landfilled_country.get(
iso3, defaults_2019.fraction_landfilled.get(region, 0)
# Determine waste split fractions, as the cities table does for a city
# with no landfill data
split_fractions = SplitFractions(
**defaults_2019.disposal_split_for(iso3, region)
)
landfill_w_capture_frac = 0.0 # Default as per original function

try:
split_fractions = SplitFractions(
dumpsite=dumpsite_frac,
landfill_wo_capture=landfill_wo_capture_frac,
landfill_w_capture=landfill_w_capture_frac,
)
except KeyError:
if self.region in defaults_2019.landfill_default_regions:
split_fractions = SplitFractions(
landfill_w_capture=0.0, landfill_wo_capture=1.0, dumpsite=0.0
)
else:
split_fractions = SplitFractions(
landfill_w_capture=0.0, landfill_wo_capture=0.0, dumpsite=1.0
)

# Normalize split fractions
split_total = sum(split_fractions.model_dump().values())
if split_total > 0:
split_fractions = SplitFractions(
**{
site: frac / split_total
for site, frac in split_fractions.model_dump().items()
}
)

# Instantiate landfill objects
years_range = range(MODEL_START_YEAR, MODEL_END_YEAR + 1)
Expand Down
42 changes: 42 additions & 0 deletions SWEET_python/defaults_2019.py
Original file line number Diff line number Diff line change
Expand Up @@ -1284,6 +1284,48 @@ def get_precipitation_zone(rainfall):
country_to_iso3[country]: value
for country, value in fraction_landfilled_country.items()
}


def disposal_split_for(iso3, region):
"""Default disposal split for a city with no landfill data of its own.

Returns the shares of disposed waste sent to ``landfill_w_capture``,
``landfill_wo_capture`` and ``dumpsite``, summing to 1. The country's row is
used when it sends waste to a landfill or a dump, else the region's; a place
with neither is all landfill in ``landfill_default_regions`` and all dumpsite
elsewhere.

Southern Asia, South-Eastern Asia and Southern Africa have no regional row
(the other regional tables carry them as ``0.0 # np.nan``), so their
countries without a row of their own take that fallback. Canada's, Germany's
and Switzerland's rows are all zeros, so they take their region's, which is
landfill only.

This is the split ``City.load_andre_params`` gives a cities-table row with no
landfill data, so a Custom Location starts from the same split as a mapped
city in the same country.
"""
for key, dumped, landfilled in (
(iso3, fraction_open_dumped_country, fraction_landfilled_country),
(region, fraction_open_dumped, fraction_landfilled),
):
dumpsite = dumped.get(key, 0.0)
landfill = landfilled.get(key, 0.0)
total = dumpsite + landfill
if total > 0:
return {
"landfill_w_capture": 0.0,
"landfill_wo_capture": landfill / total,
"dumpsite": dumpsite / total,
}
landfills = region in landfill_default_regions
return {
"landfill_w_capture": 0.0,
"landfill_wo_capture": 1.0 if landfills else 0.0,
"dumpsite": 0.0 if landfills else 1.0,
}


fraction_incinerated_country = {
"Japan": 0.76,
"South Korea": 0.22,
Expand Down
69 changes: 69 additions & 0 deletions changelog/2026-10.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# SWEET_python Changelog — October 2026

**Highlights:** A Custom Location city modelled no landfill methane at all in 31
countries, among them Pakistan, India, Bangladesh, Indonesia, the Philippines,
Vietnam, South Africa, Canada and Germany. Its disposal split came out
(0, 0, 0), so its landfills took none of its waste. It now gets the split the
cities table gives a city with no landfill data (model-output change).

## Fixed

- **A Custom Location buries or dumps its waste in every country**
(model-output change). `City.dst_baseline_blank`, which WasteMAP runs for any
city not in the cities table, looked a country's disposal split up as
`fraction_open_dumped_country.get(iso3, fraction_open_dumped.get(region, 0))`,
and the same for `fraction_landfilled`. Two gaps in those tables made the split
(0, 0, 0):
- **Southern Asia, South-Eastern Asia and Southern Africa have no regional
row.** This is not a label mismatch: every other regional table carries these
three regions under the same names, as `0.0 # np.nan`. None of their 28
countries here has a row of its own.
- **Canada's, Germany's and Switzerland's country rows are all zeros.**

The normalization step skipped a zero total, so every landfill's
`fraction_of_waste` was 0 and the waste the city didn't divert left the model.
The `except KeyError` fallback written for this never ran, because `.get`
never raises.

The new `defaults_2019.disposal_split_for(iso3, region)` uses the country's row
when it sends waste to a landfill or a dump, else the region's. A place with
neither is all landfill in `landfill_default_regions` and all dumpsite
elsewhere. That is the split `City.load_andre_params` gives a cities-table row
with no landfill data, which a test checks for one country per region and
rule. `load_andre_params` itself is unchanged, so the cities table doesn't
move.

2040 emissions for a Custom Location of 2 million people (500 mm, 25 °C):
- **The 28 countries in the three regions dump all their waste.** Pakistan
goes from 0 to 21,156 t CH4, the Philippines to 10,513, South Africa to
33,478 and India to 5,284.
- **Canada, Germany and Switzerland landfill it.** Canada goes from 40 to
74,846 t, Germany from 26 to 29,521 and Switzerland from 36 to 17,263. The
old figures were compost alone.
- **The other 218 countries are bit-identical.** This was checked across all
249 countries a Custom Location can be built for (Kosovo's `XKX` can't be
looked up, before or after).

Tests: `tests/test_custom_location_disposal_split.py`.
([#74](https://github.com/RMI/SWEET_python/pull/74),
[RMI/WasteMAP#853](https://github.com/RMI/WasteMAP/pull/853))

## Known limitations

- **The three regions fall back to all dumpsite.** Central Asia and Rest of
Oceania have done the same since #18, for the same reason: the source gives
them no disposal figures. That is coarse for countries such as South Africa,
which landfill most of their waste. The cities table uses the same fallback.
([#74](https://github.com/RMI/SWEET_python/pull/74))

- **Germany and Switzerland now landfill everything they don't compost or
burn.** Their rows file the rest under "unspecified", which holds recycling,
and Custom Location has no recycling default. That overstates their
landfilling, as it already did for Japan, Austria, the Netherlands and Sweden,
whose rows landfill 1–3%. ([#74](https://github.com/RMI/SWEET_python/pull/74))

- **Sri Lanka gives the highest figure of the 31: 122,745 t.** Its per-capita
default in `msw_per_capita_country` is 1.86 t/year (5.1 kg/person/day).
Fourteen countries there are above 3 kg/person/day, Kuwait highest at 8.4.
That is a separate data problem, which the zero split had hidden for Sri Lanka.
([#74](https://github.com/RMI/SWEET_python/pull/74))
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-10](2026-10.md) — a Custom Location city models landfill methane in every country: in 31 (Southern Asia, South-Eastern Asia and Southern Africa, which have no regional disposal row, plus Canada, Germany and Switzerland, whose rows are all zeros) its disposal split was (0, 0, 0), so its landfills took none of its waste; it now gets the cities table's split for a city with no landfill data (model-output change)
- [2026-09](2026-09.md) — WasteMAP's site tool, city tool and Custom Location grow waste with the country's UN population year by year, as Climate TRACE does; big cities keep their own UN city rates (model-output change); model output frames get one fixed waste-type column order instead of a per-process one (a `set` fed the ordering, and Python randomizes set iteration per process), moving values in the last bit only, where a sum runs across the reordered columns; City DST's diversion sum stops passing pandas a keyword pandas is removing, which would have broken the model path on pandas 4 behind a misleading `AttributeError` (no output change); annual model no longer charges a flare's inefficiency twice (a hardcoded 2% slip stacked on the `flaring` destruction efficiency), so it agrees with the monthly model again and a site with gas capture emits ~3.5% less (model-output change); `gas_capture_efficiency` bounded to [0, 1], where an out-of-range value used to produce negative emissions silently; one methane density constant instead of two 9.1% apart
- [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); MCF consolidated into a new `SWEET_python.mcf` module and both dump types moved to the IPCC uncategorised-SWDS 0.6 (open dumps up from 0.4, controlled dumps down from 0.7), with a supplied depth now selecting the deep/shallow category (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)
Expand Down
177 changes: 177 additions & 0 deletions tests/test_custom_location_disposal_split.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
"""A Custom Location buries or dumps its waste in every country.

`City.dst_baseline_blank` (WasteMAP's Custom Location) looked up a country's
disposal split with `fraction_open_dumped_country.get(iso3,
fraction_open_dumped.get(region, 0))`, and the same for `fraction_landfilled`.
Two gaps in those tables made the split (0, 0, 0) for 31 countries:

- Southern Asia, South-Eastern Asia and Southern Africa have no regional row.
This is not a label mismatch: every other regional table carries these three
regions under the same names, as `0.0 # np.nan`. None of their 28 countries
here has a row of its own.
- Canada's, Germany's and Switzerland's country rows are all zeros.

With a zero split every landfill took 0% of the waste, so the city modelled no
landfill methane at all. Pakistan and the Philippines at 2M people gave 0 t CH4
in 2040, against 28,745 t for Mexico. The `except KeyError` fallback written for
this never ran, because `.get` never raises.

The cities table (`City.load_andre_params`) handles both gaps. A region with no
row falls back to all landfill or all dumpsite by `landfill_default_regions`, and
the three zero-row countries are landfilled. Custom Location now takes the split
the cities table gives a city with no landfill data
(`defaults_2019.disposal_split_for`).
"""

import numpy as np
import pytest

from SWEET_python import defaults_2019
from SWEET_python.city_params import City

POPULATION = 2_000_000
PRECIPITATION = 500.0
TEMPERATURE = 25.0
YEAR = 2040

# Every country whose Custom Location split was (0, 0, 0).
NO_ROW = {
"Southern Asia": ["BGD", "BTN", "IND", "IOT", "IRN", "LKA", "MDV", "NPL", "PAK"],
"South-Eastern Asia": [
"BRN", "IDN", "KHM", "LAO", "MMR", "MYS", "PHL", "THA", "TLS", "VNM",
],
"Southern Africa": ["ATF", "BWA", "LSO", "MOZ", "NAM", "SHN", "SWZ", "ZAF", "ZWE"],
}
ZERO_COUNTRY_ROW = ["CAN", "CHE", "DEU"]
ZEROED = sorted([iso3 for codes in NO_ROW.values() for iso3 in codes] + ZERO_COUNTRY_ROW)


def _custom_location(iso3):
city = City("Custom Location")
city.dst_baseline_blank(iso3, POPULATION, PRECIPITATION, TEMPERATURE)
return city.baseline_parameters


class _NoDataRow(dict):
"""A cities-table row in which every field not given is missing (NaN)."""

def __missing__(self, key):
return np.nan


def _cities_table_split(iso3):
"""The split `load_andre_params` gives a city with no landfill data in `iso3`.

The row says where the city is and nothing about its waste: every landfill and
diversion share is NaN, so the loader takes its defaults for both, which is
the cities-table counterpart of a Custom Location.
"""
country = next(
name
for name, code in defaults_2019.country_to_iso3.items()
if code == iso3 and name in defaults_2019.region_lookup
)
row = _NoDataRow(
{
"country": country,
"iso": iso3,
"population_count": POPULATION,
"population_year": 2022.0,
"population_data_source": "test",
"msw_generated_metric_tons_per_year": 500_000.0,
"msw_generated_year": 2022.0,
"mean_yearly_precip_2000_2021": PRECIPITATION,
"mean_yearly_temp_2000_2021": TEMPERATURE,
"Temperature (C)": TEMPERATURE,
"latitude": 0.0,
"longitude": 0.0,
"historic_growth_rate": 1.0,
"future_growth_rate": 1.0,
}
)
city = City("Cities table")
city.load_andre_params(row)
return city.baseline_parameters.split_fractions


def _how_the_split_is_decided(iso3, region):
if iso3 in defaults_2019.fraction_open_dumped_country:
row = (
defaults_2019.fraction_open_dumped_country[iso3]
+ defaults_2019.fraction_landfilled_country[iso3]
)
return "country row" if row > 0 else "all-zero country row"
if region in defaults_2019.fraction_open_dumped:
return "regional row"
return "no row"


def _one_country_per_region_and_rule():
"""The first country of each (region, which row decides) pair, plus Germany.

Comparing every country with the cities table takes ten seconds; one per
pair runs every branch of both lookups for every region in a fifth of that.
Germany is added so all three zero-row countries are covered.
"""
first = {}
for iso3, region in sorted(defaults_2019.region_lookup_iso3.items()):
first.setdefault((region, _how_the_split_is_decided(iso3, region)), iso3)
return sorted(set(first.values()) | set(ZERO_COUNTRY_ROW))


@pytest.mark.parametrize("iso3", ZEROED)
def test_a_custom_location_models_methane_from_the_waste_it_disposes_of(iso3):
parameters = _custom_location(iso3)

assert sum(landfill.fraction_of_waste for landfill in parameters.landfills) == (
pytest.approx(1.0)
)
# Landfill methane only. Germany's total was already 26 t from composting
# while its landfills took nothing.
assert parameters.landfill_emissions.loc[YEAR, "total"] > 0


@pytest.mark.parametrize("iso3", ["PAK", "PHL", "ZAF"])
def test_a_country_in_a_region_with_no_row_dumps_its_waste(iso3):
# None of the three regions is in `landfill_default_regions`.
split = _custom_location(iso3).split_fractions

assert (split.landfill_w_capture, split.landfill_wo_capture, split.dumpsite) == (
0.0,
0.0,
1.0,
)


@pytest.mark.parametrize("iso3", ZERO_COUNTRY_ROW)
def test_a_country_whose_row_is_all_zeros_landfills_its_waste(iso3):
split = _custom_location(iso3).split_fractions

assert (split.landfill_w_capture, split.landfill_wo_capture, split.dumpsite) == (
0.0,
1.0,
0.0,
)


@pytest.mark.parametrize("iso3", _one_country_per_region_and_rule())
def test_a_custom_location_starts_from_the_cities_tables_split(iso3):
custom = _custom_location(iso3).split_fractions
table = _cities_table_split(iso3)

assert custom.model_dump() == pytest.approx(table.model_dump())


def test_every_country_has_a_disposal_split():
splits = {
iso3: defaults_2019.disposal_split_for(iso3, region)
for iso3, region in defaults_2019.region_lookup_iso3.items()
}

assert {
iso3: split
for iso3, split in splits.items()
if set(split) != {"landfill_w_capture", "landfill_wo_capture", "dumpsite"}
or min(split.values()) < 0
or sum(split.values()) != pytest.approx(1.0)
} == {}
Loading