Skip to content

The City DST's "add gas capture" applies the oxidation it sets - #78

Merged
HughRunyan merged 3 commits into
mainfrom
add-gas-capture-keeps-its-oxidation
Oct 17, 2026
Merged

HughRunyan merged 3 commits into
mainfrom
add-gas-capture-keeps-its-oxidation

Conversation

@HughRunyan

@HughRunyan HughRunyan commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

Bug

The City DST's "Add gas capture" lever (add_gas in City.implement_dst_changes_simple_v1_5) applied the gas capture it adds but not the oxidation it sets.

Environment: SWEET_python main at d5c592e, the generic branch of implement_dst_changes_simple_v1_5 (if not scenario_parameters.sites_method:). WasteMAP /v1/city_emissions/implement_dst_simple_v1_5, with the June 2026 cities table (cities_for_map_0626.csv) and load_city replicated offline.

Reproduction: load Lima (all disposal to a landfill without gas capture, no diversion). Run the scenario with implement_year=2026, once with add_gas=0 and once with add_gas=1.

Expected: after 2026 the landfill captures 60% of its methane and oxidises 22% of the rest instead of 10%. Emissions are the no-change scenario's × 0.4 × 0.78 / 0.9 = 0.347, i.e. 65.3% lower.

Actual: × 0.4, i.e. 60.0% lower: 30,393 t CH₄ in 2040 instead of 26,341. After the run, the landfill's oxidation_factor is the scalar 0.1 and the dumpsite's is 0, in every year.

Root cause

After the implementation year, add_gas gives the landfill without capture 60% gas capture and an oxidation factor of 0.22. It converts the dumpsite to a controlled dumpsite with 30% capture and 0.1. Then it sets skip_ox = True so that Landfill.estimate_emissions keeps those factors. The emissions loop at the end of the method overwrote that:

skip_ox = scenario_parameters.sites_method   # False on the generic branch
for landfill in scenario_parameters.landfills:
    landfill.estimate_emissions(skip_ox=skip_ox)

With skip_ox=False, estimate_emissions resets every non-advanced landfill's oxidation factor from its gas_capture flag (ox_cap / ox_nocap). add_gas never sets that flag, so the converted sites got the no-capture defaults: 0.1 for the landfill and 0 for the dumpsite. Both lines date from the repository's first commit (2c5db07).

Are 0.22 and 0.1 the right values?

Yes. I found no reason they shouldn't apply.

  • They are the model's own values for these sites with gas collection. estimate_emissions' ox_cap table gives a landfill 0.22 and a controlled_dumpsite 0.1, and so does defaults_2019.oxidation_factor["with_lfg"]. The real-sites branch of this method also gives a sanitary landfill 0.22 when it adds capture.
  • Up to the implementation year they equal the baseline's. add_gas keeps the no-capture defaults (0.1 and 0) through the implementation year. So nothing moves before it, and the converted sites are a capped landfill and a controlled dump only from the year their capture starts.
  • No double counting. The engine computes (generated − captured) × (1 − oxidation), so oxidation acts only on the gas the capture misses. Since the August fix for RMI/WasteMAP#719 it is applied by emission year, as capture is, so it covers waste deposited before the implementation year just as the capture does.

Fix

Each branch sets skip_ox itself, and the emissions loop uses it:

  • the generic branch: False, or True when add_gas has set its oxidation factors (as it already did)
  • the real-sites branch: True (what it got before, from sites_method)

The overwrite is gone. Real sites are advanced and keep the oxidation they carry whatever skip_ox says. skip_ox matters on that branch only for the generic landfills that WasteMAP's Brazilian rows bring there; see RMI/WasteMAP#851.

Model output (model-output-change)

Measured over WasteMAP's June 2026 cities table (5,689 city names) and a Custom Location in each of 249 countries (2 million people, 500 mm, 25 °C). Each scenario uses the city's own diversions with gas capture added in 2026, compared before and after this change.

2040 emissions against the no-change scenario:

