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
51 changes: 40 additions & 11 deletions SWEET_python/advanced_dst.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ class AdvancedDSTRequest(BaseModel):
landfill_type: Variant[LandfillType] = Field(
..., description="Site type: 0 landfill, 1 controlled dump, 2 open dump."
)
depth: Optional[Variant[float]] = Field(
None,
description=(
"Site depth in metres. A controlled/open dump (type 1 or 2) deeper "
"than 5 m has its MCF raised to 0.8 (deep dumps decompose more "
"anaerobically), matching City.sdst_v1_5. Omit to derive MCF from "
"landfill type alone."
),
)
landfill_open_close: Variant[tuple[int, int]] = Field(
..., description="(open_year, close_year) of the site."
)
Expand All @@ -76,6 +85,15 @@ class AdvancedDSTRequest(BaseModel):
biocover: Optional[Variant[YearlyFloat]] = Field(
None, description="Biocover oxidation floor per year (a fraction; defaults to 0)."
)
k_override: Optional[Variant[YearlyFloat]] = Field(
None,
description=(
"Decomposition rate k per year. When supplied, this single rate is "
"applied to every biodegradable component and bypasses the derived "
"(temperature/precipitation/composition) k, matching City.sdst_v1_5's "
"ks_overrides. Omit to derive k."
),
)
temperature: float = Field(10.0, description="Average annual temperature, deg C.")
country: Optional[str] = Field(
None, description="ISO3 country code. Stored for identity; does not affect the math."
Expand Down Expand Up @@ -145,22 +163,33 @@ def run_advanced_dst(request: AdvancedDSTRequest) -> dict[str, pd.DataFrame]:
scenario_mass = common.apply_window(scenario_mass, scenario_open, scenario_close)

# --- Decomposition rates ---
ref_year = min(max(implement_year, int(years.min())), int(years.max()))
ks_baseline, ks_scenario = common.decomposition_rates(
request.temperature,
request.precipitation,
implement_year,
years,
common.representative_vector(baseline_fractions, ref_year),
common.representative_vector(scenario_fractions, ref_year),
)
if request.k_override is not None:
# Caller-supplied k: fan one rate out to all degradable components,
# spliced at implement_year, mirroring City.sdst_v1_5's ks_overrides.
k_baseline, k_scenario = common.variant_series(
request.k_override, years, implement_year, default=None
)
ks_baseline = common.uniform_decomposition_rates(k_baseline)
ks_scenario = common.uniform_decomposition_rates(k_scenario)
else:
ref_year = min(max(implement_year, int(years.min())), int(years.max()))
ks_baseline, ks_scenario = common.decomposition_rates(
request.temperature,
request.precipitation,
implement_year,
years,
common.representative_vector(baseline_fractions, ref_year),
common.representative_vector(scenario_fractions, ref_year),
)

# --- MCF / gas capture / flaring / oxidation series ---
baseline_type = int(request.landfill_type["baseline"])
scenario_type = int(request.landfill_type["scenario"]) if request.landfill_type["scenario"] is not None else baseline_type

mcf_baseline = common.mcf_series(baseline_type, baseline_type, implement_year, years)
mcf_scenario = common.mcf_series(baseline_type, scenario_type, implement_year, years)
baseline_depth = common.variant_get(request.depth, "baseline")
scenario_depth = common.variant_get(request.depth, "scenario")
mcf_baseline = common.mcf_series(baseline_type, baseline_type, implement_year, years, baseline_depth, baseline_depth)
mcf_scenario = common.mcf_series(baseline_type, scenario_type, implement_year, years, baseline_depth, scenario_depth)

gas_baseline, gas_scenario = common.variant_series(request.gas_capture_efficiency, years, implement_year, default=0.0)
flare_baseline, flare_scenario = common.variant_series(request.flaring, years, implement_year, default=common.DEFAULT_FLARE_EFFICIENCY)
Expand Down
49 changes: 45 additions & 4 deletions SWEET_python/dst_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@
MCF_BY_TYPE: List[float] = [1.0, 0.6, 0.4]
SITE_TYPE_NAMES: List[str] = ["landfill", "controlled_dumpsite", "dumpsite"]

# A controlled/open dump deeper than DEEP_SITE_DEPTH_M behaves more like an
# anaerobic engineered landfill, so its MCF is raised to DEEP_DUMP_MCF. This
# mirrors City.sdst_v1_5's depth rule; it does not apply to engineered landfills
# (type 0), whose MCF is already 1.0.
DEEP_SITE_DEPTH_M = 5.0
DEEP_DUMP_MCF = 0.8
DEEP_MCF_DUMP_TYPES = (1, 2) # controlled dump, open dump

# Oxidation factor lookup, mirroring City.sdst_v1_5 / Landfill.estimate_emissions.
OX_NOCAP: Dict[str, float] = {"landfill": 0.1, "controlled_dumpsite": 0.05, "dumpsite": 0.0}
OX_CAP: Dict[str, float] = {"landfill": 0.22, "controlled_dumpsite": 0.1, "dumpsite": 0.0}
Expand Down Expand Up @@ -247,13 +255,46 @@ def oxidation_series(
return pd.Series(values, index=years, dtype=float)


def mcf_series(baseline_type: int, scenario_type: int, implement_year: int, years: pd.Index) -> pd.Series:
"""MCF series for one landfill: baseline type before implement_year, scenario after."""
series = pd.Series(MCF_BY_TYPE[baseline_type], index=years, dtype=float)
series.loc[implement_year:] = MCF_BY_TYPE[scenario_type]
def _mcf_for_type(site_type_idx: int, depth: Optional[float]) -> float:
"""MCF for one site type, raised for deep controlled/open dumps.

A dump (type 1 or 2) deeper than ``DEEP_SITE_DEPTH_M`` gets ``DEEP_DUMP_MCF``;
otherwise the standard per-type MCF applies. ``depth`` of ``None`` (the
default when no depth is supplied) leaves MCF at the per-type value.
"""
if depth is not None and depth > DEEP_SITE_DEPTH_M and site_type_idx in DEEP_MCF_DUMP_TYPES:
return DEEP_DUMP_MCF
return MCF_BY_TYPE[site_type_idx]


def mcf_series(
baseline_type: int,
scenario_type: int,
implement_year: int,
years: pd.Index,
baseline_depth: Optional[float] = None,
scenario_depth: Optional[float] = None,
) -> pd.Series:
"""MCF series for one landfill: baseline type before implement_year, scenario after.

A controlled/open dump deeper than ``DEEP_SITE_DEPTH_M`` has its MCF raised to
``DEEP_DUMP_MCF`` (deep dumps decompose more anaerobically), matching
City.sdst_v1_5. Depths of ``None`` leave MCF at the per-type value.
"""
series = pd.Series(_mcf_for_type(baseline_type, baseline_depth), index=years, dtype=float)
series.loc[implement_year:] = _mcf_for_type(scenario_type, scenario_depth)
return series


def uniform_decomposition_rates(k: pd.Series) -> DecompositionRates:
"""A DecompositionRates applying one k series to every degradable component.

Mirrors City.sdst_v1_5's ``ks_overrides``, which fans a single caller-supplied
decomposition rate out to all biodegradable components.
"""
return DecompositionRates(food=k, green=k, wood=k, paper_cardboard=k, textiles=k)


def build_landfill(
*,
open_year: int,
Expand Down
7 changes: 7 additions & 0 deletions changelog/2026-08.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# SWEET_python Changelog — August 2026

**Highlights:** The single-site advanced DST (`advanced_dst`) gains two optional inputs — `depth` and `k_override` — that restore the last two site-DST (`City.sdst_v1_5`) levers the adst model had no equivalent for. Both default to "derive as before," so existing adst calls are byte-for-byte unaffected; they are opt-in and not a model-output change for current callers.

## Added
- `AdvancedDSTRequest.depth` (`Optional[Variant[float]]`, metres): a controlled or open dump (landfill type 1 or 2) deeper than 5 m has its methane correction factor raised to 0.8, matching `City.sdst_v1_5`'s deep-dump rule (deep dumps decompose more anaerobically). Implemented in `dst_common.mcf_series`, which now accepts optional per-variant `baseline_depth`/`scenario_depth`; a `None` depth (the default) leaves MCF at the per-type value, and the rule never applies to engineered landfills (type 0). Restores the `/sdst` "Depth" control for adst. ([#40](https://github.com/RMI/SWEET_python/pull/40))
- `AdvancedDSTRequest.k_override` (`Optional[Variant[YearlyFloat]]`): a per-year decomposition rate `k` that, when supplied, is applied uniformly to every biodegradable component and bypasses the derived (temperature/precipitation/composition) k — mirroring `City.sdst_v1_5`'s `ks_overrides`. Implemented via the new `dst_common.uniform_decomposition_rates` helper; spliced at `implement_year` like every other adst scenario input. Restores the `/sdst` "Degradation rate (k)" control for adst. ([#40](https://github.com/RMI/SWEET_python/pull/40))
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-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; both opt-in, no change to existing calls
- [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
Expand Down