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
10 changes: 6 additions & 4 deletions SWEET_python/advanced_dst.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,12 @@ class AdvancedDSTRequest(BaseModel):
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."
"Site depth in metres. For a controlled/open dump (type 1 or 2) the "
"depth selects the IPCC unmanaged category: deeper than 5 m raises "
"MCF to 0.8, at or below 5 m lowers it to 0.4. Omit (the default) "
"when the depth is unknown, which keeps the IPCC uncategorised 0.6 "
"\u2014 an omitted depth is not read as shallow. Never applies to an "
"engineered landfill (type 0), which is 1.0 regardless."
),
)
landfill_open_close: Variant[tuple[int, int]] = Field(
Expand Down
21 changes: 19 additions & 2 deletions SWEET_python/advanced_dst_city.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,17 @@ class CityLandfillSpec(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. For a controlled/open dump (type 1 or 2) the "
"depth selects the IPCC unmanaged category: deeper than 5 m raises "
"MCF to 0.8, at or below 5 m lowers it to 0.4. Omit (the default) "
"when the depth is unknown, which keeps the IPCC uncategorised 0.6 "
"\u2014 an omitted depth is not read as shallow. Never applies to an "
"engineered landfill (type 0), which is 1.0 regardless."
),
)
landfill_open_close: Variant[tuple[int, int]] = Field(
..., description="(open_year, close_year) of this site."
)
Expand Down Expand Up @@ -374,8 +385,14 @@ def run_advanced_dst_city(request: AdvancedDSTCityRequest) -> dict[str, pd.DataF
baseline_shares.append(share_base)
scenario_shares.append(share_scen)

mcf_base = common.mcf_series(base_type, base_type, implement_year, years)
mcf_scen = common.mcf_series(base_type, scen_type, implement_year, years)
base_depth = common.variant_get(spec.depth, "baseline")
scen_depth = common.variant_get(spec.depth, "scenario")
mcf_base = common.mcf_series(
base_type, base_type, implement_year, years, base_depth, base_depth
)
mcf_scen = common.mcf_series(
base_type, scen_type, implement_year, years, base_depth, scen_depth
)
ox_base = common.oxidation_series(base_type, base_type, gas_base, bio_base, implement_year, years)
ox_scen = common.oxidation_series(base_type, scen_type, gas_scen, bio_scen, implement_year, years)
baseline_ox.append(ox_base)
Expand Down
138 changes: 54 additions & 84 deletions SWEET_python/city_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from SWEET_python.landfill import Landfill
from SWEET_python.singapore_k import compute_singapore_k
import SWEET_python.defaults_2019 as defaults_2019
import SWEET_python.mcf as mcf_defaults
import psycopg2
from psycopg2.extras import RealDictCursor
from sqlalchemy import create_engine, text
Expand Down Expand Up @@ -104,13 +105,21 @@ def _build_oxidation_series(default_value, canonical_row, time_series_rows, year
# Cities can have multiple sets of CityParameters, one for each scenario.
# Sets of CityParameters can have one or more landfills, dumpsites, waste to energy, etc.
# Even for modeling a single landfill, City and CityParameters classes need to be used.
def _population_series_from_pop_data(pop_data, iso3, start_year=1990, end_year=2050):
def _population_series_from_pop_data(pop_data, iso3, start_year=MODEL_START_YEAR,
end_year=MODEL_END_YEAR):
"""Extract the WPP2024 per-year population Series for ``iso3`` from ``pop_data``.

``pop_data`` carries ``pop_1990``..``pop_2050`` columns when the yearly WPP
table is available (see helper_functions.load_population_data). Returns a
year-indexed Series, or ``None`` when the columns/country are absent so the
caller falls back to the frozen-CAGR ``growth_rate_*`` scalars.
``pop_data`` carries ``pop_{MODEL_START_YEAR}``..``pop_{MODEL_END_YEAR}`` columns
when the yearly WPP table is available (see helper_functions.load_population_data).
Returns a year-indexed Series, or ``None`` when the columns/country are absent so
the caller falls back to the frozen-CAGR ``growth_rate_*`` scalars.

The bounds default to the modeling window rather than to literals, because the
``all(c in pop_data.columns)`` guard below is all-or-nothing: a pops_yearly.csv
that starts later than MODEL_START_YEAR returns None for EVERY country and
silently reverts the whole run to frozen-CAGR growth. Keeping the default tied to
the constant makes that a loud missing-column mismatch instead of a quiet
regression.
"""
if pop_data is None or iso3 is None:
return None
Expand Down Expand Up @@ -1085,9 +1094,6 @@ def load_andre_params(self, row, backfill=False):
precip_zone = defaults_2019.get_precipitation_zone(precip)
temperature = row["mean_yearly_temp_2000_2021"]

# depth
depth = 3 # m

# k values, which are decomposition rates
# ks = defaults_2019.k_defaults[precip_zone]

Expand Down Expand Up @@ -1802,11 +1808,8 @@ def sinar_city_and_site(self, row, linker, for_trace=False):
"Controlled Dumpsite": 1,
"Dumpsite": 2,
}
mcf_options = {
"Sanitary Landfill": 1,
"Controlled Dumpsite": 0.7,
"Dumpsite": 0.4,
}
# MCF by site type; see SWEET_python.mcf for the values and the depth rule.
mcf_options = dict(mcf_defaults.MCF_BY_SITE_TYPE_NAME)
ox_options = {
"ox_nocap": {
"Sanitary Landfill": 0.1,
Expand All @@ -1824,7 +1827,9 @@ def sinar_city_and_site(self, row, linker, for_trace=False):
"Controlled Dumpsite": 0.45,
"Dumpsite": 0.0,
}
depth = 3
# No depth is available on this path. None means "unknown", which keeps
# MCF at the uncategorised per-type value; it is not read as shallow.
depth = None
landfills = linker["site_id"].unique().tolist()
lifespans = {}
site_types = {}
Expand Down Expand Up @@ -1867,10 +1872,7 @@ def sinar_city_and_site(self, row, linker, for_trace=False):
site_type = site_data.at[0, "site_type"]
site_types[landfill] = site_type
site_type_idx = get_site_type_idx[site_type]
if (depth > 5) and (site_type_idx in (1, 2)):
mcfs[landfill] = 0.8
else:
mcfs[landfill] = mcf_options[site_type]
mcfs[landfill] = mcf_defaults.mcf_for_site(site_type_idx, depth)
if "Yes" in site_data["fgc_lfg_collection_system_in_place"].unique():
gas_capture_presences[landfill] = True
oxidation_values[landfill] = ox_options["ox_cap"][site_type]
Expand Down Expand Up @@ -1955,10 +1957,7 @@ def sinar_city_and_site(self, row, linker, for_trace=False):
site_type = site_data.at[0, "site_type"]
site_types[landfill] = site_type
site_type_idx = get_site_type_idx[site_type]
if (depth > 5) and (site_type in (1, 2)):
mcfs[landfill] = 0.8
else:
mcfs[landfill] = mcf_options[site_type]
mcfs[landfill] = mcf_defaults.mcf_for_site(site_type_idx, depth)
if "Yes" in site_data["fgc_lfg_collection_system_in_place"].unique():
gas_capture_presences[landfill] = True
oxidation_values[landfill] = ox_options["ox_cap"][site_type]
Expand Down Expand Up @@ -2241,11 +2240,6 @@ def site_only_estimate(self, row=None, pop_data=None):
"Controlled Dumpsite": 1,
"Dumpsite": 2,
}
mcf_options = {
"Sanitary Landfill": 1,
"Controlled Dumpsite": 0.7,
"Dumpsite": 0.4,
}
ox_options = {
"ox_nocap": {
"Sanitary Landfill": 0.1,
Expand All @@ -2263,7 +2257,9 @@ def site_only_estimate(self, row=None, pop_data=None):
"Controlled Dumpsite": 0.45,
"Dumpsite": 0.0,
}
depth = 3
# No depth is available on this path. None means "unknown", which keeps
# MCF at the uncategorised per-type value; it is not read as shallow.
depth = None
site_type = row["Site Type"].values[0]
if site_type not in get_site_type_idx.keys():
if self.region in [
Expand All @@ -2289,10 +2285,7 @@ def site_only_estimate(self, row=None, pop_data=None):
gas_capture_presence = False
oxidation_value = ox_options["ox_nocap"][site_type]
gas_capture_efficiency = gas_eff_options[site_type]
if (depth > 5.0) and (site_type_idx in (1, 2)):
mcf = 0.8
else:
mcf = mcf_options[site_type]
mcf = mcf_defaults.mcf_for_site(site_type_idx, depth)
open_date = row['Site Open Year'].fillna(MODEL_START_YEAR).values[0]
if open_date < MODEL_START_YEAR:
open_date = MODEL_START_YEAR
Expand Down Expand Up @@ -2468,11 +2461,6 @@ def site_only_estimate_trace(self, canonical_row=None, time_series_rows=None, po
"Controlled Dumpsite": 1,
"Dumpsite": 2,
}
mcf_options = {
"Sanitary Landfill": 1,
"Controlled Dumpsite": 0.7,
"Dumpsite": 0.4,
}
ox_options = {
"ox_nocap": {
"Sanitary Landfill": 0.1,
Expand Down Expand Up @@ -2584,10 +2572,7 @@ def site_only_estimate_trace(self, canonical_row=None, time_series_rows=None, po
gas_capture_efficiency = 0
gas_capture_efficiency = pd.Series(gas_capture_efficiency, index=self.years_range)

if (depth > 5.0) and (site_type_idx in (1, 2)):
mcf = 0.8
else:
mcf = mcf_options[site_type]
mcf = mcf_defaults.mcf_for_site(site_type_idx, depth)
open_date = canonical_row['site_open_year']
if isinstance(open_date, str):
if open_date[-2:] == '.0':
Expand Down Expand Up @@ -2792,11 +2777,6 @@ def citysite_estimate_trace(self, canonical_row=None, time_series_rows=None, cit
"Controlled Dumpsite": 1,
"Dumpsite": 2,
}
mcf_options = {
"Sanitary Landfill": 1,
"Controlled Dumpsite": 0.7,
"Dumpsite": 0.4,
}
ox_options = {
"ox_nocap": {
"Sanitary Landfill": 0.1,
Expand Down Expand Up @@ -2868,10 +2848,7 @@ def citysite_estimate_trace(self, canonical_row=None, time_series_rows=None, cit
gas_capture_efficiency = gas_eff_options[site_type]
gas_capture_efficiency = pd.Series(gas_capture_efficiency, index=self.years_range)

if (depth > 5.0) and (site_type_idx in (1, 2)):
mcf = 0.8
else:
mcf = mcf_options[site_type]
mcf = mcf_defaults.mcf_for_site(site_type_idx, depth)
open_date = canonical_row['site_open_year']
if isinstance(open_date, str):
if open_date[-2:] == '.0':
Expand Down Expand Up @@ -4336,7 +4313,9 @@ def _calculate_divs(self, advanced_baseline=False, advanced_dst=False) -> None:
open_date=MODEL_START_YEAR,
close_date=MODEL_END_YEAR,
site_type="dumpsite",
mcf=pd.Series(0.4, index=years),
# The generic city split carries no depth, so the dumpsite bucket
# takes the uncategorised MCF (see SWEET_python.mcf).
mcf=pd.Series(mcf_defaults.MCF_UNCATEGORISED, index=years),
city_params_dict=city_params_dict,
city_instance_attrs=city_parameters.city_instance_attrs,
landfill_index=2,
Expand Down Expand Up @@ -7092,10 +7071,14 @@ def implement_dst_changes_simple_v1_5(
scenario_parameters.landfills[2].oxidation_factor.loc[
:implement_year
] = 0.0
# Converting the dumpsite to a controlled dumpsite leaves MCF
# unchanged: both dump types take the uncategorised 0.6 (see
# SWEET_python.mcf). The conversion's benefit here comes from the
# gas capture and oxidation set just above.
scenario_parameters.landfills[2].mcf = pd.Series(
0.7, index=range(MODEL_START_YEAR, MODEL_END_YEAR + 1)
mcf_defaults.MCF_UNCATEGORISED,
index=range(MODEL_START_YEAR, MODEL_END_YEAR + 1),
)
scenario_parameters.landfills[2].mcf.loc[:implement_year] = 0.4
skip_ox = True

if move_gas:
Expand Down Expand Up @@ -7618,9 +7601,8 @@ def implement_dst_changes_advanced(

# Set up new landfills
city_params_dict = self.update_cityparams_dict(scenario_parameters)
# mcfs = [1, 0.7, 0.4] # Should this include ameliorated?
# mcf_ameliorated = [0.7, 0.4, 0.1]
mcf_options = [1, 0.6, 0.4]
# MCF by landfill type index; see SWEET_python.mcf. A depth of None
# means the DST caller had no answer, and keeps the uncategorised value.
gas_capture_efficiencies = {}
gas_capture_efficiencies["ameliorated"] = [0.5, 0.3, 0]
gas_capture_efficiencies["not_ameliorated"] = [0.6, 0.45, 0]
Expand All @@ -7639,14 +7621,10 @@ def implement_dst_changes_advanced(

# Get MCF
old_lf_type = new_landfill_types["baseline"][i]
mcf["baseline"] = mcf_options[old_lf_type]
mcf["scenario"] = mcf_options[lf_type]

if (depths["baseline"][i] > 5) and (old_lf_type in (1, 2)):
mcf["baseline"] = 0.8

if (depths["scenario"][i] > 5) and (lf_type in (1, 2)):
mcf["scenario"] = 0.8
mcf["baseline"] = mcf_defaults.mcf_for_site(
old_lf_type, depths["baseline"][i]
)
mcf["scenario"] = mcf_defaults.mcf_for_site(lf_type, depths["scenario"][i])

# Handle baseline first
if i >= len(new_gas_efficiency["baseline"]):
Expand Down Expand Up @@ -8325,9 +8303,8 @@ def _apply_open_close_window(

# Set up new landfills
city_params_dict = self.update_cityparams_dict(scenario_parameters)
# mcfs = [1, 0.7, 0.4] # Should this include ameliorated?
# mcf_ameliorated = [0.7, 0.4, 0.1]
mcf_options = [1, 0.6, 0.4]
# MCF by landfill type index; see SWEET_python.mcf. A depth of None
# means the DST caller had no answer, and keeps the uncategorised value.
gas_capture_efficiencies = {}
gas_capture_efficiencies["ameliorated"] = [0.5, 0.3, 0]
gas_capture_efficiencies["not_ameliorated"] = [0.6, 0.45, 0]
Expand All @@ -8350,14 +8327,8 @@ def _apply_open_close_window(

# Get MCF
old_lf_type = new_landfill_types["baseline"][0]
mcf["baseline"] = mcf_options[old_lf_type]
mcf["scenario"] = mcf_options[new_lf_type]

if (depths["baseline"][0] > 5) and (old_lf_type in (1, 2)):
mcf["baseline"] = 0.8

if (depths["scenario"][0] > 5) and (new_lf_type in (1, 2)):
mcf["scenario"] = 0.8
mcf["baseline"] = mcf_defaults.mcf_for_site(old_lf_type, depths["baseline"][0])
mcf["scenario"] = mcf_defaults.mcf_for_site(new_lf_type, depths["scenario"][0])

# Handle baseline first
if new_gas_efficiency["baseline"][0] == 0.0:
Expand Down Expand Up @@ -8728,9 +8699,8 @@ def advanced_baseline(

# Set up new landfills
city_params_dict = self.update_cityparams_dict(scenario_parameters)
# mcfs = [1, 0.7, 0.4] # Should this include ameliorated?
# mcf_ameliorated = [0.7, 0.4, 0.1]
mcf_options = [1, 0.6, 0.4]
# MCF by landfill type index; see SWEET_python.mcf. A depth of None
# means the DST caller had no answer, and keeps the uncategorised value.
# mcfs['ameliorated'] = {}
# mcf_options['not_ameliorated'] = {}
# mcfs['ameliorated']['gas_capture'] = [0.18, 0, 0]
Expand All @@ -8749,9 +8719,7 @@ def advanced_baseline(
for i, lf_type in enumerate(new_landfill_types):
# Make the MCF, oxidation, and efficiency vectors
years = pd.Index(range(MODEL_START_YEAR, MODEL_END_YEAR + 1))
mcf = mcf_options[lf_type]
if (depth > 5) and (lf_type in (1, 2)):
mcf = 0.8
mcf = mcf_defaults.mcf_for_site(lf_type, depth)
# Handle no gas capture first
if new_gas_efficiency[i] == 0:
# mcf = mcf_options['not_ameliorated']['no_gas_capture'][lf_type]
Expand Down Expand Up @@ -8977,13 +8945,15 @@ async def sdst_prepopulate(

if site_type in ["Landfill", "Sanitary Landfill"]:
site_type = 0
depth = 100
elif site_type == "Controlled Dumpsite":
site_type = 1
depth = 100
else:
site_type = 2
depth = 3
# This path resolves a site type from a location; it never learns a waste
# depth. None says so. It used to return 100 m for a landfill/controlled
# dump and 3 m for a dumpsite, which the MCF rule read as a firm claim
# that the site was deep or shallow (see SWEET_python.mcf).
depth = None

# SQL query to get average precipitation and temperature using provided latitude and longitude
QUERY_WEATHER = """
Expand Down
21 changes: 20 additions & 1 deletion SWEET_python/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,26 @@
source data, but models treat deposition as starting in MODEL_START_YEAR.
Export and display windows (Climate TRACE submissions, WasteMAP charts) are
filters over this window, never separate modeling horizons.

MODEL_START_YEAR moved 1990 -> 1970 on 2026-08-26. The cutoff is a truncation of
the decay tail, not a neutral choice: methane emitted today comes from decades of
accumulated stock, so zeroing deposition before the cutoff understates every site
with a long landfilling history -- most severely in cold/dry climates, where the
IPCC k values are lowest and the tail is longest. Measured on the 08_24_26 run,
Russia deposited as much municipal waste before 1990 as it did 1990-2021, and
restoring the earlier stock raises its national FOD by ~20%. 1970 is chosen over
1950 because the population series backing the waste projection (WPP2024, via
pops_yearly.csv) is credible per-year that far back while per-capita generation
before ~1970 is not, and because the residual tail before 1970 is small at every
k in defaults_2019.

CHANGING THIS CONSTANT REQUIRES A MATCHING pops_yearly.csv. The waste series is
population-driven, and city_params._population_series_from_pop_data returns None
unless the table carries EVERY column from MODEL_START_YEAR onward -- which
silently drops every country back to the frozen-CAGR growth scalars. Regenerate
with diagnostic_scripts/generate_pops_yearly.py and upload to blob
static_data/pops_yearly.csv BEFORE the constant lands in a run.
"""

MODEL_START_YEAR: int = 1990
MODEL_START_YEAR: int = 1970
MODEL_END_YEAR: int = 2050
Loading