Skip to content

A disposal site can accept waste the city did not send it - #65

Merged
HughRunyan merged 5 commits into
am/adst/proto-work-focusfrom
a-site-can-receive-waste-the-city-did-not-send
Sep 9, 2026
Merged

A disposal site can accept waste the city did not send it#65
HughRunyan merged 5 commits into
am/adst/proto-work-focusfrom
a-site-can-receive-waste-the-city-did-not-send

Conversation

@HughRunyan

@HughRunyan HughRunyan commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Purpose

A city-level ADST run shares the city's landfilled residual out across the city's own sites: landfill_split_timeline is fractions that must sum to 1.0, so every ton at every site came from the city by construction. Real sites are not like that. A regional landfill takes waste from neighbouring municipalities, from private haulers, from a catchment the baseline never describes — and the operator knows the gate total, not this city's share of it.

As a WasteMAP modeller, I want a disposal site to be able to accept waste my city did not send it, so that a site's modelled emissions match the hole in the ground rather than only the slice my city is answerable for — without my city's own inventory changing because a neighbour started tipping there.

What changes

CityLandfillSpec gains an optional accepted_waste_mass: everything crossing that site's weighbridge in a year, from every source. Whatever it exceeds the city's own allocation is modelled as a second stream at the same site — its own Landfill, its own pass through the decay kernel, deposited and windowed on exactly the same terms as the city's waste, because it is the same hole in the ground.

run_advanced_dst_city(request, with_site_emissions=True) returns the split: per landfill in request order, city, other and total, in tons of methane per year.

New behaviour lives in SWEET_python/site_inflow.py. city_params.py is untouched — not one line. The module is free functions taking City and Landfill, which is the convention dst_common.build_landfill already sets: a landfill's construction already lives outside landfill.py.

The two guarantees

The city's own emissions do not move. Structural, not arithmetic: City.sum_landfill_emissions sums parameters.landfills, and the surplus landfills are never put in that list. The test asserts frame.equals(frame) — bit-identity, not a tolerance. A request without the field takes exactly the path it took before.

A site's total is its streams, summed exactly. The kernel is linear in deposited mass (measured at ~1e-16 relative; E(0) is exactly 0.0), so "the city's share of this site's methane" is a measured quantity rather than an allocation convention.

The bug this is most likely to be "simplified" into

Attributing by tonnage — city tons / site tons × site total — is wrong wherever a site's mix of sources moves over time, because this year's emissions come from decades of deposit cohorts each with their own split. Measured here at up to 28% error for a site with a growing neighbour, in a direction that looks entirely plausible: at 2050 the city was 7.2% of intake but 10.1% of emissions. test_the_city_share_is_not_this_years_tonnage_ratio fails if anyone reintroduces it.

Landfill.doing_fancy_ox is the one thing that would genuinely break superposition — its body derives an oxidation factor from one year's available methane and clips it three times. It is hardcoded False today; require_linear now raises rather than letting the split silently become meaningless if it ever flips.

A note on composition

The surplus carries the city's post-diversion residual mix. This is a modelling default, not a numerical requirement, and the PR is explicit about that: k is computed once per variant from the city's generated composition and handed identically to every landfill, so a stream's own mix moves only its per-component deposited masses — two streams at one site with entirely different mixes still superpose exactly. (#785 quotes a 27.2% error for differing compositions; that is about blending two mixes into a single kernel run, which this does not do.) The residual is the default because it is the defensible reading of a gate observation. site_inflow.residual_composition is the single function a later "a site states its own mix" change replaces.

Model-output change

No existing output moves. Every request without accepted_waste_mass is bit-identical, which the 214-test suite (unchanged) and an explicit equality test both cover. The label is not applied for that reason; the WasteMAP PR that starts sending the field carries it, because per-site displayed totals do move there.

Acceptance criteria

  • A site can state a gate total above the city's allocation and the surplus is modelled.
  • The city's baseline and scenario frames are bit-identical with and without the field.
  • A site's city + other == total, to floating point.
  • The city's share equals what that site would have emitted with no other source.
  • A stated total below the city's allocation is floored, not treated as a reduction of the city's stream.
  • The surplus is combusted, windowed and spliced on the same terms as the city's stream.
  • mass_flow's sites band still reports only the city's deposits, so sum(sites) == landfilled still holds.
  • The limits endpoint ignores the new field.
  • city_params.py unchanged.
  • Changelog entry + README line.

Definition of done

  • Acceptance criteria met
  • 214 tests pass (12 new)
  • Changelog updated (changelog/2026-09.md)
  • Reviewed and merged

Cross-repo

Base is am/adst/proto-work-focus, not main. Staging is deployed from WasteMAP's focus branch, and every WasteMAP deploy workflow builds the API image with --build-arg SWEET_PYTHON_REF=${{ github.ref_name }} (backend/Dockerfile:29). So a staging deploy run from am/adst/proto-work-focus installs the SWEET_python branch of that same name — if this landed on main instead, staging would build an API that silently ignores accepted_waste_mass.

Test CI is a separate mechanism and is already satisfied: it matches on the feature branch name, and a-site-can-receive-waste-the-city-did-not-send exists in both repos.

