User Guide¶
This guide covers the main workflows in TRAILS: loading data packages, running temporal LCA, and interpreting results.
Data packages¶
TRAILS expects a Frictionless data package (datapackage.json) with
scenario-indexed technosphere and biosphere matrices plus index metadata.
Packages produced by premise are supported out of the box.
Key components include:
Technosphere matrix (
A): exchanges between activities/productsBiosphere matrix (
B): biosphere flows per activityActivity indices: metadata for activities (name, location, unit)
Biosphere indices: metadata for flows (name, compartment, unit)
Temporal exchanges (optional): distribution metadata for delayed flows
Loading TRAILS¶
from datapackage import Package
from trails import Trails, get_lcia_method_names
package = Package("path/to/datapackage.json")
method = get_lcia_method_names(ei_version="3.11")[0]
# interpolate_annual=True expands scenario slices to annual resolution.
# Default annual bounds are [min_year-1, max_year+1].
trails = Trails(
package,
interpolate_annual=True,
methods=[method],
ei_version="3.11",
)
# Optional: widen interpolation bounds for endpoint duplication
# trails = Trails(
# package,
# interpolate_annual=True,
# interpolation_start_year_offset=-20,
# interpolation_end_year_offset=20,
# methods=[method],
# ei_version="3.11",
# )
After initialization, you can access scenario labels and metadata:
print(trails.scenario_labels)
activity_indices = next(iter(trails.activity_indices.values()))
print(list(activity_indices.items())[:5])
Interpolation boundary behavior:
Between provided inventory years, TRAILS uses linear interpolation.
Before the earliest year and after the latest year, TRAILS duplicates the nearest endpoint year according to
interpolation_start_year_offset/interpolation_end_year_offset.
Selecting activities¶
Activities are referenced by integer indices from the metadata. A typical workflow is to select an activity and store the index for repeated calls:
activity_indices = next(iter(trails.activity_indices.values()))
start_act_idx = next(iter(activity_indices.keys()))
Running temporal LCA¶
The primary entry point is trails.lca.lca. The usual workflow is to store
regular LCIA methods and the LCIA data version on the Trails instance at
initialization, then reuse those defaults during routing and scoring.
from trails import lca
# Run temporal routing (builds the traversal graph).
# By default this uses adaptive routing with a relative cutoff of 1e-4.
trails.temporal_routing(
start_year=2030,
start_act_idx=start_act_idx,
amount=1.0,
min_amount=1e-18,
show_progress=True,
debug=False,
attribute_to_roots=True,
)
# Run temporal LCA (stores results on the Trails instance)
lca(
trails=trails,
# defaults shown explicitly:
solver_mode="iterative",
iterative_rtol=1e-3,
)
You can still override the constructor defaults in a specific call with
lca(trails, methods=[...], ei_version="...") or
temporal_routing(..., adaptive_methods=[...]).
Temporal LCA results are stored on the Trails instance. Use trails.scores
for impact scores (when compute_score=True). If you run
lca(..., store_inventory=True), TRAILS also stores trails.inventory;
with compute_score=True and store_inventory=True, it also stores
trails.characterized_inventory.
Adaptive score-potential routing¶
For deep temporalized systems, a fixed max_depth can expand many branches
whose eventual contribution is negligible. Adaptive routing lets TRAILS use
static LCIA activity scores as a screening estimate before deciding whether a
child branch should be routed explicitly. This is the default routing mode:
when max_depth is omitted, TRAILS uses max_depth=None and
adaptive_relative_score_cutoff=1e-4.
trails.temporal_routing(
start_year=2030,
start_act_idx=start_act_idx,
amount=1.0,
max_depth=None,
min_amount=1e-18,
adaptive_relative_score_cutoff=1e-4,
adaptive_min_depth=1,
)
lca(trails=trails)
The relative cutoff is multiplied by the functional unit’s static score
potential. In the example above, branches are stopped once their estimated
static potential is at most 1e-4 of the functional unit potential.
Routing modes¶
There are four common routing configurations:
- 1. Default adaptive routing
Use this for normal regular-LCIA workflows.
temporal_routing()defaults tomax_depth=Noneandadaptive_relative_score_cutoff=1e-4.trails.temporal_routing( start_year=2030, start_act_idx=start_act_idx, )
- 2. Adaptive routing with another relative cutoff
Use a smaller value for stricter routing and a larger value for looser routing.
trails.temporal_routing( start_year=2030, start_act_idx=start_act_idx, adaptive_relative_score_cutoff=1e-5, )
- 3. Adaptive routing with a hard depth cap
Use this when branches should be pruned by score potential but never routed beyond a fixed graph depth.
trails.temporal_routing( start_year=2030, start_act_idx=start_act_idx, max_depth=5, adaptive_relative_score_cutoff=1e-4, )
- 4. Fixed-depth routing
Use this when you want the older depth-based behavior. Passing an integer
max_depthwithout an adaptive relative cutoff disables adaptive pruning.trails.temporal_routing( start_year=2030, start_act_idx=start_act_idx, max_depth=3, )
Important behavior:
Adaptive pruning only changes graph expansion. Stopped branches are stored as frontier demands and still enter the year-wise matrix solve in
lca().Passing an integer
max_depthwithout an adaptive cutoff selects fixed-depth routing. You can also combine an adaptive relative cutoff with a finitemax_depthto keep a hard cap.Adaptive routing uses
Trails(..., methods=...)unlessadaptive_methods=...is provided explicitly. EDGES methods cannot currently be used for adaptive screening.Static activity scores are cached by default. Set
adaptive_use_cache=Falsefor tests or diagnostics that should recompute the screening intensities.For multi-method screening, TRAILS uses the maximum absolute potential across the selected methods, so a branch is retained if it is relevant for any screening indicator.
Importing Excel inventories¶
You can import user-provided inventories from Excel using bw2io. When you
omit year and scenario_label, the exchanges are applied to all template
years and interpolated across annual years.
from trails import Trails
trails = Trails(package)
trails.import_excel_inventory("path/to/inventory.xlsx")
# Target a single scenario slice instead
trails.import_excel_inventory("path/to/inventory.xlsx", year=2020)
This file demonstrates the bw2io Excel format that TRAILS expects:
Activity section (top of the sheet): key/value pairs that define the activity being imported.
Exchanges section (table starting after the
Exchangesrow): each row is an exchange linked to the activity.
Activity section fields (column A = field name, column B = value):
Activity: activity name (string).reference product: reference product (string).location: location code (string).unit: activity unit (string).Additional fields (e.g.,
lifetime [km],average age [year]) are allowed and preserved as metadata.
Exchange table columns (from the header row):
name: exchange name (string).amount: base exchange amount (float). Used when no year-specific columns are provided.Year-specific columns (e.g.,
2020,2040,2060): optional numeric columns used as year-specific amounts. TRAILS writes these values into the corresponding years in the A/B matrices and then linearly interpolates between them for intermediate years. For years outside the provided range, the nearest endpoint value is used (clamped).location: exchange location (string). Required for technosphere/production linking.categories: biosphere categories (tuple orcompartment/subcompartment).unit: exchange unit (string).type: exchange type:production,technosphere, orbiosphere.reference product: required for technosphere/production exchanges.temporal_distribution: integer code for the temporal distribution.temporal_loc: location parameter (mean/median/mode depending on distribution).temporal_scale: scale parameter (e.g., stddev/sigma).temporal_min/temporal_max: integer offsets defining the support.temporal_offsets: JSON list of integer offsets (used by discrete empirical).temporal_weights: JSON list of pulse weights (used by discrete empirical).temporal_amount_source: eitherportormatrix:portapplies the ported exchange amount over time;matrixuses the matrix-stored values in other years.comment: optional notes.
The temporal columns correspond directly to TemporalExchange:
temporal_distribution: distribution code (1=discrete, 2=lognormal, 3=normal, 4=uniform, 5=triangular, 6=discrete empirical).temporal_locandtemporal_scale: distribution parameters.temporal_minandtemporal_max: inclusive integer offsets.temporal_offsetsandtemporal_weights: JSON-list pulse definitions fordistribution=6. Example:[0, 5, 12]and[0.5, 0.3, 0.2].temporal_amount_source: -port: uses the exchange amount for all years (ported value). -matrix: uses the matrix values for the selected years.
Temporal distributions¶
Temporal distributions control how exchanges are spread across impact years.
In the main temporal workflow, temporal_routing and lca use temporal
exchange distributions by default:
trails.temporal_routing(
start_year=2030,
start_act_idx=start_act_idx,
min_amount=1e-18,
)
lca(
trails=trails,
)
For a non-temporal baseline in a single year, use trails.static_lca(...).
Plotting results¶
Use plot_temporal_scores to visualize the time series and contribution by
first-level suppliers:
from trails import plot_temporal_scores
fig = plot_temporal_scores(
trails,
method_label=method,
stacked=True,
show_cumulative_axis=True,
)
fig.show()
Use plot_adaptive_sankey to inspect the explicit graph created by adaptive
temporal_routing:
from trails import plot_adaptive_sankey
fig = plot_adaptive_sankey(
trails,
method=method,
branch_visual_cutoff=0.001,
max_sankey_links=0,
output_path="adaptive_sankey.html",
)
fig.show()
The Sankey links represent routed graph edges weighted by the child node’s adaptive score potential. Nodes are placed by routing depth and year, labels are shown on hover, and optional right/bottom density panels summarize impact density over time and depth. Matrix-solved frontier demands remain part of the final LCA results, but only explicitly routed edges are displayed in the Sankey.
The same helper is available on a Trails instance:
fig = trails.plot_adaptive_sankey(method=method)
Interpreting outputs¶
Impact time series can be accessed from trails.scores (if computed). When
store_inventory=True, you can also inspect trails.inventory and (if
compute_score=True) trails.characterized_inventory.
Troubleshooting and diagnostics¶
If a requested year is not available in the data package, TRAILS will snap to the nearest available year and emit a warning.
Set
debug=Trueto enable detailed logging and retain additional diagnostics on the Trails instance.Advanced performance tuning: - For FaIR perturbation parallelism, use
run_fair_delta_rf(..., per_species_workers=<int>).For TD biosphere accumulation buffering, set
trails._bio_inventory_flush_nnzbefore callinglca. Default is2_000_000. This is an advanced/private knob.
FaIR radiative forcing¶
TRAILS integrates with the FaIR climate model to convert time-resolved inventories into radiative forcing and temperature anomalies. The workflow runs a baseline FaIR scenario from the bundled REMIND/FaIR emissions data, then performs per-species perturbation runs derived from the Trails inventory. Positive and negative emissions are treated separately to preserve long-lived CO2 tails for both uptake and release. Results are allocated to root activities using cumulative signed emissions for each (flow, root) pair, and summarized across all FaIR configurations as quantiles (2.5, 25, 50, 75, 97.5).
from trails import lca
from trails.fair_rf import run_fair_delta_rf
# Ensure an inventory with root attribution is available
lca(
trails=trails,
store_inventory=True,
)
rf = run_fair_delta_rf(
trails,
scenario="REMIND|SSP2-PkBudg650",
# defaults shown explicitly:
per_species_runs=True,
per_species_workers=None, # auto: min(4, cpu_count, n_work_items)
)
The outputs are stored on:
trails.instant_radiative_forcingwith dims(quantile, year, flow, root activity)trails.delta_temperaturewith dims(quantile, year, flow, root activity)
Notes:
run_fair_delta_rfrequirestrails.inventorywith aroot activitydimension. Runlca(..., store_inventory=True)first.scenariomust match a scenario label present in the emissions CSV used byrun_fair_delta_rf.If
config_nameandconfig_namesare omitted, TRAILS evaluates all available FaIR configurations and stores quantiles across the ensemble.
Visualization helpers default to the 50th quantile:
from trails import plot_rf, plot_temp
plot_rf(trails, year_range=(2000, 2100))
plot_temp(trails, year_range=(2000, 2100))