From 4ccc48a7859918bac911d28f8bc4cc77133a6748 Mon Sep 17 00:00:00 2001 From: Hugh Runyan Date: Thu, 2 Jul 2026 21:36:11 -0700 Subject: [PATCH 1/3] Make all waste types eligible for combustion Add metal, glass, and other to every combustion-eligibility set so all 10 waste types can be combusted (incinerated). The model computes methane only, so combustion simply diverts mass from the landfill -- combusted mass produces no methane -- and there is no reason to hold the three inert types out of that diversion. Eligibility sets updated: - city_params.py: all 5 live combustion sets (City.__init__ default, load_csv_new, import_basics, and the two import_basics_site paths) - config.py: combustion_components template - dst_allocation.py: DEFAULT_ELIGIBILITY mirror The min-cost-max-flow allocator is unchanged; only the combustion eligibility set it reads is widened. dst_allocation.py's spare_combustibles two-tier cost now degrades to a documented no-op (no non-combustible types remain to spare, and the combustion remainder no longer depends on the 3-treatment allocation). Tests/harness updated for the new semantics (a metal-only city can now combust): test_dst_allocation.py, test_mass_checker_integration.py, dst_allocation_prototype.py. Adds changelog/2026-07.md. Model-output change (not breaking): the combustion composition renormalizes over 10 types and the incineration remainder grows to the full post-diversion leftover. Co-Authored-By: Claude Opus 4.8 --- SWEET_python/city_params.py | 15 +++++++++++++++ SWEET_python/config.py | 13 ++++++++++++- SWEET_python/dst_allocation.py | 7 ++++++- changelog/2026-07.md | 7 +++++++ changelog/README.md | 1 + dst_allocation_prototype.py | 8 ++++---- tests/test_dst_allocation.py | 3 ++- tests/test_mass_checker_integration.py | 2 +- 8 files changed, 48 insertions(+), 8 deletions(-) create mode 100644 changelog/2026-07.md diff --git a/SWEET_python/city_params.py b/SWEET_python/city_params.py index d4e2ddf..81411de 100644 --- a/SWEET_python/city_params.py +++ b/SWEET_python/city_params.py @@ -203,6 +203,9 @@ def __init__(self, city_name: str): "textiles", "plastic", "rubber", + "metal", + "glass", + "other", }, "recycling": { "wood", @@ -1015,6 +1018,9 @@ def load_andre_params(self, row, backfill=False): "textiles", "plastic", "rubber", + "metal", + "glass", + "other", ] ) value1 = float(row["waste_treatment_incineration_percent"]) @@ -3074,6 +3080,9 @@ def import_basics(self, row) -> None: "textiles", "plastic", "rubber", + "metal", + "glass", + "other", ] ) self.recycling_components = set( @@ -3482,6 +3491,9 @@ def _is_transient_db_error(err: Exception) -> bool: "textiles", "plastic", "rubber", + "metal", + "glass", + "other", ] ) self.recycling_components = set( @@ -3751,6 +3763,9 @@ def _is_transient_db_error(err: Exception) -> bool: "textiles", "plastic", "rubber", + "metal", + "glass", + "other", ] ) self.recycling_components = set( diff --git a/SWEET_python/config.py b/SWEET_python/config.py index b010eeb..80cdd5b 100644 --- a/SWEET_python/config.py +++ b/SWEET_python/config.py @@ -41,7 +41,18 @@ "anaerobic_mass": None, # Set this either way not both # Combustion params "combustion_components": set( - ["food", "green", "wood", "paper_cardboard", "textiles", "plastic", "rubber"] + [ + "food", + "green", + "wood", + "paper_cardboard", + "textiles", + "plastic", + "rubber", + "metal", + "glass", + "other", + ] ), "combustion_fraction": 0.1, "combustion_mass": None, # Set this either way not both diff --git a/SWEET_python/dst_allocation.py b/SWEET_python/dst_allocation.py index 8753572..995ffb6 100644 --- a/SWEET_python/dst_allocation.py +++ b/SWEET_python/dst_allocation.py @@ -34,7 +34,7 @@ "compost": {"food", "green", "wood", "paper_cardboard"}, "anaerobic": {"food", "green", "wood", "paper_cardboard"}, "combustion": {"food", "green", "wood", "paper_cardboard", - "textiles", "plastic", "rubber"}, + "textiles", "plastic", "rubber", "metal", "glass", "other"}, "recycling": {"wood", "paper_cardboard", "textiles", "plastic", "rubber", "metal", "glass", "other"}, } @@ -48,6 +48,11 @@ # so when combustion > 0 the three treatments spare combustibles for it. The # per-type contention degree breaks ties within a tier (protects shared # wood/paper). Max degree (3) << penalty, so the tiers never cross. +# NOTE: with the current config every waste type is combustion-eligible, so this +# penalty is added uniformly to all edges and is a no-op -- there are no +# non-combustible types to steer the other treatments onto, and the combustion +# remainder (total - compost - anaerobic - recycling) is independent of the +# allocation. Kept for the general case (if a non-combustible type is reintroduced). _COMBUSTIBLE_PENALTY = 1000 diff --git a/changelog/2026-07.md b/changelog/2026-07.md new file mode 100644 index 0000000..45fdb74 --- /dev/null +++ b/changelog/2026-07.md @@ -0,0 +1,7 @@ +# SWEET_python Changelog — July 2026 + +**Highlights:** All ten waste types are now eligible for combustion (incineration) diversion — `metal`, `glass`, and `other` were previously held out as non-combustible and routed only to recycling. Because the model computes methane only (no combustion CO₂), combustion simply diverts mass away from the landfill, so any combusted waste produces no methane. This is a model-output change: for a given incineration level, the combusted composition and the resulting net-of-diversion landfill stream shift. + +## Changed +- Combustion (incineration) is now eligible for **all ten** waste types. Added `metal`, `glass`, and `other` to every combustion-eligibility set: the five live definitions in `city_params.py` (the `City.__init__` default plus the `load_csv_new`, `import_basics`, and two `import_basics_site` construction paths), the `params` template in `config.py`, and the `DEFAULT_ELIGIBILITY` mirror in `dst_allocation.py`. Previously combustion was restricted to seven types (food, green, wood, paper_cardboard, textiles, plastic, rubber), which modeled metal/glass/other as inert and recycling-only. Since the model is methane-only, combustion just removes mass from the landfill (combusted mass generates no methane), so this widens a mass-flow pathway rather than adding an emissions pathway. **Model-output change:** the per-type combustion composition now renormalizes over ten types (the existing types' shares drop and metal/glass/other pick up a share), and the leftover-combustible remainder available to the incineration slider grows to the full leftover after compost/anaerobic/recycling. ([#XX](https://github.com/RMI/SWEET_python/pull/XX)) +- Documented that `dst_allocation.py`'s `spare_combustibles` two-tier cost degrades to a no-op under universal combustion eligibility: with no non-combustible types left, there is nothing to steer the other treatments onto and the combustion remainder (`total − compost − anaerobic − recycling`) no longer depends on the allocation. The mechanism is retained for the general case. Verified with a 40k-case fuzz sweep (identical feasibility and allocation with the penalty on vs. off). ([#XX](https://github.com/RMI/SWEET_python/pull/XX)) diff --git a/changelog/README.md b/changelog/README.md index 084c972..0c62df3 100644 --- a/changelog/README.md +++ b/changelog/README.md @@ -15,6 +15,7 @@ The project does not publish semantic version tags, so releases are tracked by Newest first: +- [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 - [2026-04](2026-04.md) — SDST baseline/scenario oxidation hardening against pandas dtype bugs, new PM2.5/PM10 particulate emissions from flared methane diff --git a/dst_allocation_prototype.py b/dst_allocation_prototype.py index e8a58ed..0a2546c 100644 --- a/dst_allocation_prototype.py +++ b/dst_allocation_prototype.py @@ -54,13 +54,13 @@ "compost": {"food", "green", "wood", "paper_cardboard"}, "anaerobic": {"food", "green", "wood", "paper_cardboard"}, "combustion": {"food", "green", "wood", "paper_cardboard", - "textiles", "plastic", "rubber"}, + "textiles", "plastic", "rubber", "metal", "glass", "other"}, "recycling": {"wood", "paper_cardboard", "textiles", "plastic", "rubber", "metal", "glass", "other"}, } NON_COMBUSTION = ("compost", "anaerobic", "recycling") COMBUSTIBLE = ELIGIBILITY["combustion"] # types combustion can take -NON_COMBUSTIBLE = set(WASTE_TYPES) - COMBUSTIBLE # metal, glass, other +NON_COMBUSTIBLE = set(WASTE_TYPES) - COMBUSTIBLE # now empty: all types combustible # 1e9 grid: feasibility decided to ~1e-9, far below slider/display precision. SCALE = 10 ** 9 @@ -393,8 +393,8 @@ def wf(**kw): "remainder 0 -> infeasible, must not ZeroDivisionError"), ("C-divzero combustion0 ok", wf(paper_cardboard=1.0), {"compost": 0.60, "recycling": 0.40, "combustion": 0.0}, True, ""), - ("C combustion on non-combustible only", wf(metal=1.0), - {"combustion": 0.10}, False, "remainder 0"), + ("C combustion on metal only (now combustible)", wf(metal=1.0), + {"combustion": 0.10}, True, "all types combustible -> remainder 1.0"), ("C full 4-slider tight boundary", wf(food=0.25, green=0.10, wood=0.05, paper_cardboard=0.15, plastic=0.15, metal=0.10, glass=0.10, textiles=0.05), diff --git a/tests/test_dst_allocation.py b/tests/test_dst_allocation.py index 0951003..15012c6 100644 --- a/tests/test_dst_allocation.py +++ b/tests/test_dst_allocation.py @@ -201,7 +201,8 @@ def solve_with_combustion(waste, targets): {"compost": 0.60, "recycling": 0.40, "combustion": 0.05}, False), ("C_divzero_combustion0_ok", wf(paper_cardboard=1.0), {"compost": 0.60, "recycling": 0.40, "combustion": 0.0}, True), - ("C_combustion_noncombustible_only", wf(metal=1.0), {"combustion": 0.10}, False), + # metal/glass/other are now combustion-eligible: a 100%-metal city CAN combust. + ("C_combustion_metal_now_combustible", wf(metal=1.0), {"combustion": 0.10}, True), ("C_full_4slider_tight_boundary", wf(food=0.25, green=0.10, wood=0.05, paper_cardboard=0.15, plastic=0.15, metal=0.10, glass=0.10, textiles=0.05), diff --git a/tests/test_mass_checker_integration.py b/tests/test_mass_checker_integration.py index 9e02336..9565665 100644 --- a/tests/test_mass_checker_integration.py +++ b/tests/test_mass_checker_integration.py @@ -24,7 +24,7 @@ ELIG = { "compost": ["food", "green", "wood", "paper_cardboard"], "anaerobic": ["food", "green", "wood", "paper_cardboard"], - "combustion": ["food", "green", "wood", "paper_cardboard", "textiles", "plastic", "rubber"], + "combustion": ["food", "green", "wood", "paper_cardboard", "textiles", "plastic", "rubber", "metal", "glass", "other"], "recycling": ["wood", "paper_cardboard", "textiles", "plastic", "rubber", "metal", "glass", "other"], } From 86a209b7325ee2142b81d97f0863f5ea7e58fda6 Mon Sep 17 00:00:00 2001 From: Hugh Runyan Date: Wed, 8 Jul 2026 14:08:03 -0700 Subject: [PATCH 2/3] changelog: link 2026-07 combustion entry to PR #33 Co-Authored-By: Claude Opus 4.8 --- changelog/2026-07.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog/2026-07.md b/changelog/2026-07.md index 45fdb74..a7dc23b 100644 --- a/changelog/2026-07.md +++ b/changelog/2026-07.md @@ -3,5 +3,5 @@ **Highlights:** All ten waste types are now eligible for combustion (incineration) diversion — `metal`, `glass`, and `other` were previously held out as non-combustible and routed only to recycling. Because the model computes methane only (no combustion CO₂), combustion simply diverts mass away from the landfill, so any combusted waste produces no methane. This is a model-output change: for a given incineration level, the combusted composition and the resulting net-of-diversion landfill stream shift. ## Changed -- Combustion (incineration) is now eligible for **all ten** waste types. Added `metal`, `glass`, and `other` to every combustion-eligibility set: the five live definitions in `city_params.py` (the `City.__init__` default plus the `load_csv_new`, `import_basics`, and two `import_basics_site` construction paths), the `params` template in `config.py`, and the `DEFAULT_ELIGIBILITY` mirror in `dst_allocation.py`. Previously combustion was restricted to seven types (food, green, wood, paper_cardboard, textiles, plastic, rubber), which modeled metal/glass/other as inert and recycling-only. Since the model is methane-only, combustion just removes mass from the landfill (combusted mass generates no methane), so this widens a mass-flow pathway rather than adding an emissions pathway. **Model-output change:** the per-type combustion composition now renormalizes over ten types (the existing types' shares drop and metal/glass/other pick up a share), and the leftover-combustible remainder available to the incineration slider grows to the full leftover after compost/anaerobic/recycling. ([#XX](https://github.com/RMI/SWEET_python/pull/XX)) -- Documented that `dst_allocation.py`'s `spare_combustibles` two-tier cost degrades to a no-op under universal combustion eligibility: with no non-combustible types left, there is nothing to steer the other treatments onto and the combustion remainder (`total − compost − anaerobic − recycling`) no longer depends on the allocation. The mechanism is retained for the general case. Verified with a 40k-case fuzz sweep (identical feasibility and allocation with the penalty on vs. off). ([#XX](https://github.com/RMI/SWEET_python/pull/XX)) +- Combustion (incineration) is now eligible for **all ten** waste types. Added `metal`, `glass`, and `other` to every combustion-eligibility set: the five live definitions in `city_params.py` (the `City.__init__` default plus the `load_csv_new`, `import_basics`, and two `import_basics_site` construction paths), the `params` template in `config.py`, and the `DEFAULT_ELIGIBILITY` mirror in `dst_allocation.py`. Previously combustion was restricted to seven types (food, green, wood, paper_cardboard, textiles, plastic, rubber), which modeled metal/glass/other as inert and recycling-only. Since the model is methane-only, combustion just removes mass from the landfill (combusted mass generates no methane), so this widens a mass-flow pathway rather than adding an emissions pathway. **Model-output change:** the per-type combustion composition now renormalizes over ten types (the existing types' shares drop and metal/glass/other pick up a share), and the leftover-combustible remainder available to the incineration slider grows to the full leftover after compost/anaerobic/recycling. ([#33](https://github.com/RMI/SWEET_python/pull/33)) +- Documented that `dst_allocation.py`'s `spare_combustibles` two-tier cost degrades to a no-op under universal combustion eligibility: with no non-combustible types left, there is nothing to steer the other treatments onto and the combustion remainder (`total − compost − anaerobic − recycling`) no longer depends on the allocation. The mechanism is retained for the general case. Verified with a 40k-case fuzz sweep (identical feasibility and allocation with the penalty on vs. off). ([#33](https://github.com/RMI/SWEET_python/pull/33)) From 930fe5edf745c88030b3ec34b486c8072fee541c Mon Sep 17 00:00:00 2001 From: Hugh Runyan Date: Thu, 9 Jul 2026 14:23:54 -0700 Subject: [PATCH 3/3] changelog: drop non-reproducible 40k-case count from no-op claim Address Copilot review on PR #33. The "40k-case fuzz sweep" cited for the spare_combustibles no-op refers to an ad-hoc penalty-on-vs-off run that isn't part of the committed harness, and it reads as inconsistent with the ~400k solver-vs-oracle harness documented elsewhere. Keep the verified claim, drop the specific count. Co-Authored-By: Claude Opus 4.8 --- changelog/2026-07.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/2026-07.md b/changelog/2026-07.md index a7dc23b..2b04f52 100644 --- a/changelog/2026-07.md +++ b/changelog/2026-07.md @@ -4,4 +4,4 @@ ## Changed - Combustion (incineration) is now eligible for **all ten** waste types. Added `metal`, `glass`, and `other` to every combustion-eligibility set: the five live definitions in `city_params.py` (the `City.__init__` default plus the `load_csv_new`, `import_basics`, and two `import_basics_site` construction paths), the `params` template in `config.py`, and the `DEFAULT_ELIGIBILITY` mirror in `dst_allocation.py`. Previously combustion was restricted to seven types (food, green, wood, paper_cardboard, textiles, plastic, rubber), which modeled metal/glass/other as inert and recycling-only. Since the model is methane-only, combustion just removes mass from the landfill (combusted mass generates no methane), so this widens a mass-flow pathway rather than adding an emissions pathway. **Model-output change:** the per-type combustion composition now renormalizes over ten types (the existing types' shares drop and metal/glass/other pick up a share), and the leftover-combustible remainder available to the incineration slider grows to the full leftover after compost/anaerobic/recycling. ([#33](https://github.com/RMI/SWEET_python/pull/33)) -- Documented that `dst_allocation.py`'s `spare_combustibles` two-tier cost degrades to a no-op under universal combustion eligibility: with no non-combustible types left, there is nothing to steer the other treatments onto and the combustion remainder (`total − compost − anaerobic − recycling`) no longer depends on the allocation. The mechanism is retained for the general case. Verified with a 40k-case fuzz sweep (identical feasibility and allocation with the penalty on vs. off). ([#33](https://github.com/RMI/SWEET_python/pull/33)) +- Documented that `dst_allocation.py`'s `spare_combustibles` two-tier cost degrades to a no-op under universal combustion eligibility: with no non-combustible types left, there is nothing to steer the other treatments onto and the combustion remainder (`total − compost − anaerobic − recycling`) no longer depends on the allocation. The mechanism is retained for the general case. Verified that feasibility and allocation are identical with the penalty on vs. off. ([#33](https://github.com/RMI/SWEET_python/pull/33))