The companion is RMI/WasteMAP#791, also based on am/adst/proto-work-focus. This one merges first. main picks the feature up whenever the focus branch is merged down, as the rest of the ADST work will be.

🤖 Generated with Claude Code

@HughRunyan HughRunyan added enhancement New feature or request python Pull requests that update python code test Adds or modifies tests labels Sep 9, 2026
HughRunyan added a commit that referenced this pull request Sep 9, 2026
HughRunyan and others added 4 commits September 9, 2026 11:59
Tests only; the feature they describe does not exist yet, so they fail.
Written first because the two guarantees are the whole design and are easy
to lose to a plausible-looking shortcut:

- the city's emissions must not move when other-source waste is added,
  which the implementation has to make structural rather than arithmetic;
- a site's total must be its streams summed exactly, which is only a
  physical quantity because the kernel is linear in deposited mass.

The last test is the regression guard for the shortcut: attributing by
this year's tonnage ratio instead of running the kernel on the city's own
deposit series. Measured here at up to 28% error where the city's share of
a site moves over time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`landfill_split_timeline` is fractions summing to 1, so until now every ton
at every site came from the city by construction. A spec may now state
`accepted_waste_mass` -- its whole gate total -- and whatever that exceeds
the city's allocation is modelled as a second stream at the same site.

The city's own emissions cannot move, and the reason is structural rather
than arithmetic: `City.sum_landfill_emissions` sums `parameters.landfills`,
and the surplus landfills are never put in that list. The regression test
asserts frame equality, not a tolerance.

A site's total is its streams summed exactly, because the kernel is linear
in deposited mass (~1e-16 relative, E(0) == 0.0). That holds only while
every stream at a site shares one `k`, which is a step function of
composition -- so the surplus carries the city's post-diversion residual
mix, which is also what a gate observation physically is.

New behaviour lives in `site_inflow.py`, following the convention
`dst_common.build_landfill` already sets: a landfill's construction lives
outside `landfill.py`. `city_params.py` is untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The module said two streams at one site must share a composition or they
stop superposing, citing #785's 27.2% figure. That figure is about blending
two mixes into one kernel run, which this does not do: `k` is computed once
per variant from the city's generated mix and handed to every landfill, so
a stream's own mix moves only its per-component masses and superposition
holds regardless. Verified against the code and by measurement.

The residual mix is still the default -- a gate observation is downstream
of whatever diversion happened upstream of it -- but it is a modelling
judgement, not a constraint, and a later PR can lift it.

Also adds `require_linear`: `doing_fancy_ox` is the one thing that would
genuinely break superposition, so the split refuses to run rather than
returning an attribution nobody could defend. And four tests pinning what
the rest of the engine is entitled to keep assuming.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@HughRunyan
HughRunyan force-pushed the a-site-can-receive-waste-the-city-did-not-send branch from b81c69c to ee7c3cf Compare September 9, 2026 18:59
@HughRunyan
HughRunyan changed the base branch from main to am/adst/proto-work-focus September 9, 2026 18:59
@HughRunyan
HughRunyan requested a balanced review from Copilot September 9, 2026 19:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Zero city-generated mass can incorrectly erase positive external intake, and scenario-specific behavior lacks coverage.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds support for modelling waste accepted from outside a city while preserving the city’s emissions inventory.

Changes:

  • Adds accepted_waste_mass and per-site emissions reporting.
  • Models surplus waste as independent landfill streams.
  • Adds regression tests and changelog documentation.
File summaries
File Description
SWEET_python/advanced_dst_city.py Integrates surplus streams and site-level output.
SWEET_python/site_inflow.py Implements surplus allocation and emissions aggregation.
tests/test_site_other_source.py Tests external-source waste behavior.
changelog/2026-09.md Documents the feature.
changelog/README.md Updates the changelog index.
Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 3
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread SWEET_python/site_inflow.py Outdated
Comment thread SWEET_python/advanced_dst_city.py
Comment thread tests/test_site_other_source.py Outdated
Three review findings, all real.

The surplus is split by the city's post-diversion residual mix, and in a
year the city buries nothing that mix is 0/0. The fallback was the
composition as *masses*, which is all-zero in exactly the years it was
needed -- so every share stayed zero and the surplus was deposited as
nothing at all. A site taking regional waste while its city has not
started collecting yet is an ordinary shape: 200,000 t/yr across thirty
years came back as 0 t CH4, silently. The fallback is now the composition
as shares, which survives a zero-mass year.

The frame-level pre-implement splice had no test that could fail. A
variant-differing gate total cannot exercise it -- `variant_series`
already splices the series upstream -- so the case that reaches it is a
window or `combusts` that differs between variants. Pinned with a site
that starts incinerating at the implement year, which without the splice
unburies 90% of its own history.

And the test module documented an `other_source_mass` field that never
existed, describing it as the outside share rather than the gate total.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@HughRunyan
HughRunyan merged commit a8d915f into am/adst/proto-work-focus Sep 9, 2026
2 checks passed
@HughRunyan
HughRunyan deleted the a-site-can-receive-waste-the-city-did-not-send branch September 9, 2026 20:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request python Pull requests that update python code test Adds or modifies tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants