From 23f9177f9ac29683ae37b1a6bbf343acb2676898 Mon Sep 17 00:00:00 2001 From: Hugh Runyan Date: Thu, 9 Jul 2026 17:45:05 -0700 Subject: [PATCH 1/2] Modeled landfills use measured site oxidation when available Previously every modeled landfill got a type/gas-capture default oxidation (sanitary 0.10 no-capture / 0.22 with capture; controlled dump 0.05/0.10; dumpsite 0). The pipeline input query carries a per-site `oxidation` column (populated for ~19% of rows from the standardized source table, NULL for the GPW/OSM area-matched branch) that was never read. site_only_estimate_trace and citysite_estimate_trace now prefer that input value over the default via a new _build_oxidation_series helper: - site-wide mean of non-null input values as the full-series baseline, then a per-year overwrite (mean of conflicting same-year rows) keyed by reported_emissions_year where present -- mirrors the existing gas_collection_efficiency handling; - no input -> the prior type/gas-capture default, broadcast across all years; - values used as-is (no clamping); the dead 0.25 biocover clamp does not apply. model-output-change: emissions shift for the ~19% of sites with a measured oxidation. Not a breaking change (schemas/contracts unchanged). Paired with RMI_Climate_TRACE_Waste_Methane ers-update. Co-Authored-By: Claude Opus 4.8 --- SWEET_python/city_params.py | 70 +++++++++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/SWEET_python/city_params.py b/SWEET_python/city_params.py index d4e2ddf..c1a0f8a 100644 --- a/SWEET_python/city_params.py +++ b/SWEET_python/city_params.py @@ -30,6 +30,64 @@ import time +def _build_oxidation_series(default_value, canonical_row, time_series_rows, years_range): + """Per-year oxidation factor for one modeled landfill, preferring per-site input + oxidation over the type/gas-capture default. + + A site can have several input rows from independent sources. Oxidation carries no + year of its own in the source data -- the value that varies row-to-row is the + *emissions* year (``reported_emissions_year``), not an oxidation year -- so we use + the site-wide mean of the available input oxidation values as a constant baseline + across all model years, then overwrite individual years with that year's value + (mean of any conflicting same-year rows) where an emissions year is present. + + When the site has no usable input oxidation, fall back to ``default_value`` (the + type/gas-capture default) broadcast across all years -- i.e. the prior behaviour. + Input values are used as-is (no clamping); e.g. a measured 0.35 passes through. + + ``time_series_rows`` is a DataFrame for multi-row sites and a Series for single-row + sites; ``canonical_row`` is the single deduped row. Either may carry ``oxidation``. + """ + years_index = pd.Index(years_range) + series = pd.Series(float(default_value), index=years_index) + + # Assemble the site's input rows as a frame so single-row (Series) and multi-row + # (DataFrame) sites are handled uniformly. Prefer the multi-row frame -- it holds + # every source record -- and fall back to the single canonical row. + if isinstance(time_series_rows, pd.DataFrame): + frame = time_series_rows + elif isinstance(canonical_row, pd.DataFrame): + frame = canonical_row + elif isinstance(canonical_row, pd.Series): + frame = canonical_row.to_frame().T + else: + frame = None + + if frame is None or 'oxidation' not in frame.columns: + return series + + ox = pd.to_numeric(frame['oxidation'], errors='coerce') + if not ox.notna().any(): + return series # no input oxidation -> keep the type/gas-capture default + + # 1) Site-wide mean as the full-series baseline. + series[:] = float(ox[ox.notna()].mean()) + + # 2) Overwrite individual years where an emissions year ties a value to a year. + if 'reported_emissions_year' in frame.columns: + yr = pd.to_numeric(frame['reported_emissions_year'], errors='coerce') + mask = ox.notna() & yr.notna() + if mask.any(): + per_year = pd.Series(ox[mask].to_numpy(), index=yr[mask].astype(int).to_numpy()) + per_year = per_year.groupby(level=0).mean() + lo, hi = int(years_index.min()), int(years_index.max()) + per_year = per_year[(per_year.index >= lo) & (per_year.index <= hi)] + if not per_year.empty: + series.loc[per_year.index] = per_year.to_numpy() + + return series + + # The way this model is set up is based on the unit of a City, corresponding to the City class. # 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. @@ -2486,6 +2544,9 @@ def site_only_estimate_trace(self, canonical_row=None, time_series_rows=None, po ) fraction_of_waste_vector.loc[open_date:close_date-1] = 1.0 id = int(canonical_row['asset_identifier']) + oxidation_series = _build_oxidation_series( + oxidation_value, canonical_row, time_series_rows, self.years_range + ) new_landfill = Landfill( open_date=open_date, close_date=close_date, @@ -2506,7 +2567,7 @@ def site_only_estimate_trace(self, canonical_row=None, time_series_rows=None, po advanced=True, latlon=(self.latitude, self.longitude), ks=baseline.ks, - oxidation_factor=pd.Series(oxidation_value, index=self.years_range), + oxidation_factor=oxidation_series, rmi_id=id, ) baseline.landfills.append(new_landfill) @@ -2755,6 +2816,9 @@ def citysite_estimate_trace(self, canonical_row=None, time_series_rows=None, cit close_date = int(close_date) id = int(canonical_row['asset_identifier']) + oxidation_series = _build_oxidation_series( + oxidation_value, canonical_row, time_series_rows, self.years_range + ) baseline._singapore_k(advanced_baseline=True) if (citysite_rows is None) or (isinstance(citysite_rows, pd.Series)): fraction_of_waste_vector = pd.Series( @@ -2781,7 +2845,7 @@ def citysite_estimate_trace(self, canonical_row=None, time_series_rows=None, cit advanced=True, latlon=(self.latitude, self.longitude), ks=baseline.ks, - oxidation_factor=pd.Series(oxidation_value, index=self.years_range), + oxidation_factor=oxidation_series, rmi_id=id, city_id=citysite_rows['city_id'] ) @@ -2876,7 +2940,7 @@ def citysite_estimate_trace(self, canonical_row=None, time_series_rows=None, cit advanced=True, latlon=(self.latitude, self.longitude), ks=baseline.ks, - oxidation_factor=pd.Series(oxidation_value, index=self.years_range), + oxidation_factor=oxidation_series, rmi_id=id, city_id=city_id, ) From e88a3d9914e8d3aa93dcad7da945fc34746e6f52 Mon Sep 17 00:00:00 2001 From: Hugh Runyan Date: Thu, 9 Jul 2026 18:30:00 -0700 Subject: [PATCH 2/2] Set baseline flaring efficiency explicitly to DEFAULT_FLARE_EFFICIENCY The modeled baseline previously left each landfill's `flaring` unset, so it fell to model_v2's internal default (0.98 in the monthly path used by TRACE). There is no per-site flaring-efficiency source column (the source has only flared volumes and a has_flaring boolean), so no per-site value can be read. site_only_estimate_trace and citysite_estimate_trace now set flaring explicitly to dst_common.DEFAULT_FLARE_EFFICIENCY (0.98) on every TRACE landfill, so the value no longer depends on model internals. A function-local import is used because dst_common imports city_params (a top-level import would be circular). No TRACE output change: baseline flaring was already effectively 0.98 via the monthly default, so this is numerically identical. Mitigation still raises it to 0.98/0.99 for the few improve-GCCS strategies via _gccs_flaring's floor (max/clip). Deliberately NOT touched: model_v2's default handling. The annual estimate_emissions2 defaults unset flaring to 1.0 vs the monthly estimate_emissions_monthly's 0.98 -- a real inconsistency, but TRACE/mitigation only use the monthly path, and changing the annual default would shift WasteMAP/DST output. Left as a separate, optional cleanup. Paired with RMI_Climate_TRACE_Waste_Methane ers-update. Co-Authored-By: Claude Opus 4.8 --- SWEET_python/city_params.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/SWEET_python/city_params.py b/SWEET_python/city_params.py index c1a0f8a..6834164 100644 --- a/SWEET_python/city_params.py +++ b/SWEET_python/city_params.py @@ -2547,6 +2547,12 @@ def site_only_estimate_trace(self, canonical_row=None, time_series_rows=None, po oxidation_series = _build_oxidation_series( oxidation_value, canonical_row, time_series_rows, self.years_range ) + # Baseline flaring destruction efficiency, set explicitly to the canonical default + # (was unset -> fell to model_v2's internal default). No per-site source exists; + # mitigation raises it to 0.98/0.99 via _gccs_flaring (max/clip). Local import + # avoids the dst_common <-> city_params import cycle. + from SWEET_python.dst_common import DEFAULT_FLARE_EFFICIENCY + flaring_series = pd.Series(DEFAULT_FLARE_EFFICIENCY, index=self.years_range) new_landfill = Landfill( open_date=open_date, close_date=close_date, @@ -2561,7 +2567,7 @@ def site_only_estimate_trace(self, canonical_row=None, time_series_rows=None, po scenario=0, new_baseline=True, gas_capture_efficiency=gas_capture_efficiency, - # flaring=pd.Series(flaring, index=year_range), + flaring=flaring_series, # leachate_circulate=leachate_circulate[i], fraction_of_waste_vector=fraction_of_waste_vector, advanced=True, @@ -2819,6 +2825,11 @@ def citysite_estimate_trace(self, canonical_row=None, time_series_rows=None, cit oxidation_series = _build_oxidation_series( oxidation_value, canonical_row, time_series_rows, self.years_range ) + # Baseline flaring destruction efficiency, set explicitly to the canonical default + # (see site_only_estimate_trace). Applies to both the single- and multi-city + # landfill constructors below. Local import avoids the dst_common import cycle. + from SWEET_python.dst_common import DEFAULT_FLARE_EFFICIENCY + flaring_series = pd.Series(DEFAULT_FLARE_EFFICIENCY, index=self.years_range) baseline._singapore_k(advanced_baseline=True) if (citysite_rows is None) or (isinstance(citysite_rows, pd.Series)): fraction_of_waste_vector = pd.Series( @@ -2839,7 +2850,7 @@ def citysite_estimate_trace(self, canonical_row=None, time_series_rows=None, cit scenario=0, new_baseline=True, gas_capture_efficiency=gas_capture_efficiency, - # flaring=pd.Series(flaring, index=year_range), + flaring=flaring_series, # leachate_circulate=leachate_circulate[i], fraction_of_waste_vector=fraction_of_waste_vector, advanced=True, @@ -2934,7 +2945,7 @@ def citysite_estimate_trace(self, canonical_row=None, time_series_rows=None, cit scenario=0, new_baseline=True, gas_capture_efficiency=gas_capture_efficiency, - # flaring=pd.Series(flaring, index=year_range), + flaring=flaring_series, # leachate_circulate=leachate_circulate[i], fraction_of_waste_vector=fraction_of_waste_df[city_id], advanced=True,