From 03714fe13767039f53a36fcf007590375732d9e2 Mon Sep 17 00:00:00 2001 From: Hugh Runyan Date: Thu, 29 Oct 2026 14:38:03 -0700 Subject: [PATCH 1/2] SWEET holds no income table; the caller passes the country's income group country_income.py was a copy of What a Waste 2.0's 2018 World Bank groups, a second source beside the one the ingestion pipeline now loads from the World Bank. It and site_type_defaults.income_for are deleted; the site-type rule's high-income branch uses only the income the caller passes. New arguments so WasteMAP can pass its build's income: sdst_prepopulate income_by_iso3, load_andre_params income_id, dst_baseline_blank income_id (through disposal_split_for). Climate TRACE already passes its row's income. Co-Authored-By: Claude Opus 5.5 --- SWEET_python/MODEL_DECISIONS.md | 4 +- SWEET_python/city_params.py | 30 ++- SWEET_python/country_income.py | 241 ------------------- SWEET_python/defaults_2019.py | 8 +- SWEET_python/site_type_defaults.py | 26 +- changelog/2026-10.md | 18 ++ tests/test_custom_location_disposal_split.py | 15 +- tests/test_site_type_defaults.py | 62 ++--- 8 files changed, 101 insertions(+), 303 deletions(-) delete mode 100644 SWEET_python/country_income.py diff --git a/SWEET_python/MODEL_DECISIONS.md b/SWEET_python/MODEL_DECISIONS.md index 6de28fd..cae73ac 100644 --- a/SWEET_python/MODEL_DECISIONS.md +++ b/SWEET_python/MODEL_DECISIONS.md @@ -168,7 +168,7 @@ A site or city with no recorded type is assumed a sanitary landfill or an open d - **Why:** Site type sets MCF, oxidation and the gas-capture default, and most sites and many cities record no type. - **Options, best first:** 1. `recorded`: the type the caller supplies. - 2. `sanitary-by-country`: sanitary landfill if high income, in a listed region, or China, Russia, Mexico, Brazil or Costa Rica. Never Mongolia or Yemen. + 2. `sanitary-by-country`: sanitary landfill if high income, in a listed region, or China, Russia, Mexico, Brazil or Costa Rica. Never Mongolia or Yemen. The caller passes the income group; SWEET holds none, so without one only region and named countries count. 3. `dumpsite`: open dump otherwise, including a missing or unplaceable country. Never a controlled dump. - **Paths:** - `ct-site`: Climate TRACE fills the type before the call, with its own income groups; SWEET applies the rule again to a blank type. @@ -178,7 +178,7 @@ A site or city with no recorded type is assumed a sanitary landfill or an open d - **Better:** Classify each site from its area, intake or imagery, not its country's income, and use one fallback order on every path. - **Value:** `SWEET_python.site_type_defaults`: `LANDFILL_DEFAULT_COUNTRIES`, `DUMPSITE_COUNTRIES`, `LANDFILL_DEFAULT_REGIONS` - **Code:** `SWEET_python/site_type_defaults.py` -- **Reviewed:** 2026-10-22 +- **Reviewed:** 2026-10-29 ### `waste-composition` diff --git a/SWEET_python/city_params.py b/SWEET_python/city_params.py index 97fb7c8..7772fb3 100644 --- a/SWEET_python/city_params.py +++ b/SWEET_python/city_params.py @@ -1502,7 +1502,7 @@ def load_csv_new( self.baseline_parameters = city_parameters - def load_andre_params(self, row, backfill=False): + def load_andre_params(self, row, backfill=False, income_id: Optional[str] = None): """ Loads model parameters from the internal RMI WasteMAP database. Defaults are used where data is missing, incomplete, or incompatible. @@ -1510,6 +1510,10 @@ def load_andre_params(self, row, backfill=False): Args: row (tuple): row[0] is the index of the row in the dataframe used for input, row[1] is the row itself. + income_id: The country's World Bank income group, for the landfill/dump + split's last fallback (the shared site-type rule). + SWEET holds no income table; without one, only region and named + countries decide. Returns: None @@ -1880,7 +1884,7 @@ def load_andre_params(self, row, backfill=False): } self.landfill_split_defaults = True else: - if site_type_defaults.landfill_is_default(iso3): + if site_type_defaults.landfill_is_default(iso3, income_id): split_fractions = { "landfill_w_capture": 0, "landfill_wo_capture": 1, @@ -1971,7 +1975,7 @@ def load_andre_params(self, row, backfill=False): } self.landfill_split_defaults = True else: - if site_type_defaults.landfill_is_default(iso3): + if site_type_defaults.landfill_is_default(iso3, income_id): split_fractions = { "landfill_w_capture": 0.0, "landfill_wo_capture": 1.0, @@ -2004,7 +2008,7 @@ def load_andre_params(self, row, backfill=False): # Normalize landfills to 1 split_total = sum([x for x in split_fractions.values()]) if split_total == 0: - if site_type_defaults.landfill_is_default(iso3): + if site_type_defaults.landfill_is_default(iso3, income_id): split_fractions = { "landfill_w_capture": 0.0, "landfill_wo_capture": 1.0, @@ -4833,6 +4837,7 @@ def dst_baseline_blank( precipitation: float, temperature: float, population_series: Optional[pd.Series] = None, + income_id: Optional[str] = None, ) -> None: """ Initializes the baseline scenario with given parameters for a blank/custom city. @@ -4845,6 +4850,9 @@ def dst_baseline_blank( out once by country_population_trajectory and stored. The city's waste grows along it instead of along SWEET's own table. Only its shape counts: the city's waste is set by ``population``. + income_id (str): the country's World Bank income group, from the caller, + for the disposal split's last fallback + (``defaults_2019.disposal_split_for``). SWEET holds no income table. Returns: None @@ -4908,7 +4916,7 @@ def dst_baseline_blank( # Determine waste split fractions, as the cities table does for a city # with no landfill data split_fractions = SplitFractions( - **defaults_2019.disposal_split_for(iso3, region) + **defaults_2019.disposal_split_for(iso3, region, income_id) ) # Instantiate landfill objects @@ -9160,6 +9168,7 @@ async def sdst_prepopulate( sites_list: pd.DataFrame, latlon: Optional[str] = None, site_id: Optional[int] = None, + income_by_iso3: Optional[Dict[str, str]] = None, ) -> None: """ API endpoing function to prepopulate the SDST, @@ -9175,6 +9184,11 @@ async def sdst_prepopulate( sites_list (pd.DataFrame): DataFrame containing site information. latlon (Optional[str]): Latitude and longitude as a string. Defaults to None. site_id (Optional[int]): Site ID. Defaults to None. + income_by_iso3 (Optional[Dict[str, str]]): World Bank income group by + country, from the caller (WasteMAP reads it from the World Bank list + the ingestion pipeline loads). Looked up once the point's country is + known. SWEET holds no income table; a country missing from it gets + only the region and named-country rules. Returns: dict: Dictionary containing data for prepopulation. """ @@ -9198,6 +9212,7 @@ async def sdst_prepopulate( region = defaults_2019.REGION_LOOKUP_ISO3.get(iso3) if region is None: raise ValueError(f"Region for ISO3 code '{iso3}' not found.") + income_id = (income_by_iso3 or {}).get(iso3) else: location = geolocator.reverse((latlon[0], latlon[1]), language="en") # geopy gives None where Nominatim can't place the point (open sea), and @@ -9213,13 +9228,14 @@ async def sdst_prepopulate( region = defaults_2019.REGION_LOOKUP_ISO3.get(iso3) if region is None: raise ValueError(f"Region for ISO3 code '{iso3}' not found.") - if site_type_defaults.landfill_is_default(iso3): + income_id = (income_by_iso3 or {}).get(iso3) + if site_type_defaults.landfill_is_default(iso3, income_id): site_type = "Landfill" else: site_type = "Dumpsite" # A site in the sites list with no recorded type takes the same rule. if pd.isna(site_type) or site_type == "": - site_type = site_type_defaults.default_site_type(iso3) + site_type = site_type_defaults.default_site_type(iso3, income_id) if site_type in ["Landfill", "Sanitary Landfill"]: site_type = 0 diff --git a/SWEET_python/country_income.py b/SWEET_python/country_income.py deleted file mode 100644 index 57f317a..0000000 --- a/SWEET_python/country_income.py +++ /dev/null @@ -1,241 +0,0 @@ -"""World Bank income group by country, as Climate TRACE reads it. - -Copied from ``raw_emissions.raw_world_bank_country_level`` in the WasteMAP -database on 2026-10-13. That table is loaded, unchanged, from -``country_level_data_0.csv`` in the ``worldbank`` blob container, which is the -country file of the World Bank's What a Waste 2.0 dataset (2018). Its income -groups are the World Bank classification of that time, not the current one: -Argentina is high income here, and Bulgaria and Costa Rica are not. - -Keep this in step with that table rather than with the latest World Bank list. -Climate TRACE's pipeline reads the table for every site, and the point of -:mod:`SWEET_python.site_type_defaults` is that every tool reaches the same -answer for the same country. - -Countries the table does not list (overseas departments, uninhabited -territories, North Korea, the Holy See) are absent here too, so they have no -income group. -""" - -from typing import Dict - -#: ``HIC`` high, ``UMC`` upper-middle, ``LMC`` lower-middle, ``LIC`` low. -INCOME_ID_BY_ISO3: Dict[str, str] = { - "ABW": "HIC", # Aruba - "AFG": "LIC", # Afghanistan - "AGO": "LMC", # Angola - "ALB": "UMC", # Albania - "AND": "HIC", # Andorra - "ARE": "HIC", # United Arab Emirates - "ARG": "HIC", # Argentina - "ARM": "UMC", # Armenia - "ASM": "UMC", # American Samoa - "ATG": "HIC", # Antigua and Barbuda - "AUS": "HIC", # Australia - "AUT": "HIC", # Austria - "AZE": "UMC", # Azerbaijan - "BDI": "LIC", # Burundi - "BEL": "HIC", # Belgium - "BEN": "LIC", # Benin - "BFA": "LIC", # Burkina Faso - "BGD": "LMC", # Bangladesh - "BGR": "UMC", # Bulgaria - "BHR": "HIC", # Bahrain - "BHS": "HIC", # Bahamas, The - "BIH": "UMC", # Bosnia and Herzegovina - "BLR": "UMC", # Belarus - "BLZ": "UMC", # Belize - "BMU": "HIC", # Bermuda - "BOL": "LMC", # Bolivia - "BRA": "UMC", # Brazil - "BRB": "HIC", # Barbados - "BRN": "HIC", # Brunei Darussalam - "BTN": "LMC", # Bhutan - "BWA": "UMC", # Botswana - "CAF": "LIC", # Central African Republic - "CAN": "HIC", # Canada - "CHE": "HIC", # Switzerland - "CHI": "HIC", # Channel Islands - "CHL": "HIC", # Chile - "CHN": "UMC", # China - "CIV": "LMC", # Côte d’Ivoire - "CMR": "LMC", # Cameroon - "COD": "LIC", # Congo, Dem. Rep. - "COG": "LMC", # Congo, Rep. - "COL": "UMC", # Colombia - "COM": "LIC", # Comoros - "CPV": "LMC", # Cabo Verde - "CRI": "UMC", # Costa Rica - "CUB": "UMC", # Cuba - "CUW": "HIC", # Curacao - "CYM": "HIC", # Cayman Islands - "CYP": "HIC", # Cyprus - "CZE": "HIC", # Czech Republic - "DEU": "HIC", # Germany - "DJI": "LMC", # Djibouti - "DMA": "UMC", # Dominica - "DNK": "HIC", # Denmark - "DOM": "UMC", # Dominican Republic - "DZA": "UMC", # Algeria - "ECU": "UMC", # Ecuador - "EGY": "LMC", # Egypt, Arab Rep. - "ERI": "LIC", # Eritrea - "ESP": "HIC", # Spain - "EST": "HIC", # Estonia - "ETH": "LIC", # Ethiopia - "FIN": "HIC", # Finland - "FJI": "UMC", # Fiji - "FRA": "HIC", # France - "FRO": "HIC", # Faeroe Islands - "FSM": "LMC", # Micronesia, Fed. Sts. - "GAB": "UMC", # Gabon - "GBR": "HIC", # United Kingdom - "GEO": "LMC", # Georgia - "GHA": "LMC", # Ghana - "GIB": "HIC", # Gibraltar - "GIN": "LIC", # Guinea - "GMB": "LIC", # Gambia, The - "GNB": "LIC", # Guinea-Bissau - "GNQ": "UMC", # Equatorial Guinea - "GRC": "HIC", # Greece - "GRD": "UMC", # Grenada - "GRL": "HIC", # Greenland - "GTM": "UMC", # Guatemala - "GUM": "HIC", # Guam - "GUY": "UMC", # Guyana - "HKG": "HIC", # Hong Kong SAR, China - "HND": "LMC", # Honduras - "HRV": "HIC", # Croatia - "HTI": "LIC", # Haiti - "HUN": "HIC", # Hungary - "IDN": "LMC", # Indonesia - "IMN": "HIC", # Isle of Man - "IND": "LMC", # India - "IRL": "HIC", # Ireland - "IRN": "UMC", # Iran, Islamic Rep. - "IRQ": "UMC", # Iraq - "ISL": "HIC", # Iceland - "ISR": "HIC", # Israel - "ITA": "HIC", # Italy - "JAM": "UMC", # Jamaica - "JOR": "UMC", # Jordan - "JPN": "HIC", # Japan - "KAZ": "UMC", # Kazakhstan - "KEN": "LMC", # Kenya - "KGZ": "LMC", # Kyrgyz Republic - "KHM": "LMC", # Cambodia - "KIR": "LMC", # Kiribati - "KNA": "HIC", # St. Kitts and Nevis - "KOR": "HIC", # Korea, Rep. - "KWT": "HIC", # Kuwait - "LAO": "LMC", # Lao PDR - "LBN": "UMC", # Lebanon - "LBR": "LIC", # Liberia - "LBY": "UMC", # Libya - "LCA": "UMC", # St. Lucia - "LIE": "HIC", # Liechtenstein - "LKA": "LMC", # Sri Lanka - "LSO": "LMC", # Lesotho - "LTU": "HIC", # Lithuania - "LUX": "HIC", # Luxembourg - "LVA": "HIC", # Latvia - "MAC": "HIC", # Macao SAR, China - "MAF": "HIC", # St. Martin (French part) - "MAR": "LMC", # Morocco - "MCO": "HIC", # Monaco - "MDA": "LMC", # Moldova - "MDG": "LIC", # Madagascar - "MDV": "UMC", # Maldives - "MEX": "UMC", # Mexico - "MHL": "UMC", # Marshall Islands - "MKD": "UMC", # Macedonia, FYR - "MLI": "LIC", # Mali - "MLT": "HIC", # Malta - "MMR": "LMC", # Myanmar - "MNE": "UMC", # Montenegro - "MNG": "LMC", # Mongolia - "MNP": "HIC", # Northern Mariana Islands - "MOZ": "LIC", # Mozambique - "MRT": "LMC", # Mauritania - "MUS": "UMC", # Mauritius - "MWI": "LIC", # Malawi - "MYS": "UMC", # Malaysia - "NAM": "UMC", # Namibia - "NCL": "HIC", # New Caledonia - "NER": "LIC", # Niger - "NGA": "LMC", # Nigeria - "NIC": "LMC", # Nicaragua - "NLD": "HIC", # Netherlands - "NOR": "HIC", # Norway - "NPL": "LIC", # Nepal - "NRU": "UMC", # Nauru - "NZL": "HIC", # New Zealand - "OMN": "HIC", # Oman - "PAK": "LMC", # Pakistan - "PAN": "HIC", # Panama - "PER": "UMC", # Peru - "PHL": "LMC", # Philippines - "PLW": "HIC", # Palau - "PNG": "LMC", # Papua New Guinea - "POL": "HIC", # Poland - "PRI": "HIC", # Puerto Rico - "PRT": "HIC", # Portugal - "PRY": "UMC", # Paraguay - "PSE": "LMC", # West Bank and Gaza - "PYF": "HIC", # French Polynesia - "QAT": "HIC", # Qatar - "ROU": "UMC", # Romania - "RUS": "UMC", # Russian Federation - "RWA": "LIC", # Rwanda - "SAU": "HIC", # Saudi Arabia - "SDN": "LMC", # Sudan - "SEN": "LIC", # Senegal - "SGP": "HIC", # Singapore - "SLB": "LMC", # Solomon Islands - "SLE": "LIC", # Sierra Leone - "SLV": "LMC", # El Salvador - "SMR": "HIC", # San Marino - "SOM": "LIC", # Somalia - "SRB": "UMC", # Serbia - "SSD": "LIC", # South Sudan - "STP": "LMC", # São Tomé and Príncipe - "SUR": "UMC", # Suriname - "SVK": "HIC", # Slovak Republic - "SVN": "HIC", # Slovenia - "SWE": "HIC", # Sweden - "SWZ": "LMC", # Eswatini - "SXM": "HIC", # Sint Maarten (Dutch part) - "SYC": "HIC", # Seychelles - "SYR": "LIC", # Syrian Arab Republic - "TCA": "HIC", # Turks and Caicos Islands - "TCD": "LIC", # Chad - "TGO": "LIC", # Togo - "THA": "UMC", # Thailand - "TJK": "LIC", # Tajikistan - "TKM": "UMC", # Turkmenistan - "TLS": "LMC", # Timor-Leste - "TON": "UMC", # Tonga - "TTO": "HIC", # Trinidad and Tobago - "TUN": "LMC", # Tunisia - "TUR": "UMC", # Turkey - "TUV": "UMC", # Tuvalu - "TWN": "HIC", # NA - "TZA": "LIC", # Tanzania - "UGA": "LIC", # Uganda - "UKR": "LMC", # Ukraine - "URY": "HIC", # Uruguay - "USA": "HIC", # United States - "UZB": "LMC", # Uzbekistan - "VCT": "UMC", # St. Vincent and the Grenadines - "VEN": "UMC", # Venezuela, RB - "VGB": "HIC", # British Virgin Islands - "VIR": "HIC", # Virgin Islands (U.S.) - "VNM": "LMC", # Vietnam - "VUT": "LMC", # Vanuatu - "WSM": "UMC", # Samoa - "XKX": "LMC", # Kosovo - "YEM": "LIC", # Yemen, Rep. - "ZAF": "UMC", # South Africa - "ZMB": "LMC", # Zambia - "ZWE": "LIC", # Zimbabwe -} diff --git a/SWEET_python/defaults_2019.py b/SWEET_python/defaults_2019.py index 2f2143f..e66b547 100644 --- a/SWEET_python/defaults_2019.py +++ b/SWEET_python/defaults_2019.py @@ -1326,7 +1326,7 @@ def compost_emission_factor(food, green): } -def disposal_split_for(iso3, region): +def disposal_split_for(iso3, region, income_id=None): """Default disposal split for a city with no landfill data of its own. Returns the shares of disposed waste sent to ``landfill_w_capture``, @@ -1335,7 +1335,9 @@ def disposal_split_for(iso3, region): with neither is all landfill where a site of unknown type is assumed a sanitary landfill (``site_type_defaults.landfill_is_default``, Climate TRACE's rule: high income, or a region or country it names), and all dumpsite - elsewhere. + elsewhere. ``income_id`` is the country's World Bank income group, from the + caller; SWEET holds no income table, so without one only region and named + countries decide. Southern Asia, South-Eastern Asia and Southern Africa have no regional row (the other regional tables carry them as ``0.0 # np.nan``), so their @@ -1363,7 +1365,7 @@ def disposal_split_for(iso3, region): # Imported here: site_type_defaults imports this module. from SWEET_python.site_type_defaults import landfill_is_default - landfills = landfill_is_default(iso3) + landfills = landfill_is_default(iso3, income_id) return { "landfill_w_capture": 0.0, "landfill_wo_capture": 1.0 if landfills else 0.0, diff --git a/SWEET_python/site_type_defaults.py b/SWEET_python/site_type_defaults.py index ce52feb..6443e57 100644 --- a/SWEET_python/site_type_defaults.py +++ b/SWEET_python/site_type_defaults.py @@ -14,8 +14,12 @@ in Denmark or the United States. A country counts as engineered if any of these holds: -- **It is high income** in the World Bank grouping Climate TRACE reads - (:mod:`SWEET_python.country_income`). +- **It is high income** in the World Bank's grouping. SWEET holds no copy of + it: the caller passes the country's group. Climate TRACE passes the one on its + site row and WasteMAP the one its build reads; both come from the World Bank + list the ingestion pipeline loads (``supplementary.supplemented_facility``, + ``transformed.transformed_world_bank_income_classification``). A caller with + none gets the region and named-country rules alone. - **Its SWEET region is in** :data:`LANDFILL_DEFAULT_REGIONS`. That is ``defaults_2019.LANDFILL_DEFAULT_REGIONS`` plus Western Asia and Central Asia. Central Asia joins on the argument that admits Russia: the post-Soviet @@ -44,7 +48,6 @@ import pandas as pd from SWEET_python import defaults_2019 -from SWEET_python.country_income import INCOME_ID_BY_ISO3 SANITARY_LANDFILL = "Sanitary Landfill" DUMPSITE = "Dumpsite" @@ -60,22 +63,15 @@ def _blank(value) -> bool: return value is None or (not isinstance(value, str) and pd.isna(value)) or value == "" -def income_for(iso3: Optional[str]) -> Optional[str]: - """The World Bank income group SWEET holds for a country, or None.""" - if _blank(iso3): - return None - return INCOME_ID_BY_ISO3.get(str(iso3).upper()) - - # decision: site-type-fill def landfill_is_default(iso3: Optional[str], income_id: Optional[str] = None) -> bool: """True where a site of unknown type should be assumed a sanitary landfill. Args: iso3: The country's ISO 3166 alpha-3 code. - income_id: The caller's own income group for the country, if it has - one. A missing value (None, NaN or empty) falls back to - :func:`income_for`, which reads the same World Bank table. + income_id: The country's World Bank income group ('HIC', 'UMC', 'LMC', + 'LIC'), from the caller. A missing value (None, NaN or empty) is not + high income, so only the region and named-country rules apply. Returns: False for an unknown or unplaceable country. @@ -85,10 +81,8 @@ def landfill_is_default(iso3: Optional[str], income_id: Optional[str] = None) -> iso3 = str(iso3).upper() if iso3 in DUMPSITE_COUNTRIES: return False - if _blank(income_id): - income_id = income_for(iso3) return ( - income_id == "HIC" + (not _blank(income_id) and income_id == "HIC") or defaults_2019.REGION_LOOKUP_ISO3.get(iso3) in LANDFILL_DEFAULT_REGIONS or iso3 in LANDFILL_DEFAULT_COUNTRIES ) diff --git a/changelog/2026-10.md b/changelog/2026-10.md index dce0df1..046a764 100644 --- a/changelog/2026-10.md +++ b/changelog/2026-10.md @@ -118,6 +118,24 @@ heavily: Yokohama's city DST methane falls 74% without it (model-output change). ## Changed +- **SWEET holds no income table; the caller passes the country's income group** + (model-output change where a caller passes none). `SWEET_python/country_income.py`, + a copy of What a Waste 2.0's 2018 World Bank groups, is deleted, and with it + `site_type_defaults.income_for`. The high-income branch of the site-type rule now + uses only the income the caller passes, so every tool reads income from one place: + the World Bank list the ingestion pipeline loads. + - `landfill_is_default` and `default_site_type`: a missing income is not high + income. Climate TRACE already passes its site row's income. + - New arguments for WasteMAP to pass its build's income: `sdst_prepopulate(..., + income_by_iso3=)` (looked up once a point's country is known), + `load_andre_params(..., income_id=)` and `dst_baseline_blank(..., income_id=)`, + which hands it to `defaults_2019.disposal_split_for`. + - A caller that passes nothing loses the high-income branch: Australia, Chile and + the high-income Caribbean islands default to open dump, and Brunei's and + Singapore's cities with no split of their own to all dumpsite. + - `site_only_estimate` passes no income; nothing calls it. + - Tests: `tests/test_site_type_defaults.py`, `tests/test_custom_location_disposal_split.py`. + - **A site several cities share is split by each city's own waste, not its population** (model-output change for Climate TRACE's Brazil cities). In `City.citysite_estimate_trace`, a city's weight is now its waste in tonnes diff --git a/tests/test_custom_location_disposal_split.py b/tests/test_custom_location_disposal_split.py index 80c151b..adfa6c8 100644 --- a/tests/test_custom_location_disposal_split.py +++ b/tests/test_custom_location_disposal_split.py @@ -46,9 +46,9 @@ ZEROED = sorted([iso3 for codes in NO_ROW.values() for iso3 in codes] + ZERO_COUNTRY_ROW) -def _custom_location(iso3): +def _custom_location(iso3, income_id=None): city = City("Custom Location") - city.dst_baseline_blank(iso3, POPULATION, PRECIPITATION, TEMPERATURE) + city.dst_baseline_blank(iso3, POPULATION, PRECIPITATION, TEMPERATURE, income_id=income_id) return city.baseline_parameters @@ -145,8 +145,9 @@ def test_a_country_in_a_region_with_no_row_dumps_its_waste(iso3): def test_a_high_income_country_in_a_region_with_no_row_landfills_its_waste(): - # Brunei, by Climate TRACE's site-type rule, as its cities-table rows do. - split = _custom_location("BRN").split_fractions + # Brunei, by Climate TRACE's site-type rule, as its cities-table rows do. The + # caller passes the income group; SWEET holds none. + split = _custom_location("BRN", income_id="HIC").split_fractions assert (split.landfill_w_capture, split.landfill_wo_capture, split.dumpsite) == ( 0.0, @@ -155,6 +156,12 @@ def test_a_high_income_country_in_a_region_with_no_row_landfills_its_waste(): ) +def test_without_an_income_brunei_falls_to_its_regions_rule(): + split = _custom_location("BRN").split_fractions + + assert split.dumpsite == 1.0 + + @pytest.mark.parametrize("iso3", ZERO_COUNTRY_ROW) def test_a_country_whose_row_is_all_zeros_landfills_its_waste(iso3): split = _custom_location(iso3).split_fractions diff --git a/tests/test_site_type_defaults.py b/tests/test_site_type_defaults.py index 6ee1810..ecde3ca 100644 --- a/tests/test_site_type_defaults.py +++ b/tests/test_site_type_defaults.py @@ -15,7 +15,7 @@ import SWEET_python.city_params as city_params import SWEET_python.site_type_defaults as std -from SWEET_python.country_income import INCOME_ID_BY_ISO3 +from SWEET_python import defaults_2019 # --------------------------------------------------------------------------- # @@ -25,9 +25,6 @@ @pytest.mark.parametrize( "iso3, why", [ - ("AUS", "high income; its region is not in the list"), - ("ABW", "high income in the Caribbean"), - ("CHL", "high income in South America"), ("POL", "Eastern Europe"), ("KAZ", "Central Asia"), ("TUR", "Western Asia"), @@ -43,6 +40,18 @@ def test_engineered_disposal_systems_default_to_landfill(iso3, why): assert std.default_site_type(iso3) == "Sanitary Landfill", why +@pytest.mark.parametrize( + "iso3, why", + [ + ("AUS", "its region is not in the list"), + ("ABW", "the Caribbean"), + ("CHL", "South America"), + ], +) +def test_a_high_income_country_defaults_to_landfill_when_the_caller_says_so(iso3, why): + assert std.default_site_type(iso3, income_id="HIC") == "Sanitary Landfill", why + + @pytest.mark.parametrize("iso3", ["NGA", "IND", "IDN", "PAK", "EGY", "PER", "ZAF"]) def test_other_countries_default_to_an_open_dump(iso3): assert std.default_site_type(iso3) == "Dumpsite" @@ -61,7 +70,7 @@ def test_an_unknown_country_is_an_open_dump(iso3): def test_a_lower_case_code_is_read_the_same(): - assert std.default_site_type("aus") == "Sanitary Landfill" + assert std.default_site_type("pol") == "Sanitary Landfill" assert std.default_site_type("mng") == "Dumpsite" @@ -72,39 +81,32 @@ def test_a_callers_own_income_is_used_when_it_has_one(): @pytest.mark.parametrize("missing", [None, np.nan, pd.NA, ""]) -def test_a_missing_income_falls_back_to_sweets_table(missing): - assert std.default_site_type("AUS", income_id=missing) == "Sanitary Landfill" +def test_without_an_income_only_region_and_named_countries_decide(missing): + """SWEET keeps no income table: a caller that passes none gets no high-income rule.""" + assert std.default_site_type("AUS", income_id=missing) == "Dumpsite" + assert std.default_site_type("POL", income_id=missing) == "Sanitary Landfill" assert std.default_site_type("NGA", income_id=missing) == "Dumpsite" -def test_landfill_is_default_agrees_with_default_site_type(): - for iso3 in INCOME_ID_BY_ISO3: - landfill = std.landfill_is_default(iso3) - assert std.default_site_type(iso3) == ("Sanitary Landfill" if landfill else "Dumpsite") - +def test_sweet_holds_no_income_table(): + from pathlib import Path -# --------------------------------------------------------------------------- # -# The income table -# --------------------------------------------------------------------------- # + assert not Path(std.__file__).with_name("country_income.py").exists() + assert not hasattr(std, "income_for") -def test_the_income_table_is_the_one_climate_trace_reads(): - """217 countries, the What a Waste 2.0 classification (2018). - - Pinned on three countries the World Bank has reclassified since, so a swap to - the current list fails here instead of quietly disagreeing with Climate TRACE. - """ - assert len(INCOME_ID_BY_ISO3) == 217 - assert set(INCOME_ID_BY_ISO3.values()) == {"HIC", "UMC", "LMC", "LIC"} - assert INCOME_ID_BY_ISO3["ARG"] == "HIC" - assert INCOME_ID_BY_ISO3["BGR"] == "UMC" - assert INCOME_ID_BY_ISO3["CRI"] == "UMC" +def test_landfill_is_default_agrees_with_default_site_type(): + for iso3 in defaults_2019.region_lookup_iso3: + for income in (None, "HIC", "LIC"): + landfill = std.landfill_is_default(iso3, income) + assert std.default_site_type(iso3, income) == ("Sanitary Landfill" if landfill else "Dumpsite") -def test_every_listed_country_has_a_sweet_region_except_the_channel_islands(): - from SWEET_python import defaults_2019 - unplaced = set(INCOME_ID_BY_ISO3) - set(defaults_2019.region_lookup_iso3) - assert unplaced == {"CHI"} +def test_the_city_split_and_the_point_prefill_take_income_from_the_caller(): + andre = inspect.signature(city_params.City.load_andre_params).parameters + prefill = inspect.signature(city_params.City.sdst_prepopulate).parameters + assert "income_id" in andre and "income_by_iso3" in prefill + assert inspect.getsource(city_params.City.load_andre_params).count("landfill_is_default(iso3, income_id)") == 3 # --------------------------------------------------------------------------- # From c82984e8da7e281b75a3d29831fd206471920e00 Mon Sep 17 00:00:00 2001 From: Hugh Runyan Date: Thu, 29 Oct 2026 16:06:53 -0700 Subject: [PATCH 2/2] site_only_estimate takes the caller's income too The last path that guesses a site type with no income argument. Nothing calls it, but a caller would have lost the high-income rule silently. Co-Authored-By: Claude Opus 5.5 --- SWEET_python/city_params.py | 7 +++++-- changelog/2026-10.md | 2 +- tests/test_site_type_defaults.py | 6 +++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/SWEET_python/city_params.py b/SWEET_python/city_params.py index 8d09dd1..0177842 100644 --- a/SWEET_python/city_params.py +++ b/SWEET_python/city_params.py @@ -2775,13 +2775,16 @@ def check_for_landfill_gaps( self.estimate_diversion_emissions(scenario=0) self.sum_landfill_emissions(scenario=0) - def site_only_estimate(self, row=None, pop_data=None): + def site_only_estimate(self, row=None, pop_data=None, income_id: Optional[str] = None): """ For generating estimates for sites only Args: id (int): The ID of the site. df (DataFrame): The DataFrame containing site data. + income_id: The country's World Bank income group, for a site with no + recorded type (the shared site-type rule). SWEET holds no income + table; without one, only region and named countries decide. Returns: None @@ -2900,7 +2903,7 @@ def site_only_estimate(self, row=None, pop_data=None): depth = None site_type = row["Site Type"].values[0] if site_type not in get_site_type_idx.keys(): - site_type = site_type_defaults.default_site_type(self.iso3) + site_type = site_type_defaults.default_site_type(self.iso3, income_id) site_type_idx = get_site_type_idx[site_type] city_params_dict = baseline.update_cityparams_dict() baseline.city_params_dict = city_params_dict diff --git a/changelog/2026-10.md b/changelog/2026-10.md index 9b76f10..9c43a97 100644 --- a/changelog/2026-10.md +++ b/changelog/2026-10.md @@ -133,7 +133,7 @@ heavily: Yokohama's city DST methane falls 74% without it (model-output change). - A caller that passes nothing loses the high-income branch: Australia, Chile and the high-income Caribbean islands default to open dump, and Brunei's and Singapore's cities with no split of their own to all dumpsite. - - `site_only_estimate` passes no income; nothing calls it. + - `site_only_estimate(..., income_id=)` takes one too, though nothing calls it. - Tests: `tests/test_site_type_defaults.py`, `tests/test_custom_location_disposal_split.py`. - **A site several cities share is split by each city's own waste, not its diff --git a/tests/test_site_type_defaults.py b/tests/test_site_type_defaults.py index ecde3ca..efa39c0 100644 --- a/tests/test_site_type_defaults.py +++ b/tests/test_site_type_defaults.py @@ -102,10 +102,14 @@ def test_landfill_is_default_agrees_with_default_site_type(): assert std.default_site_type(iso3, income) == ("Sanitary Landfill" if landfill else "Dumpsite") -def test_the_city_split_and_the_point_prefill_take_income_from_the_caller(): +def test_every_path_that_guesses_a_type_takes_income_from_the_caller(): andre = inspect.signature(city_params.City.load_andre_params).parameters prefill = inspect.signature(city_params.City.sdst_prepopulate).parameters + site = inspect.signature(city_params.City.site_only_estimate).parameters + blank = inspect.signature(city_params.City.dst_baseline_blank).parameters assert "income_id" in andre and "income_by_iso3" in prefill + assert "income_id" in site and "income_id" in blank + assert "default_site_type(self.iso3, income_id)" in inspect.getsource(city_params.City.site_only_estimate) assert inspect.getsource(city_params.City.load_andre_params).count("landfill_is_default(iso3, income_id)") == 3