Changelog#
All notable changes to this project will be documented in this file.
[2.0.0] - 2026-10-06#
Added#
Reduced-space solver backend —
solve(formulation='reduced'). Every scaling vector that satisfies the balances is one fixed vector plus a combination of the alternatives’ outputs, so the LP can be solved over the alternatives instead of over every process. The reformulation is exact, supports every static constraint type and is much smaller and faster on large databases; the solution is written back onto the instance, so results are read as before. Solved with HiGHS (default) or Gurobi. The time-dependent model supportsformulation='full'only.macOS and Linux on ARM — PULPO installs there without PARDISO, which needs Intel’s MKL. The reduced solves then use UMFPACK when
scikit-umfpackis installed from conda-forge, and SciPy’s slower solver otherwise.One uncertainty method, solved in reduced space —
pulpo.utils.uncertainty. As of now, uncertainty in the LCA data is considered only in the biosphere flows and the characterization factors; technosphere exchanges are deterministic. The parameters of an impact are imported with their declared distributions (import_declared); parameters without one are deterministic and listed, not filled in. Expert knowledge replaces distributions (override). The mean and variance of the impact follow in closed form, shared characterization factors included (compute_moments).ChanceConstrainedminimizes the impact at a joint reliability level over the impact and any uncertain capacities (Boole’s inequality, exact quantiles for the capacities) and sweeps the levels; each level is a small second-order cone program over the alternatives, solved with Clarabel (open source) or Gurobi. It reproduces PULPO 1.8.0’s exact front.PulpoOptimizerUncoffers these steps as worker methods.Analyses of a solved front — exact Sobol’ indices of the impact at a decision (
decompose), a screening of the parameters that declare no uncertainty and the sensitivity of the standard deviation to their widths (screen_undeclared,width_sensitivity), diagnostics per point (diagnostics), and out-of-sample validation of every point on shared draws (validate). The reduced system, the impact’s projections onto it and a vectorized sampler are public, for formulations of one’s own. The uncertainty notebook now runs on the bundled demo database with open-source solvers only.Examples — four notebooks (showcase, rice, time-dependent, uncertainty), each in
notebooks/and in the documentation, run on bundled databases with open-source solvers. The ecoinvent showcase notebooks are removed.
Defaults#
formulation='full'andinstantiate(scale=False)remain the defaults. Current development considersformulation='reduced'andscale=Truesuperior, and a future release may switch the defaults.Methods with weight 0 now appear in the results with their calculated impact.
Finite
default_limits['lower_bound']/['upper_bound']raise aFutureWarning: a bound on every activity has no physical meaning and makesformulation='reduced'build a dense row per process. Setlower_limit/upper_limiton the processes that have a real limit.default_limitsmay be deprecated in a near-future release.Nonemeans “no limit” in choice capacities and in the limit dicts (upper_limit,lower_limitand the flow and impact limits), likefloat('inf');default_limitstakes numbers.solve(formulation='reduced')warns about huge finite stand-ins (e.g. a capacity of1e10); the examples and notebooks usefloat('inf').Loading LCI data without (complete) uncertainty data no longer warns;
uncertainty.import_declaredsays what is missing when it is needed.
Breaking changes#
The 1.x uncertainty API is replaced without a deprecation period.
PulpoOptimizerUnc:import_and_filter_uncertainty_data→import_uncertainty_data,apply_uncertainty_strategies→apply_expert_knowledge,create_SOC_formulation→moments, andcreate_CC_formulation/solve_CC_problem/solve_SOC_problem→chance_constrained(...).solve(lambdas).Removed: gap-filling strategies, the parameter filter, Monte Carlo re-optimization over the uncertainty data (
run_mc_from_uncertainty), the sampled sensitivity analysis (run_gsa), theL1formulation, the cutting planes,restore_deterministic_objective, and the modulesuncertainty.soc,uncertainty.gsa,uncertainty.monte_carloanduncertainty.plots(soc.SOCCoefficientsis nowuncertainty.Moments).PulpoOptimizer.solve_MC, which re-optimizes over Brightway’s own resampling, is kept.compute_closed_form_moments(now inuncertainty.moments, importable fromuncertainty) andcc.apply_CC_formulationtake new arguments and return new results.Unused 1.x helpers are gone: the data checks in
uncertainty.processor,saver.compare_subsequent_paretosolutionsandbw_parser.update_lci_data.A solve that does not end optimal (infeasible, unbounded, or stopped by a limit) raises
optimizer.SolveErrorand loads nothing, in both formulations and with every solver;ReducedSolveErrorandChanceConstrainedErrorare subclasses.PulpoOptimizer(and itsTime/Uncvariants) no longer takes the unuseddirectoryargument;save_resultstakes a relative or an absolute file path.Equal lower and upper limits of 0 switch a process off; they no longer define a supply of 0, whose slack made the process’s product free.
In the static model an
upper_limitoverrides a choice capacity, with a warning when it replaces a finite one; the capacity used to win silently. The time-dependent model already did this.retrieve_envflowsmatches names and categories exactly (a name used to match every flow whose name it contains) and returns an empty list, with a warning, when nothing matches; keys may be tuples.The
uncertaintyextra is empty;clarabelandstats_arraysare core dependencies.JupyterLab, IPython,
fsandopenpyxlare no longer installed with PULPO. The newnotebooksextra brings JupyterLab, matplotlib and seaborn for the example notebooks.
[1.8.0] - 2026-09-19#
Added#
Exact chance-constrained optimization via a second-order cone —
PulpoOptimizerUnc.create_SOC_formulation()/solve_SOC_problem(). The existingsolve_CC_problembounds the impact’s standard deviation by anL1sum over processes, which over-estimates it and treats the per-process contributions as independent although one characterization factor multiplies them all. The new formulation represents the standard deviation exactly, shared characterization factors included, so at the same reliability level it returns a front at or below theL1one.method='cutting_plane'(the default) solves a sequence of the ordinary LPs PULPO already builds and is what works at ecoinvent scale;method='direct'hands the cone to Gurobi in a single solve and suits small or well-conditioned systems. The cutting-plane solve reports a certified optimality gap per iteration.restore_deterministic_objective()returns the instance to the plain impact objective.Closed-form moments —
create_SOC_formulation(moments='closed_form'). Mean and variance are computed analytically per distribution family instead of by refitting every parameter to a Normal through Monte Carlo: faster, and free of sampling noise in the coefficients.Joint chance constraints —
cc.bonferroni_budget()andapply_CC_formulation(risk_budget=...). Imposing each row atlambdaindividually controls no joint probability: withKconstrained rows, the chance that at least one of them fails reachesK(1 - lambda). A risk budget divides the failure probability among the rows so they hold together atlambda. It adds no variables and no constraints — only each right-hand side moves — and it needs no correlation estimates. Opt-in; the default behaviour is unchanged.Exact quantiles for uncertain bounds —
apply_CC_formulation(bound_quantile='exact'). A chance constraint on a bare bound is exactly the declared distribution’s own quantile and needs no Gaussian approximation. This matters at high reliability, where a moment-matched normal can demand a bound below the distribution’s support — a negative capacity, in practice. Opt-in, and requires a risk budget.Optional LP equilibration —
instantiate(scale=True), off by default. An ecoinvent technosphere spans 1e-13 to 2e+11, because infrastructure processes have a functional unit of one whole facility. Solvers apply their feasibility tolerance per row relative to that row’s largest coefficient, so such a facility can be under-supplied by more than its own activity level and still count as feasible — worth roughly 1 % of the optimum on an unaggregated system, with different solvers landing on different answers. No solver option repairs it. Equilibration rescales rows and columns by powers of two, which is exact in floating point, and the solution is unscaled after the solve, soscaling_vector,impactsand everythingextract_results()returns stay in original units. It covers the uncertainty formulations too, sosolve_CC_problemandsolve_SOC_problemreturn the same fronts either way. Recommended for unaggregated ecoinvent backgrounds.import_and_filter_uncertainty_data(scaling_vector_strategy='none')— retain every declared uncertain parameter. The contribution filter is a device for large systems; on a small one it strips the uncertainty from alternatives that happen to be inactive at the vector it filters on, which biases the subsequent risk-averse choice towards exactly those alternatives.run_gsa(seed=...)— the SALib sampler’s seed is now controllable from the façade instead of fixed (still 161 by default, so previously reported indices are unchanged).pulpo/datasets/soc_demo_database.py— an open six-activity demonstration system covering every supported uncertainty family, usable without an ecoinvent licence.
Changed#
solve_gurobino longer recommendsScaleFlag=2/NumericFocus=3. On an ecoinvent-scale model those made the optimum worse rather than better, because the feasibility tolerance is still applied relative to the 1e11 facility coefficients. On a scaled model it now appliesScaleFlag=0,FeasibilityTol=OptimalityTol=1e-9andNumericFocus=1unless the caller overrides them (scaling.GUROBI_OPTIONS_SCALED); addMethod=1for bit-identical repeated solves.Under
scale=True, process bounds inherited fromdefault_limitsbecome infinite, with a warning saying how many. A finiteupper_bound=1e9never binds anyway, but on a facility column it scales to 1e20 and degrades the solve. Explicitlower_limit/upper_limitvalues and choice capacities are kept.
Fixed#
An infinite variable bound was registered as an uncertain parameter. Every choice alternative and every explicit limit was recorded without testing its value, so an alternative declared
float('inf')— the default already used to mean “unlimited” — became an uncertain bound with aninfamount, ananvariance and ananbound handed to the solver. Infinite bounds are now skipped, so an unbounded alternative no longer has to be written as a large sentinel capacity.A deterministic characterization factor turned every sampled impact into
NaNin the global sensitivity analysis. A flow whose factor declares no distribution received an all-NaNcolumn, and one such column makes every sample impactNaN. Such a factor is constant rather than missing, and is now filled with its value from the characterization matrix. Previously the only way to keep the flow was to gap-fill its factor with an invented spread.seed=did not make a Monte Carlo reproducible unless every parameter was Normal. Lognormal, triangular and uniform parameters drew from NumPy’s global stream and ignored the seed, so any run containing one was irreproducible,run_mc_from_uncertainty(seed=...)included.
Known issues#
run_mc_from_uncertaintydraws its per-draw seeds from[0, 10**6), so about 164 of 20 000 draws are duplicates and the effective sample is ~0.8 % smaller than requested. Widening the range would shift published draw sequences, so it is deferred rather than bundled into this release.
[1.7.0] - 2026-08-11#
Add a goal-programming objective (
objective='goal'): minimize the average transgression of user-defined soft impact limits (imp_goals), e.g. for Planetary-Boundary-style budgets. Unlikeupper_imp_limit, goals can be exceeded — the solver stays feasible and reports the transgression level per category instead. Available on bothPulpoOptimizerand the time-extendedPulpoOptimizerTime(goals apply to impacts aggregated across the whole time horizon), and carried through Monte Carlo re-instantiation.Report per-category goal results (impact, goal, transgression level) via
extract_results()["Transgressions"],summarize_results(), and the Excel export.Fix several
default_limits/ goal-programming interactions found while hardening the new objective:default_limitsno longer silently hard-caps categories that carry a goal (was causing infeasibility or artificially suppressed transgression, #33).default_limitsnow persists across Monte Carlo re-instantiation instead of reverting to the built-in infinite defaults on every sample.Goal limits accept numpy scalar types (e.g. from a DataFrame/array) and correctly reject
bool.extract_transgressions()’s empty-goal-case result now shares the sameMethod-indexed schema as the populated case.objective='goal'with an empty goal set now raises a clearValueErrorinstead of a bareZeroDivisionError.
Fix Monte Carlo re-instantiation to forward every
instantiate()argument instead of a hardcoded, drifting kwarg list — time-indexed workers were silently losingtime_steps/storageon every MC sample and falling back to the static formulation.Fix bw25 uncertainty: stabilize
construct_scaling_vector_from_choices()(NaN scaling vector from unmapped node ids, non-deterministic cutoff filtering from unstable dict ordering) so MC/CC/GSA results are deterministic and match the bw2 stack.Fix GSA on the bw25 stack under SALib 1.5 / numpy 2 (bump the
uncertaintyextra to SALib 1.5.1).Fix
import_datato activate itsprojectargument instead of silently using whatever bw2data project happened to be active.Dev: migrate the test suite to
pytestwith in-memory bw2data databases (faster, order-independent); seetests/README.md.
[1.6.1] - 2026-07-13#
Fix a solver hang on Windows with pyomo >= 6.6: the huge finite default limits (±1e20 / ±1e24) made HiGHS log “treated as ±Infinity” warnings during model construction, which deadlocked pyomo’s appsi output capture (solve stuck forever at zero CPU). Unspecified limits and supply-slack bounds are now truly infinite (
float('inf')), which HiGHS, Gurobi, and GAMS all handle natively — the resulting LP is unchanged. The choices documentation now recommendsfloat('inf')instead of1e20for unconstrained capacities.Modernize dependencies to enable Python 3.13: unpin numpy 2 (the
<2cap now lives only in the legacybw2extra, where bw2data 3.x needs it), relax pyomo to>=6.8.0,<7(6.7.3 still touches the removednp.float_under numpy 2), and replace the phantombw2data<=3.9.9pin withbw2data<4.0.0.Skip
pypardisoon macOS via an environment marker (no wheels there; scipy’s solver is used instead).Use a stable sort for result ordering in the saver so extracted results are deterministic across numpy versions.
Dev: configure pytest discovery and ignore project virtualenvs.
[1.6.0] - 2026-07-10#
Add a time-dependent extension as a first-class feature via the new
pulpo.pulpo_timemodule (PulpoOptimizerTime):Time-indexed LP formulation with per-timestep demands, limits, and impacts, plus aggregated impact bounds across timesteps.
Storage / carry-over between timesteps via a product-by-product
Kmatrix (storageargument), supporting the 4-activity CHARGE / HOLD / HOLD t-1 / DISCHARGE battery pattern.Time-indexed result extraction and saving; toy battery examples (daily, two-week, and hourly intra-day scenarios) in
notebooks/.
Major model-construction speedup on ecoinvent-scale databases (several times faster instantiation):
Build Pyomo models directly as
ConcreteModelinstead ofAbstractModel.create_instance().Embed the technology, intervention, and environmental cost matrices as plain float coefficients in
LinearExpressionconstraint rows instead of per-entry mutable Params (near-zero coefficients no longer reach the solver).Apply production-capacity, intervention-flow, and impact limits as variable bounds instead of explicit constraints, and create supply slack variables only for products with a specified supply.
The chance-constrained formulation updates environmental costs through the new
optimizer.update_env_cost(), which rebuilds the impact constraints in place.
Faster and more robust Brightway data import:
Build LCI matrices once per database rather than once per method, and push
retrieve_processesfiltering down to SQL.Load all databases in a single LCA so that the database order does not matter.
Decouple Monte Carlo RNG seeds per matrix in the bw2 path and skip unused uncertainty parameters during MC sampling.
Fixes and maintenance: numpy 2.0 compatibility,
highspypinned to 1.15.1,pypardisoadded as dependency,Nonedefaults instead of mutable dict arguments ininstantiate(), uncertainty characterization-factor index rename fix.
[1.5.2] - 2026-06-02#
Fix bw25 uncertainty parameter extraction: dynamically inspect datapackage resources by metadata instead of relying on hard-coded indices. This resolves failures when extracting uncertainty information from brightway databases that lack uncertainty distributions.
Add comprehensive uncertainty handling to the rice-husk example database, matching the uncertainty specification pattern used in the sample database.
Fix multi-database parameter accumulation in bw25 path: ensure both foreground and background database flows are included in the combined parameter array (previously only the last database’s bio_params were retained).
Refactor
bw_parser.pyfor improved maintainability: move the rarely-used bw25 uncertainty handling utilities (build_bw25_params(),BW25_PARAM_DTYPE,BW25_DISTRIBUTION_FIELDS) topulpo.utils.utils, keeping the primary import orchestration logic lean and focused.Add comprehensive test coverage for bw25 uncertainty extraction with and without complete uncertainty information in both sample and rice-husk databases.
[1.5.1] - 2026-04-24#
Fix chance-constrained environmental-cost mean computation by replacing fragile pandas sparse updates with direct sparse matrix updates and multiplication in
pulpo.utils.uncertainty.cc.Update and re-run the Section 10 uncertainty showcase notebook to include the deterministic reference result used by
run_gsaand validate the hotfix workflow end-to-end.Move the sample and rice-husk database helpers from
tests/into a newpulpo.datasetssubpackage so thatpulpo.install_sample_db()/pulpo.install_rice_husk_db()work from an installed wheel (previously they imported fromtests/, which is not shipped).Fix wheel packaging: exclude
tests/from the distribution and drop the redundantutils*include in[tool.setuptools.packages.find].Declare the mutual exclusivity of the
bw2andbw25extras via[tool.uv] conflictsso thatuvcan resolve the environment without picking one over the other.
[1.5.0] - 2026-04-23#
Integrate full uncertainty analysis pipeline as a first-class feature via the new
pulpo.pulpo_uncmodule (PulpoOptimizerUnc):Import and filter uncertain LCI parameters from Brightway databases (
import_and_filter_uncertainty_data())Apply uncertainty strategies to fill missing distributions (
apply_uncertainty_strategies())Monte Carlo sampling from prepared uncertainty distributions (
run_mc_from_uncertainty())Chance-Constrained optimization with Pareto front solving (
create_CC_formulation(),solve_CC_problem())Global Sensitivity Analysis via Sobol indices (
run_gsa())
Section 10 of the main showcase notebook (
notebooks/pulpo_showcase.ipynb) demonstrates the complete uncertainty workflow on the methanol system.Enhanced Pareto front visualization (base case overlay, choice highlighting, grouping by process/product/location)
L1-norm variance used in CC formulation
Minor bugfixes and code cleanup
[1.4.2] - 2025-06-06#
Enable the use of Gurobi solver
Pass email as argument to NEOS solver
Enable choices to be specified as dict of lists rather than dict of dicts
[0.1.5] - 2025-01-26#
Convenience changes to optimization problem formulation
Enable users to specify separate fore- and background databases to retrieve data from simultaneously.
Several bugfixes
Instances can now be solved more than once
Enable negative slack values
Downgrade pyomo in requirements to facilitate highspy solution
Switch from
setup.pytopyproject.tomlfor package management
[0.1.4] - 2025-01-05#
Resolve dependency issues revolving around scipy
[0.1.3] - 2024-12-01#
Start tracing changes in changelog.
Enable PULPO to use both bw2 and bw25 projects. Different install options are available via
pip install pulpo-dev[bw2]orpip install pulpo-dev[bw25].
[0.0.1] - 2023-10-10#
Initial version .