Split (landfill without capture / dumpsite) Before After
Lima, Riyadh, Jakarta 100% / 0% −60.0% −65.3%
Abidjan 76% / 24% −54.7% −60.3%
Cairo 18% / 82% −37.3% −43.9%
Lagos, Karachi, Dhaka, Delhi (API only) 0% / 100% −30.0% −37.0%
  • The 168 cities the UI offers the lever to (no gas capture, some landfill) all move. Their add_gas result falls another 10.0–13.3% in 2040 (median 13.3%). That is 13.3% for a landfill (0.78 / 0.9) and 10.0% for a dumpsite (0.9 / 1).
  • 187 cities that only dump move by 10.0%. The UI hides the lever for them (no_landfills_present), but the API runs it.
  • 218 of 249 Custom Locations move by 10.0–13.3%. The other 31 have the empty disposal split A Custom Location buries or dumps its waste in every country #74 fixes.
  • Brazil's 4,694 flagged rows don't move. WasteMAP's load_city still sends them down the real-sites branch, where add_gas changes nothing for generic sites. With RMI/WasteMAP#851's load_city, 4,622 of the 4,627 that run move by 10.0–13.3%; the other 5 model zero emissions.
  • Unchanged, bit for bit:
    • the no-change scenario, for all 5,871 cities and Custom Locations that run
    • every add_gas result up to and including the implementation year
    • every request without add_gas, whose skip_ox is what it was
  • Same failures: the 67 cities that fail fail the same way before and after (65 over the recycling cap, plus São Paulo and Brasília until City DST scenarios run for São Paulo and Brasília: load_csv_new normalizes a city's composition #73).
  • Cities that don't move are explained: 597 Brazilian rows publish a waste generation rate of 0 t/yr, and 41 cities send everything to capture.

Climate TRACE's pipeline doesn't call this method, so its outputs do not move.

Tests

tests/test_add_gas_capture_oxidation.py: 12 tests. They build a city from country defaults (dst_baseline_blank) with a 0/50/50 split, as in tests/test_move_to_existing_gas_capture.py (#75), and run the real implement_dst_changes_simple_v1_5, with no database.

  • 8 of 12 fail on main, each on the defect:
    • the landfill's and dumpsite's oxidation factors are reset (also with a new landfill with gas capture alongside, which the UI allows)
    • emissions after the implementation year are × 0.4 and × 0.7 instead of × 0.347 and × 0.63
    • a city that landfills everything falls 60.0%, not 65.3%
    • the generic branch passes skip_ox=False with add_gas
  • The 4 that pass on main are guards: the no-change scenario keeps the default oxidation and reproduces the baseline's landfill emissions, and each branch passes the skip_ox it should (the generic branch without add_gas, and the sites branch with and without it).
  • Full suite: 284 passed.

The WasteMAP side, RMI/WasteMAP#856 (same branch name, so its CI installs this branch), adds an endpoint test on Lima's table row through load_city. It fails against main here and passes against this branch. Merge this PR first.

changelog/2026-10.md doesn't exist on main yet. #73, #74, #75 and this PR each create it, so whichever merges later resolves a changelog conflict; the code merges cleanly with all three. #75 documented this bug as a known limitation; I've removed that entry from #75, so no merge order leaves it behind.

Acceptance criteria

  • With add_gas, the landfill without capture oxidises at 0.22 and the converted dumpsite at 0.1 from the year after implementation, and at 0.1 and 0 up to and including it.
  • Emissions after the implementation year are the no-change scenario's × (1 − capture) × (1 − new oxidation) / (1 − old oxidation) for each converted site.
  • The no-change scenario, every request without add_gas, and every year up to the implementation year are bit-identical to before.
  • The real-sites branch still passes skip_ox=True.
  • Changelog entry (changelog/2026-10.md), cross-linked with WasteMAP's backend changelog; the known-limitation entries in Moving waste to a city's existing gas capture changes its emissions #75 and RMI/WasteMAP#854 are removed.

Definition of done

  • Acceptance criteria met
  • Tests and checks pass (SWEET_python: 284 passed; WasteMAP tests/fast against this branch: 577 passed, 1 skipped)
  • Docs updated (both changelogs)
  • Reviewed and merged (before RMI/WasteMAP#856)

🤖 Generated with Claude Code

In the generic branch of implement_dst_changes_simple_v1_5, add_gas gives the
landfill without capture an oxidation factor of 0.22 after the implementation
year and the dumpsite it converts to a controlled dumpsite 0.1, and sets
skip_ox = True to keep them. The emissions loop at the end replaced that with
skip_ox = scenario_parameters.sites_method, which is false on this branch, so
Landfill.estimate_emissions reset both from gas_capture (still False) to 0.1
and 0: the gas capture applied, the oxidation never did. Each branch now sets
skip_ox itself and the loop uses it; the real-sites branch keeps True.

Every add_gas result falls another 10.0-13.3% after the implementation year; a
city that landfills everything goes from -60.0% to -65.3% against no change in
2040. Across WasteMAP's June 2026 table and a Custom Location in every country,
the no-change scenario, the other levers and every year up to the
implementation year are bit-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@HughRunyan HughRunyan added bug Something isn't working model-output-change Changes model OUTPUT values (expected progress, not breaking); results differ from prior runs test Adds or modifies tests labels Oct 9, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HughRunyan added a commit that referenced this pull request Oct 9, 2026
"Add gas capture" losing the oxidation it sets is fixed in #78 (with
RMI/WasteMAP#856), so this changelog no longer lists it as a known limitation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HughRunyan added a commit that referenced this pull request Oct 17, 2026
)

* Moving waste to a city's existing gas capture changes its emissions

The City DST's "move waste to existing gas capture" lever (move_gas and
existing_gas_pct in implement_dst_changes_simple_v1_5) rewrote split_fractions
in the generic branch but never passed the split to the landfills, whose waste
comes from their own fraction_of_waste. Only the new-landfill-with-capture block
did that, and WasteMAP switches that lever off for every city it offers this one
to. So the slider returned the no-change scenario at every setting for all 7
cities in the June 2026 table where it is live.

The arithmetic was wrong too: it multiplied the shares without capture by
existing / original, scaling them up (Lahore's landfill without capture came out
at 171% of its waste) and zeroing them for a city with no capture.

_move_to_existing_gas_capture gives capture existing_gas_pct and the landfill
and dumpsite without capture the rest, in their original proportions, and the
scenario applies that split to the landfills. A request the lever can't carry
out raises INVALID_PARAMETERS. The new-landfill sum check now builds CustomError
with both arguments; it raised TypeError, a 500 at the API.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Link the changelog to the PRs it ships in

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drop the add_gas oxidation known limitation: #78 fixes it

"Add gas capture" losing the oxidation it sets is fixed in #78 (with
RMI/WasteMAP#856), so this changelog no longer lists it as a known limitation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The October changelog and its README line keep main's entries and add this PR's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@HughRunyan
HughRunyan merged commit b732db2 into main Oct 17, 2026
2 checks passed
@HughRunyan
HughRunyan deleted the add-gas-capture-keeps-its-oxidation branch October 17, 2026 20:23
HughRunyan added a commit that referenced this pull request Oct 17, 2026
* Bump the python-minor-and-patch group with 2 updates

Bumps the python-minor-and-patch group with 2 updates: [fastapi](https://github.com/fastapi/fastapi) and [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy).


Updates `fastapi` from 0.141.1 to 0.142.2
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](fastapi/fastapi@0.141.1...0.142.2)

Updates `sqlalchemy` from 2.1.1 to 2.1.3
- [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases)
- [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES.rst)
- [Commits](https://github.com/sqlalchemy/sqlalchemy/commits)

---
updated-dependencies:
- dependency-name: fastapi
  dependency-version: 0.142.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-minor-and-patch
- dependency-name: sqlalchemy
  dependency-version: 2.1.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Indonesia sizes its waste from its own IPCC row

msw_per_capita_country is IPCC 2019 Refinement Vol. 5 Ch. 2 Table 2A.1
(Updated), 2010 column. All 167 of its rows match the IPCC's, but Indonesia's
(0.19 t/person/yr) was never typed in, so Indonesia fell back to the
South-Eastern Asia default, 0.46: 2.4x its own row. Adds the row and cites the
source tables in comments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Changelog: Indonesia's per-capita waste row (#76)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* SWEET can name its next release, with tests for the counting

scripts/next_release.py turns a list of git tags into the next free
sweet/YYYY.MM.N for a month (N from 0, one past the highest used),
checks a hand-typed release name, and picks the newest release in a
list. Tags that are not release names -- other projects' wastemap/ and
submission/ tags, a stray sweet/test -- are ignored. Standard library
only, tags on stdin, so the release workflow can run it before
installing anything and the tests need no git or network.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* A one-click workflow cuts a dated SWEET release from main

.github/workflows/release.yml (Actions > Release > Run workflow) tags a
commit on main as sweet/YYYY.MM.N -- by default the tip of main and the
next free number for the current UTC month -- and publishes a GitHub
Release whose notes list the pull requests merged since the previous
release, with a link to the month's changelog file. The job summary
records the tag, the commit and the links.

Before anything is tagged it refuses: a commit main does not contain;
a name that already exists, or one that would sort before the newest
release; a commit that is already a release; a commit that does not
contain the newest release; and a commit whose own test suite fails
(the same install and pytest command as tests.yml, run against that
commit). The tag is annotated and pushed without force, so a release
can never be moved. Only main's copy of the workflow may run, and one
release runs at a time so two cannot take the same number.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* setup.py and the package docstring say releases are sweet/ tags

Both said SWEET has no releases. They now say a release is a git tag
sweet/YYYY.MM.N cut by the Release workflow, that setup.py's "0.1" is
still decoration and never bumped, and that __version__ stays the
commit: a release names one commit, the commit is all an installed
copy can report without a repository, and it is the only name a
branch install has. No code changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The README says how to cut, name and pin a SWEET release

The README's release section described a shared vYYYY.MM.DD tag that
was renamed to wastemap/YYYY.MM.DD in September and is not SWEET's own
release anyway. It now explains sweet/YYYY.MM.N: what the name means,
that a release is never moved or reused, how to cut one from the
Actions tab, what the workflow refuses, and how an app pins one (a
release name, or a SWEET branch only on a feature branch).

Its Usage section pointed at sweet_tools_obj.py, model.py and
load_from_database, none of which exist any more; it is now a short
map of the modules and entry points the two apps actually call.

changelog/README.md no longer says the project publishes no tags. It
says releases are cut from main with generated notes listing the PRs
since the last release, and that the monthly files keep the
human-written entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The recovery command writes the same notes the Release step would have

If the tag is pushed but the Release fails, the command in the job summary now
includes the changelog link (and the first release's note), quoted for bash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* A copy built from git knows its commit, without the app having to say

setup.py writes the commit of a clean checkout into the built package as
SWEET_python/_build_sha.py, which is how pip builds a git install, and
__version__ reads it first (source "build"), then SWEET_GIT_SHA, then .git.
The stamp goes only into the build directory, never the source tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The README says how an installed copy knows its commit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* SWEET's docs say its population table is the only copy

Climate TRACE's pipeline and its offline country-data build now read
SWEET_python/pops_yearly.csv (RMI_Climate_TRACE_Waste_Methane#208), so the
"two copies, nothing keeps them in step" text in constants.py and
population.py is out of date.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* A site with no recorded type is typed by one rule, the same one Climate TRACE uses

SWEET guessed a missing site type in seven places, from one of two region
lists (one with Eastern Europe, one with Australia and New Zealand instead).
Climate TRACE's pipeline used a fuller rule: income, region and named
countries. So a Kazakh site with no type was a sanitary landfill in Climate
TRACE and an open dump in WasteMAP's site tool.

The rule now lives in SWEET_python/site_type_defaults.py, moved unchanged from
Climate TRACE's landfill_table_ops.fill_landfill_types. A site is a sanitary
landfill where its country is high income, or its region is one of
landfill_default_regions plus Western Asia and Central Asia, or it is China,
Russia, Mexico, Brazil or Costa Rica. Otherwise, and always for Mongolia and
Yemen, it is an open dump.

Income comes from SWEET_python/country_income.py, copied from the World Bank
table Climate TRACE reads (What a Waste 2.0's country file, 217 countries).

Every guess in city_params now calls it: the city split's last fallback (three
branches), site_only_estimate, both trace paths, and the site tool's prefill
by location and for a listed site with no type. dst_baseline_blank's fallback
is left to #74, which rewrites it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Changelog: a site with no recorded type is guessed the same way everywhere

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Gas capture and oxidation follow a site's reports year by year when its rows carry their year

The Climate TRACE pipeline's consolidated input gives a site one row per year and
marks each with `row_year`. On those rows a reported gas collection efficiency or
oxidation belongs to its year: one value is used in every model year, and several
are carried back and forward -- the first back to the start, each forward to the
next report, the latest to the end.

Two consequences. A modelled site, which has no reported-emissions year, keeps its
measured efficiency instead of falling to the site-type default. And if upstream
starts resolving these two per year, the series follows the reports with no change
here.

Rows without `row_year` -- the legacy input and every other caller -- keep the old
rule (site mean, overwritten in reported years).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The carried series reads nullable year and value columns

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Changelog: gas capture and oxidation follow a site's yearly reports when its rows carry their year

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Rows that carry their year never fall back to the emissions-year rule

If every row_year on a site's rows was blank, both builders fell through to the
legacy rule, which places a value by its reported-emissions year. For gas capture
that brought back the fault this branch fixes: a modelled site, with no such year,
lost its measured efficiency to the site-type default. Now an undated value on these
rows is the site's value in every year, and a site with no value at all gets the
gated default as before. Raised in review of #84.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The places SWEET applies a Climate TRACE modelling decision carry a comment naming it

Seven decisions from RMI_Climate_TRACE_Waste_Methane's docs/MODEL_DECISIONS.md are
tagged where SWEET applies them: MCF, site type, waste composition, gas capture,
oxidation, flaring and population growth. That repo's test checks the comments once a
SWEET release containing them is pinned. Comments only; no number moves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Bump the python-minor-and-patch group with 5 updates

Bumps the python-minor-and-patch group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [asyncpg](https://github.com/MagicStack/asyncpg) | `0.31.0` | `0.32.0` |
| [fastapi](https://github.com/fastapi/fastapi) | `0.142.2` | `0.143.0` |
| [numpy](https://github.com/numpy/numpy) | `2.5.3` | `2.5.4` |
| [pydantic](https://github.com/pydantic/pydantic) | `2.13.5` | `2.14.0` |
| [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) | `2.1.3` | `2.1.4` |


Updates `asyncpg` from 0.31.0 to 0.32.0
- [Release notes](https://github.com/MagicStack/asyncpg/releases)
- [Commits](MagicStack/asyncpg@v0.31.0...v0.32.0)

Updates `fastapi` from 0.142.2 to 0.143.0
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](fastapi/fastapi@0.142.2...0.143.0)

Updates `numpy` from 2.5.3 to 2.5.4
- [Release notes](https://github.com/numpy/numpy/releases)
- [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst)
- [Commits](numpy/numpy@v2.5.3...v2.5.4)

Updates `pydantic` from 2.13.5 to 2.14.0
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](pydantic/pydantic@v2.13.5...v2.14.0)

Updates `sqlalchemy` from 2.1.3 to 2.1.4
- [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases)
- [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES.rst)
- [Commits](https://github.com/sqlalchemy/sqlalchemy/commits)

---
updated-dependencies:
- dependency-name: asyncpg
  dependency-version: 0.32.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-minor-and-patch
- dependency-name: fastapi
  dependency-version: 0.143.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-minor-and-patch
- dependency-name: numpy
  dependency-version: 2.5.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-minor-and-patch
- dependency-name: pydantic
  dependency-version: 2.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-minor-and-patch
- dependency-name: sqlalchemy
  dependency-version: 2.1.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* The README explains decision comments, and a pull-request template asks about them

A "Decision comments" section says what a `# decision:` comment marks, that the list
lives in the Climate TRACE pipeline repo, which four tagged decisions WasteMAP shares,
and that changing a tagged line needs a paired Climate TRACE pull request updating its
entry. A new pull-request template asks whether a change touches a tagged line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Seven more Climate TRACE modelling decisions are marked where SWEET applies them

Comments naming entries in RMI_Climate_TRACE_Waste_Methane's docs/MODEL_DECISIONS.md:
waste-before-1970-is-zero, intake-ends-before-close-year, intake-follows-population,
decay-rate-from-climate, methane-potential-by-waste-type, monthly-from-decay-model and
particulate-factors. Comments only; no number moves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Changelog: seven decisions, not nine

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* City DST scenarios run for São Paulo and Brasília: load_csv_new normalizes a city's composition (#73)

* load_csv_new normalizes a city's composition, so the city DST runs for São Paulo and Brasília

WasteMAP's cities table publishes Brazil's precomputed rows with their source
composition as it is: São Paulo's ten Waste Components sum to 99%, Brasília's
to 87%, and Brazil's national default to 100.1%. load_csv_new was the one
loader that took a composition as published. implement_dst_changes_simple_v1_5
therefore rejected São Paulo and Brasília outright with "Invalid waste
fractions" (São Paulo by float rounding alone: 1 - 0.99 = 0.010000000000000009).
Every other Brazilian city's typed masses missed or exceeded its waste, and its
no-change scenario drifted from the baseline.

Normalize proportionally, as load_andre_params and dst_baseline_blank do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Changelog: load_csv_new normalizes a city's composition

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* A Custom Location buries or dumps its waste in every country (#74)

* A Custom Location buries or dumps its waste in every country

City.dst_baseline_blank looked a country's disposal split up with
.get(iso3, .get(region, 0)). Southern Asia, South-Eastern Asia and
Southern Africa have no row in fraction_open_dumped / fraction_landfilled
(no label mismatch: the other regional tables carry them as 0.0 # np.nan),
and Canada's, Germany's and Switzerland's country rows are all zeros, so
31 countries got a (0, 0, 0) split. Every landfill took 0% of the waste
and the city modelled no landfill methane; the except-KeyError fallback
never ran because .get never raises.

defaults_2019.disposal_split_for gives the split load_andre_params gives
a cities-table row with no landfill data: the country's row if it sends
waste anywhere, else the region's, else all landfill in
landfill_default_regions and all dumpsite elsewhere. load_andre_params is
unchanged, so the cities table doesn't move; the other 218 countries'
Custom Location output is bit-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The changelog links the WasteMAP half

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* A Custom Location's fallback split follows Climate TRACE's site-type rule

main's #83 moved load_andre_params's no-data fallback to
site_type_defaults.landfill_is_default. disposal_split_for still used
landfill_default_regions, so the parity test failed for Brunei (high income):
the cities table landfills it, a Custom Location dumped it. Calling the shared
rule makes the two agree for all 249 countries; only Brunei moves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Moving waste to a city's existing gas capture changes its emissions (#75)

* Moving waste to a city's existing gas capture changes its emissions

The City DST's "move waste to existing gas capture" lever (move_gas and
existing_gas_pct in implement_dst_changes_simple_v1_5) rewrote split_fractions
in the generic branch but never passed the split to the landfills, whose waste
comes from their own fraction_of_waste. Only the new-landfill-with-capture block
did that, and WasteMAP switches that lever off for every city it offers this one
to. So the slider returned the no-change scenario at every setting for all 7
cities in the June 2026 table where it is live.

The arithmetic was wrong too: it multiplied the shares without capture by
existing / original, scaling them up (Lahore's landfill without capture came out
at 171% of its waste) and zeroing them for a city with no capture.

_move_to_existing_gas_capture gives capture existing_gas_pct and the landfill
and dumpsite without capture the rest, in their original proportions, and the
scenario applies that split to the landfills. A request the lever can't carry
out raises INVALID_PARAMETERS. The new-landfill sum check now builds CustomError
with both arguments; it raised TypeError, a 500 at the API.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Link the changelog to the PRs it ships in

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drop the add_gas oxidation known limitation: #78 fixes it

"Add gas capture" losing the oxidation it sets is fixed in #78 (with
RMI/WasteMAP#856), so this changelog no longer lists it as a known limitation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* The City DST's "add gas capture" applies the oxidation it sets (#78)

* The City DST's "add gas capture" applies the oxidation it sets

In the generic branch of implement_dst_changes_simple_v1_5, add_gas gives the
landfill without capture an oxidation factor of 0.22 after the implementation
year and the dumpsite it converts to a controlled dumpsite 0.1, and sets
skip_ox = True to keep them. The emissions loop at the end replaced that with
skip_ox = scenario_parameters.sites_method, which is false on this branch, so
Landfill.estimate_emissions reset both from gas_capture (still False) to 0.1
and 0: the gas capture applied, the oxidation never did. Each branch now sets
skip_ox itself and the loop uses it; the real-sites branch keeps True.

Every add_gas result falls another 10.0-13.3% after the implementation year; a
city that landfills everything goes from -60.0% to -65.3% against no change in
2040. Across WasteMAP's June 2026 table and a Custom Location in every country,
the no-change scenario, the other levers and every year up to the
implementation year are bit-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Link the changelog to the PRs it ships in

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* A Custom Location in Kosovo builds, and the name "Kosovo" no longer means Serbia (#77)

* A Custom Location in Kosovo builds: SWEET's own country table before pycountry

pycountry has no Kosovo. _iso3_for asked it first, so XKX, the code SWEET's
defaults and WasteMAP's country list use, raised LookupError, and
dst_baseline_blank and cityparams_obj_for_blank_site raised "Country 'XKX' not
found." (WasteMAP's city tool answered 500). By name, the fuzzy search resolved
"Kosovo" to Serbia, so sdst_prepopulate prefilled a point in Kosovo as Serbia.

_iso3_for now tries region_lookup_iso3 by code, then country_to_iso3 by name,
both ignoring case, before pycountry. The other 249 codes WasteMAP offers
resolve as before, so their output is bit-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Changelog links the paired WasteMAP PR

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* A blank or junk country is not found, and the site tool's prefill finds every country the geocoder names (#80)

* A Custom Location in Kosovo builds: SWEET's own country table before pycountry

pycountry has no Kosovo. _iso3_for asked it first, so XKX, the code SWEET's
defaults and WasteMAP's country list use, raised LookupError, and
dst_baseline_blank and cityparams_obj_for_blank_site raised "Country 'XKX' not
found." (WasteMAP's city tool answered 500). By name, the fuzzy search resolved
"Kosovo" to Serbia, so sdst_prepopulate prefilled a point in Kosovo as Serbia.

_iso3_for now tries region_lookup_iso3 by code, then country_to_iso3 by name,
both ignoring case, before pycountry. The other 249 codes WasteMAP offers
resolve as before, so their output is bit-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* A blank or junk country is not found; the prefill finds every country the geocoder names

_iso3_for ended with pycountry's fuzzy search, which scores every country for
an empty name and scores a country for any of its subdivisions that match. So
"" and " " were the United Kingdom (the most subdivisions), "None" Italy
(Frosinone), "nan" Thailand (Nan province), and None raised AttributeError
rather than LookupError.

_iso3_for now raises LookupError for anything that isn't a non-blank string,
strips the name, and takes the fuzzy answer only when the name is whole words
of that country's own name or its initials ("UK", "UAE").

The site tool's prefill (sdst_prepopulate) crashed with AttributeError for a
point Nominatim can't place or gives no country, and failed for 11 English names
Nominatim uses that neither SWEET nor pycountry spell its way (Turkey, Ivory
Coast, Cape Verde, ...). _iso3_at tries the geocoder's name, then its ISO 3166-1
code, and the prefill says "No country at <lat>, <lon>." where there is none.

Over 306 reverse-geocoded points (222 country names): 210 resolve as before,
the 11 now resolve, Sint Maarten moves from NLD to SXM. Every code and name in
SWEET's own table resolves as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Changelog: cite #80 and RMI/WasteMAP#859

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working model-output-change Changes model OUTPUT values (expected progress, not breaking); results differ from prior runs test Adds or modifies tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant