Skip to content

feat: FAIR-based dynamic LCIA (radiative forcing & temperature) - #24

Draft
TimoDiepers wants to merge 20 commits into
mainfrom
feat/fair-dynamic-lcia
Draft

TimoDiepers wants to merge 20 commits into
mainfrom
feat/fair-dynamic-lcia

Conversation

@TimoDiepers

@TimoDiepers TimoDiepers commented Aug 3, 2026

Copy link
Copy Markdown
Member

Summary

Adds an optional FAIR-based dynamic LCIA pathway: emission profiles from a dynamic inventory are fed into the FaIR simple climate model, and per-flow radiative forcing / temperature responses are returned as characterized results.

New metrics dispatched through the existing characterize() API:

  • fair_radiative_forcing
  • fair_temperature

What's included

  • dynamic_characterization/fair/ — species mapping (flow → FAIR species, YAML-backed), signed per-kg marginal allocation, FAIR runner with calibration discovery, and the core orchestration into a per-flow quantile DataFrame.
  • Scenario handling: set_fair_scenario(ssp, rcp) selects FAIR-native scenarios; the scenario registry is tagged with per-metric availability and FAIR markers.
  • fair is an optional extra (fair>=2.2, plus pooch for calibration download). Import is lazy and guarded — nothing changes for users who don't install it. Calibration data is not bundled and is fetched on first use.
  • Tests for species map, allocation, runner, core, dispatch, optional-import behavior, and the scenario registry.
  • Demo notebook: notebooks/fair_dynamic_lcia_demo.ipynb.
  • README + CHANGES entries.

Notes / open points

  • Draft: feedback wanted on the public API shape (set_fair_scenario, metric names) before polishing.
  • Branch is based on an older main; can rebase on request.

- Rename cached loader to _load_species_map_cached() and have public
  load_species_map() return a deep copy to prevent cache corruption
- Update test_methane_resolves to capture and assert sign == 1
- Update test_unmappable_flow_returns_none to assert sign == 1
- Fix docstring to correctly state unmappable flows return (None, sign)
Changes:
1. Refactor find_calibration_files() to call require_fair() only once
   - _find_in_fair() now accepts the fair module as argument
   - find_calibration_files() calls require_fair() once, then passes module to both _find_in_fair() calls
2. Remove misleading '# pragma: no cover' from except ImportError in require_fair()
   - This branch IS executed when fair is absent and should be covered
3. Add output validation in run_fair() before require_fair()
   - Raises ValueError if output not in ('radiative_forcing', 'temperature')
   - Validation happens before fair requirement, works without fair installed
4. Fix vacuous assertion in test_find_calibration_files_returns_tuple()
   - Changed 'assert (params is None) == (props is None) or True' to proper assertion
5. Add test_run_fair_rejects_bad_output() for output validation
   - Tests without requiring fair to be installed
Implements characterize_with_fair() that wires runner + allocation +
species_map + scenario config into a long DataFrame with columns
[date, amount, flow, activity, quantile]. Also exposes the pure helper
_inventory_emissions_by_species() for unit testing without fair installed.
- Add early guard in characterize_with_fair to return empty DataFrame
  before calling require_fair() when inventory is empty
- Reorder logic to compute by_species and check for unmapped flows
  before require_fair(), allowing unmapped flows to return early
  without requiring fair installation
- Add logger.warning when no flows map to FAIR species for visibility
- Add two new tests: test_zero_row_inventory_returns_empty and
  test_unmapped_flows_warn_and_return_empty; both pass without fair
- Add fair_radiative_forcing and fair_temperature to valid_metrics
- Add FAIR dispatch branch in characterize(): validates scenario support,
  enriches inventory with flow names via _add_fair_flow_names(), then calls
  characterize_with_fair() from fair.core
- Add _add_fair_flow_names() helper: best-effort bw2data lookup wrapped in
  try/except so FAIR path degrades gracefully without a configured project
- Import prospective config at module level (not lazily) to avoid cross-test
  sys.modules pollution from test_prospective.py
