Results are in findings.md, how to run the benchmarks in running.md.
- bw2calc: one score; a second functional unit, with UMFPACK's refinement on and off; many
functional units against three impact categories; Monte Carlo draws;
import bw2calc. Each runs with the direct solvers (PyPardiso, UMFPACK, SuperLU) and the iterative ones. - bw2data: writing a database, up to ecoinvent's size;
import bw2data. - bw_processing and matrix_utils: building datapackages and matrices from many resource groups.
- bw_graph_tools: supply-chain graph traversal.
- bw_timex: the time-explicit LCI as the timeline grows.
Each score is checked against a reference that uses no Brightway code, so a fast but wrong release fails. Monte Carlo draws are checked only for finite values, and bw_timex against bw2calc's score.
A database is generated from a seed, so the same settings give the same matrices everywhere. Its shape comes from a profile:
ecoinvent: 26,533 products, like ecoinvent 3.12 cut-off, with its measured inputs per process and consumers per product, one loop holding about two thirds of the products (64% in ecoinvent), negative waste inputs, its shares of inputs from the consumer's own location and sector, and a spectral radius of 0.78.tests/test_measure.pychecks these on seeds 0 to 3, each within three standard deviations of its spread over seeds.smoke: the same structure at 200 products.mc100kandregional: 100,000 and 250,000 products.exiobaseandstress-random: a random structure, a worst case for the solvers.
Only aggregate statistics of the ecoinvent database (ecoinvent Association) went into the
ecoinvent profile, no dataset or figure of one. They are in
src/bwbench/profiles/.
The profile is harder on a direct solver than real ecoinvent. It has ecoinvent's degrees but
wires them at random, while in ecoinvent the regional variants of one activity share their
inputs and each ISIC division buys from its own set of suppliers. So its factors fill more:
SuperLU with COLAMD on Aᵀ, the factorization bw2calc 3.1 runs, holds 85 times as many entries
as the matrix against 26 for ecoinvent (323 against 175 on A), and METIS on A + Aᵀ 110 against
31. Direct-solver times overstate real ecoinvent by 6 to 11 times on Aᵀ
(#54).
test_ecoinvent_profile_fills_like_its_target holds the fill target as an expected failure
until a profile meets it. With a
licence of your own, scripts/solver_factorize.py times each
direct solver on the real matrix, and with --cudss NVIDIA's cuDSS on a GPU. That option
refactorizes unchanged values. --cudss-updates instead applies three deterministic row-scaled
value updates at fixed sparsity, checking every solution against SuperLU and its backward
error. It separates construction/initial transfer, analysis, update/transfer, factorization and
solve/return transfer. Value generation and CPU checks are outside those phase times; neither
option measures a complete Monte Carlo draw.
just bw2io-import synthetic smoke,ecoinvent times bw2io's ecoSpold2 import without a licence:
it writes a profile's system as ecoSpold2 files with bw2io's own biosphere3 flows, so every
exchange links, and prints each step's time and peak memory. The files are as heavy as
ecoinvent 3.12's (137 KB a dataset) without changing the system's matrices: elementary exchanges,
properties, uncertainties and parameters come at the release's means per dataset, and comments at
its count and length per kind, since bw2io keeps their text in memory. ecoSpold2 allows one comment
per exchange, so datasets with few intermediate exchanges get fewer (7.2 a dataset against 12.8).
Review text, which bw2io skips, makes up the rest. At ecoinvent size the import peaks at 3.97 GB,
against 4.5 GB for the real release. just bw2io-import RELEASE_FOLDER all does the same on a
licensed release, read-only.
Copying input lists to regional variants, drawing long lists from market pools, and hidden supplier communities were tried as generator controls and removed: none brings the top separator of A + Aᵀ below 0.25, against ecoinvent's 0.11 (#54).
BWBENCH_SIZES=useeio runs the cases on USEEIO 1.1, the US EPA's input-output model: 388
commodities, 1,871 flows, and three of its impact categories (GCC, ACID, ETOX). It is
public domain, a US government work. It is the project archive that
bw2io.install_project("USEEIO-1.1") fetches from
files.brightway.dev, downloaded once into the
cache and checked against a pinned SHA-256 (src/bwbench/useeio.py).
The harness reads the matrices from the archive's processed datapackages and only renumbers their
ids, so the values are the model's own. Its reference scores come from a solve with no Brightway
code, like the synthetic ones. It is small, so it times a real structure with negative inputs and
duplicate exchanges, not the cost of a large factorization. Only timings are published, never scores.
It has no Monte Carlo case.
BWBENCH_SIZES=bafu runs them on BAFU 2025, cited as "Life Cycle Inventory database of the Swiss
Federal Administration, BAFU:2025": 11,747 processes, 2,678 flows, and three EN 15804+A2
categories (climate change, acidification, freshwater ecotoxicity). It is the archive that
bw2io.install_project("BAFU-2025-v2") fetches, read the same way
(src/bwbench/bafu.py). At under half of ecoinvent's size, it times a
real LCI database's factorization. Its licence allows use in LCA studies but not distribution, so
the harness downloads it at run time and the repository, the results and the Zenodo dataset hold
only timings and aggregate statistics. Its matrix is badly conditioned (a condition number near
1e22), and its reference scores agree with a refined direct solve to about 2e-15. It has no Monte
Carlo case, although the archive carries uncertainty.
- asv runs every release of a package on one fixed set of
dependencies, from before 2026-10-20, in
asv/<package>.stack.txt. So a step in a graph is that package's own change. bw2calc 3.0.0 and later also get threadpoolctl. - Each history starts at the oldest release that runs on that set: bw2data at 4.6 (peewee 4), bw_timex at 1.1.0. Every release of the other packages runs.
- Each run records what was installed (
pylock.toml) and an RO-Crate naming the code and settings. - A change to the synthetic generator bumps
GENERATOR_VERSION, which changes every benchmark's asv version, so each history starts again at the next run. The older times stay in the results branch's git history. - CodSpeed turns address randomization (ASLR) off, which needs
docker/seccomp-noaslr.jsonin the box.
A step is a real change only if it is larger than the noise between runs. The spread of a case
is the interquartile range of its samples relative to the median. just noise measures it
(running.md).
On a pinned benchmark machine, on 8 efficiency cores:
- asv, bw2calc's first score at the ecoinvent size under PyPardiso, six runs: 0.0% with ASLR on, 0.1% off.
- CodSpeed, the traversal cases, three runs: 0.035% and 0.014% with ASLR off, 0.7 to 1.1% on.
- asv, smoke size, 10 runs: up to 4%, median 0.9%.
So a step of 5% or more at the ecoinvent size is real. ASLR stays on for asv, where it changes
nothing measurable. Cases of a few milliseconds vary more, and up to five-fold unless the CPU
runs its performance power profile (powerprofilesctl set performance on Linux).
bw2data's write stalls for 16 to 17 s in 1 or 2 samples in 12, while the host flushes its disk cache. asv's median doesn't move.
A GitHub-hosted runner hides changes under about 15%. On ubuntu-latest (the
noise-hosted workflow), the same smoke cases spread 1 to 14%, median 4.0%. It would catch a
several-fold step such as bw2calc 3.0.0's, but not a small one.