diff --git a/SWEET_python/advanced_dst.py b/SWEET_python/advanced_dst.py index e4d5ffc..ef119c5 100644 --- a/SWEET_python/advanced_dst.py +++ b/SWEET_python/advanced_dst.py @@ -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." ) @@ -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." @@ -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) diff --git a/SWEET_python/dst_common.py b/SWEET_python/dst_common.py index 68788de..2548c98 100644 --- a/SWEET_python/dst_common.py +++ b/SWEET_python/dst_common.py @@ -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} @@ -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, diff --git a/changelog/2026-08.md b/changelog/2026-08.md new file mode 100644 index 0000000..0e87db1 --- /dev/null +++ b/changelog/2026-08.md @@ -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)) diff --git a/changelog/README.md b/changelog/README.md index 0c62df3..66088bf 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-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