A disposal site can accept waste the city did not send it - #65
Merged
HughRunyan merged 5 commits intoSep 9, 2026
Merged
Conversation
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
force-pushed
the
a-site-can-receive-waste-the-city-did-not-send
branch
from
September 9, 2026 18:59
b81c69c to
ee7c3cf
Compare
There was a problem hiding this comment.
🟡 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_massand 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.
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
deleted the
a-site-can-receive-waste-the-city-did-not-send
branch
September 9, 2026 20:06
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Purpose
A city-level ADST run shares the city's landfilled residual out across the city's own sites:
landfill_split_timelineis 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.What changes
CityLandfillSpecgains an optionalaccepted_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 ownLandfill, 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,otherandtotal, in tons of methane per year.New behaviour lives in
SWEET_python/site_inflow.py.city_params.pyis untouched — not one line. The module is free functions takingCityandLandfill, which is the conventiondst_common.build_landfillalready sets: a landfill's construction already lives outsidelandfill.py.The two guarantees
The city's own emissions do not move. Structural, not arithmetic:
City.sum_landfill_emissionssumsparameters.landfills, and the surplus landfills are never put in that list. The test assertsframe.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 exactly0.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_ratiofails if anyone reintroduces it.Landfill.doing_fancy_oxis 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 hardcodedFalsetoday;require_linearnow 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:
kis 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_compositionis 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_massis 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
city + other == total, to floating point.mass_flow'ssitesband still reports only the city's deposits, sosum(sites) == landfilledstill holds.city_params.pyunchanged.Definition of done
changelog/2026-09.md)Cross-repo
Base is
am/adst/proto-work-focus, notmain. 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 fromam/adst/proto-work-focusinstalls the SWEET_python branch of that same name — if this landed onmaininstead, staging would build an API that silently ignoresaccepted_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-sendexists in both repos.The companion is RMI/WasteMAP#791, also based on
am/adst/proto-work-focus. This one merges first.mainpicks the feature up whenever the focus branch is merged down, as the rest of the ADST work will be.🤖 Generated with Claude Code