Uncertainty#
PULPO optimizes one impact under the uncertainty its data declare. The method needs no sampling to solve: the impact’s mean and variance follow in closed form, the chance constraints become a second-order cone, and the cone is solved in reduced space, over the choice alternatives only. Sampling enters afterwards, to validate a solution out of sample. The uncertainty notebook runs every step on the bundled demo database with open-source solvers.
Note
As of now, uncertainty in the LCA data is considered only in the biosphere (intervention) flows and the characterization factors. Technosphere exchanges are deterministic, even where the database declares a distribution for them (assumption A4 below). Uncertain capacities or availabilities enter as uncertain process bounds.
from pulpo.utils import uncertainty as unc
data = unc.import_declared(worker) # parameters of the impact
unc.override(data, 'If', 'my_foreground_db', expert_specs) # elicited distributions
mom = unc.compute_moments(data, worker) # closed-form mean and variance
ccp = unc.ChanceConstrained(worker, mom, upper_bounds={activity: capacity_spec})
front = ccp.solve([0.5, 0.9, 0.99]) # reliability sweep
dec = unc.decompose(front[0.9], mom) # exact Sobol' indices
scr = unc.screen_undeclared(front[0.9], data, worker, exact_cfs=unc.co2_flows(worker))
val = unc.validate(front, ccp, data, n=200_000, seed=1)
pulpo.pulpo_unc.PulpoOptimizerUnc keeps data on the worker and offers the import, the override,
the moments, the chance-constrained problem, the screening and the validation as methods
(import_uncertainty_data, apply_expert_knowledge, moments, chance_constrained,
screen_undeclared, validate). decompose and the other analyses are called as above, with
worker.uncertainty_data as the data.
Data and assumptions#
\(A\) is the technosphere matrix (square, invertible, deterministic), \(B\) the biosphere matrix with entries \(b_{ej}\), \(q\) the characterization factors (CFs) of one LCIA method, \(s\) the scaling vector. The impact is \(X(s) = \sum_e q_e \sum_j b_{ej} s_j\).
The uncertain inputs are the entries of \(B\) and the CFs that declare a distribution (in the database, in the method, or by expert override), and the bounds on processes that the problem declares uncertain (e.g. an availability or a capacity).
Supported families: lognormal (also for negative amounts), normal, uniform, triangular, and exact values (
uncertainty_type1). Any other family raises.
The method rests on five assumptions, each stated where it is used:
Assumption |
Where |
|
|---|---|---|
A1 |
all uncertain inputs are mutually independent |
moments, decomposition |
A2 |
\(X(s)\) is approximately normal (the only normality assumption) |
the impact’s chance constraint; tested by the validation |
A3 |
undeclared parameters are deterministic |
everywhere; tested by the screening |
A4 |
\(A\) is deterministic |
makes the reduced space exact |
A5 |
the failure budget is split by Boole’s inequality |
joint chance constraints; conservative under any dependence |
import_declared takes every nonzero entry of \(B\) on a characterized flow and
every CF on a flow that occurs in \(B\). Parameters without a distribution are kept
as undeclared: their mean is their amount and their variance zero. No
contribution filter is applied and nothing is gap-filled.
Moments#
with \(y_e = \sum_j \mathrm{E}[b_{ej}] s_j\) the mean flow \(e\). The second sum is the
covariance that every process emitting flow \(e\) shares through its one factor
\(q_e\); Moments.std_independent drops it for comparison.
Chance constraints#
At a joint reliability level \(\lambda\) over \(K\) events (the impact row and one per uncertain bound), each event may fail with probability \(\varepsilon_k = w_k(1-\lambda)\), equal weights by default. Boole’s inequality then bounds the probability that any event fails by \(1-\lambda\):
and minimizing \(z\) gives
The bounds enter at the exact quantile of their declared family; only the
impact row uses the normal approximation (A2). \(\kappa \ge 0\) (\(\lambda_z \ge 1/2\))
keeps the problem convex; lower levels are refused. allocation='individual'
imposes every event at \(\lambda\) on its own, for comparison: it controls no joint
probability.
Reduced space#
With \(A\) square and invertible, every scaling vector that meets the balances is
with one free variable \(v_k\) per alternative (the net output on its product row)
and \(\tilde f\) the demand on all other rows. Every static PULPO constraint is
linear in \(s\), so it becomes one row in \(v\), and the problem over \(v\) is the
problem over \(s\): the reduction is exact. \(S\) is never formed; each row
\(m^\top S\) costs one adjoint solve with one factorization of \(A\) (PARDISO,
UMFPACK or SciPy’s SuperLU). Every finite process bound is a row \(S[j, :]\), so the problem
stays small as long as only the processes with a real limit are bounded: finite
default_limits on every process would put the whole of \(S\) into it.
The deterministic LP is solved this way by solve(formulation='reduced'). For the
chance-constrained problem \(\sigma(s) = \lVert R\,[1; v]\rVert\) with
\(R^\top R = G^\top G\), \(G = Q^{1/2}[s_0, S]\) and
\(Q = \mathrm{diag}(d) + B_u^\top \mathrm{diag}(w) B_u\): a second-order cone with one
column per alternative, solved by Clarabel or Gurobi.
Exact variance decomposition#
\(X\) is a sum of products of independent inputs, so its Sobol’ decomposition stops at second order. At a fixed \(s\), with \(V = \mathrm{Var}\,X\):
and \(\sum S_1 + \sum S_2 = 1\) exactly. decompose reports them per parameter and
per family (e.g. per database with families_by_database, and the CFs). B entries
never interact with each other, nor CFs with each other, so a family’s indices
are the sums over its members. The uncertain bounds are not inputs of \(X\) at a
fixed \(s\); they shape \(s\).
Undeclared parameters#
screen_undeclared tests assumption A3. Every undeclared parameter gets the same
coefficient of variation \(r\) with its mean unchanged (\(\mathrm{Var}\,b = r^2 b_0^2\),
\(\mathrm{Var}\,q = r^2\,\mathrm{E}[q]^2\)) and the indices above are evaluated at a fixed
decision. The CFs passed as exact_cfs never receive a width: for a global
warming potential these are the CO₂ flows, whose factor is exact by definition
because CO₂ is the reference gas (co2_flows finds them by name in an
ecoinvent biosphere). Every undeclared B entry is widened: none is exact by
definition, holding one exact could only lower the figures, and only the few at
the top of the ranking need an expert’s judgement. The parameters are ranked by their
deterministic contribution \(\mathrm{E}[q_e]\,b_{0,ej}\,s_j\), which fixes the order
of their total-order indices up to the CF factor \(1 + w_e/\mathrm{E}[q_e]^2\) and does
not depend on \(r\); the indices at each \(r\) are reported beside it.
width_sensitivity evaluates \(\sigma(s^*)\) with separate widths per database and
for the CFs. The mean does not change, so \(\kappa\,\Delta\sigma\) is an upper bound on
the rise of the optimal adjusted impact (re-optimizing can only do better).
widen(data, r, exact_cfs=co2_flows(worker)) returns the widened data, with a
lognormal of the same mean and coefficient of variation (mirrored for negative
amounts), so the same data give a re-solved front and validation draws. Widths
are given per database name and 'Cf'; a name that is not a subgroup of the data
raises.
Out-of-sample validation#
validate holds each solution \(s^*\) and its target \(z^*\) fixed and draws every
declared input from its own family, together with the uncertain bounds, from one
recorded seed. Every decision is priced on the same draws, so comparisons between
decisions are paired. Per point it reports:
the coverage of the impact row against \(\lambda_z\), of each bound against \(1-\varepsilon_k\), and of all events together against \(\lambda\), with Wilson intervals;
the normal-approximation error (the empirical \(\lambda_z\) quantile minus \(z^*\)), the skewness and excess kurtosis of \(X\);
the empirical CVaR at \(\lambda_z\) against the Gaussian \(\mu + \varphi(\Phi^{-1}(\lambda_z))/(1-\lambda_z)\,\sigma\);
the regret against the lowest level of the front and against the best front point in each draw (mean and 95th percentile).
Further decisions, such as the deterministic optimum, are priced against the same
targets with designs=. Each parameter is drawn from its own stream, keyed by the
seed and its position, so its draws depend on the seed alone; decisions validated
in separate calls with the same seed are paired as well. Only B entries that carry
variance at the decisions are drawn; the smallest ones, below tol of each
decision’s variance together, are held at their means, and the share drawn is
reported.
Diagnostics#
diagnostics(front, moments) reports at every point: \(\sigma\) without the shared-CF
covariance and the ratio \(\sigma/\sigma_{\text{indep}}\); the CFs’ share of the
variance; the bound imposed on each uncertain process; the size of the reduced
problem and the solve times.
Building blocks#
The reduced system and the sampler are public, so a study can formulate its own problem over \(v\), such as a scenario-based CVaR front:
reduced.build(worker)returns theReducedModelof an instance: itsReducedSystemmaps \(v\) to \(s\) (base,recover) and projects any linear functional of \(s\) (project,rows,project_vectors);linear_program()assembles the constraint rows in \(v\).ChanceConstrained.projections()returns \(s_0\), \(\mathrm{E}[X] = m_0 + m^\top v\), the rows \(S_J\) of \(S\) on the processes that carry variance and \(\mathrm{E}[B_u]S\).draw_parametersreturns draws of the declared parameters as arrays, andsample_specsdraws any list of specs.
Solvers and licences#
Problem |
Solvers |
Licence |
|---|---|---|
deterministic LP, |
HiGHS (default), Gurobi, GAMS/NEOS |
HiGHS: none |
deterministic LP, |
HiGHS (default), Gurobi |
HiGHS: none |
chance-constrained cone |
Clarabel (default), Gurobi |
Clarabel: none |
HiGHS and Clarabel are installed with PULPO and need no licence. Gurobi is used
when gurobipy is installed; the size-limited licence that comes with
pip install gurobipy is for non-production use and covers reduced problems of up
to 2,000 variables and 2,000 linear constraints, or 200 variables once quadratic
terms (the cone) are present (see Gurobi’s licence terms). A reduced problem has
one column per alternative, so most case studies fit.
The defaults stay formulation='full' and instantiate(scale=False). Current
development considers formulation='reduced' and scale=True superior, and a future
release may switch the defaults.