- Tests in tests/test_characterize_fair_dispatch.py (4 tests)
Other test modules install dynamic_characterization sub-package stubs at
collection time which could shadow the real package; purge them before
importing so this test validates the genuine module under any test order.
- Wrap scenario_supports("fair") check to catch RuntimeError (no scenario
  set) and raise a clear ValueError with guidance to call set_scenario()
- Remove dead monkeypatch of _characterize_with_fair in dispatch test
- Add test_fair_metric_without_any_scenario_raises() test case
- Strengthen test_add_fair_flow_names_is_best_effort() with len check
- Update _add_fair_flow_names docstring: logs at debug instead of warns
- Add logger.debug() call on exception path in _add_fair_flow_names()
Add README section "FAIR climate model (optional)" covering install
extra, fair_radiative_forcing / fair_temperature metrics, output shape
with quantile column, scenario selection, and the 12 FAIR-capable
scenarios (8 FAIR-native + 4 dual). Add available_scenarios() usage
examples. Add CHANGES.md entry for v1.5.0. Add test guard
(tests/test_docs_scenarios.py) that README contains the key strings.
…elog

- Replace internal characterize_with_fair() in README with public API via characterize(..., metric="fair_radiative_forcing"/"fair_temperature")
- Update CHANGES.md [1.5.0] heading to [Unreleased] for consistency with version state
- Add prose note about time_horizon behavior for FAIR metrics (analysis end = last emission year + time_horizon, defaults to 2100)
…core test

FIX 1: Clarify that the precursor table in species_map.yaml is informational
metadata, not used for routing. FAIR applies precursor responses natively when
the precursor emission species (NOx, VOC, Sulfur, etc.) is perturbed, so this
package does not route them separately.

FIX 2: Add autouse fixture _reset_scenario() to test_fair_core.py to reset
the prospective config scenario state before and after each test, preventing
global state leakage (mirrors pattern in test_prospective.py).
…ation)

Verified against fair 2.2.4. The stock fair package ships only the
species-properties defaults, not the calibrated 841-member AR6 ensemble, so
run_fair now:
- downloads + caches the calibration 1.4.1 params/species CSVs via pooch when
  they are not vendored in the install (content-hash pinned), else uses
  vendored files;
- simulates from 1750 so the climate state spins up, slicing reporting years
  from the timebounds axis;
- applies inventory perturbations on the timepoints axis with kg->FAIR unit
  conversion (CO2 in GtCO2, others in Mt);
- initialises forcing/temperature/cumulative/airborne emissions before run.

Probe-verified signs/magnitudes: +100 GtCO2 (2030) -> +0.035 K, +0.064 W/m2 by
2100 on ssp245; symmetric for negative. Full suite 113 passed / 1 skipped with
fair installed.
Add optional FAIR climate-model dynamic LCIA path: fair_radiative_forcing /
fair_temperature metrics, superset scenario registry with per-metric
availability, trails-style ensemble + per-flow attribution. Verified against
fair 2.2.4.
Standalone (no-Brightway) showcase of the fair_radiative_forcing and
fair_temperature metrics with ensemble quantile bands and per-activity
attribution, plus an SSP-background comparison and a Brightway + bw_temporalis
integration template. Runnable section executed with embedded plots.
FAIR is a climate emulator, not an IAM, so overloading the iam slot with an
iam='FAIR' sentinel was misleading. FAIR-native markers now live in a separate
FAIR_NATIVE_SCENARIOS map selected with set_fair_scenario(ssp, rcp) and listed
by available_fair_scenarios(); get_scenario()['iam'] is None for them. The four
IAM scenarios that match a FAIR marker still enable FAIR via set_scenario.
Updates dispatch error messages, tests, README/CHANGES/spec, and the demo
notebook (re-executed).
@TimoDiepers
TimoDiepers force-pushed the feat/fair-dynamic-lcia branch from d0da988 to 16bc3d4 Compare August 3, 2026 10:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant