Skip to main content

pyoptexplain

Practitioner-first post-optimality analysis and explanation for linear, quadratic, and mixed-integer optimization.

pyoptexplain sits above the modeling languages used to build a model and the solvers used to solve it. It is not a modeling language and not a solver. A model authored in any supported front end is adapted into one normalized internal representation, solved through a choice of backends, and interrogated through a single uniform interface: which constraints bind, what the solution is worth at the margin, how it moves under perturbation, and how it changes under what-if scenarios.

Its guiding principle is honesty. A post-optimality quantity is reported only when both the representation and the chosen backend can justify it; unavailable information is reported as unavailable rather than approximated.

Install

pip install pyoptexplain

The base install ships NumPy, SciPy, pandas, xarray, and the HiGHS and OSQP solvers, so direct-matrix LP, MILP, and QP workflows run immediately. Front ends and extra backends are optional extras:

Extra Pulls in Use for
pyoptexplain[cvxpy] cvxpy cvxpy model inspection
pyoptexplain[pyomo] Pyomo Pyomo model inspection
pyoptexplain[gurobi] gurobipy Gurobi front end + backend
pyoptexplain[cplex] cplex, docplex CPLEX front end + backend
pyoptexplain[ortools] ortools OR-Tools front end
pyoptexplain[scip] pyscipopt SCIP backend (default MIQP)
pyoptexplain[plot] matplotlib grid plotting helpers

Optional commercial solvers (Gurobi, CPLEX) still require their own licenses. All optional imports are lazy, so the base install stays light.

Quickstart

A two-product factory that maximizes profit subject to two resource capacities:

from pyoptexplain import Analyzer, LinearMatrixProblemHandle

handle = LinearMatrixProblemHandle(
    sense="max",
    c=[40.0, 30.0],
    A_ub=[[2.0, 1.0], [1.0, 2.0]],
    b_ub=[100.0, 80.0],
    bounds=[(0.0, None), (0.0, None)],
    variable_names=["chairs", "tables"],
    inequality_names=["labor", "material"],
)

analyzer = Analyzer(handle.linear_representation())
analyzer.summary()        # status, objective, primal values
analyzer.constraints()    # binding, slack, and shadow price per named block
analyzer.duals()          # block-level duals
analyzer.rhs_ranges()     # classical sensitivity (HiGHS / Gurobi / CPLEX)

The optimum makes 40 chairs and 20 tables for a profit of 2200, with both capacities binding and priced at their shadow values (labor 16.67, material 6.67).

The pipeline

pyoptexplain is a single, explicit pipeline. Each stage is chosen by hand and never collapsed into the next:

user-authored model  ->  ProblemHandle  ->  AnalysisRepresentation  ->  Analyzer
                                                  |
                                         RepresentationCertificate
  • ProblemHandle wraps an already-built model, preserves its native identity, and exposes the representations it can honestly support (basic_representation(), linear_representation(), quadratic_representation(), scenario_representation()).
  • AnalysisRepresentation is the normalized, provenance-agnostic surface the analyses run on. The same LP is analyzed identically whether it came from a raw matrix, a cvxpy inspection, or a Pyomo model.
  • RepresentationCertificate is the trust-and-provenance record: a status (verified / asserted / unknown), the capabilities the representation supports, and the mappings from each user-level block to the canonical rows it lowered into.
  • Analyzer is the single user-facing gateway. It is a factory: Analyzer(representation) returns a representation-specific subclass exposing only the methods that representation can actually run.

The user's unit of meaning is the block. A ConstraintBlock is one constraint as written, a VariableBlock its variable-side peer. A block may lower into several canonical rows (abs(x) <= b becomes two rows), but every report and experiment targets the named block, not the rows.

Certificate-gated honesty

Analysis is gated at three levels, so the library never guesses:

  1. Method presence — an unsupported operation is simply absent on the returned analyzer. reduced_costs() exists only on a linear analyzer, run_scenarios() only on a scenario analyzer.
  2. Certificate — structural analyses (LP/QP duals, classical sensitivity, integrality relaxation, ...) require a verified or asserted status.
  3. Backend — the configured backend must actually implement the diagnostic.

Analyzer.capabilities() reports the effective set as the intersection of the certificate and the backend. Unavailable information stays explicitly unavailable, reported as None, NaN, or an UnsupportedFeature.

A progression of questions

The analyses form a progression, ordered by how far they depart from the problem as originally stated.

Solve report — the static view of the current optimum:

analyzer.summary()
analyzer.constraints()
analyzer.duals()
analyzer.dual_details()            # per-row component duals for multi-row blocks
analyzer.representation_mappings() # how each block lowered into canonical rows

For linear representations on a basis-exposing backend (HiGHS, Gurobi, CPLEX), classical sensitivity is available: reduced_costs(), basis_status(), rhs_ranges(), objective_ranges(). A mixed-integer optimum carries no duals, so valuations are read on an explicitly derived relaxation via lp_relaxation() or qp_relaxation().

Perturbation analysis — does the solution stay stable under small data changes? Complete parameter families (objective, RHS, LHS, and for QPs the quadratic objective) are perturbed one at a time and re-solved:

robustness = analyzer.perturbation_robustness()   # +-1%, +-2%, +-5%, +-10% by default
robustness.summary()

Scenarios and grids — change the model itself and compare against the baseline. A scenario is a typed, ordered set of changes; a grid is the Cartesian product of scenario axes:

from pyoptexplain import (
    Analyzer, ChangeRHS, GridAxis,
    LinearMatrixScenarioRepresentation, ScenarioCase, SetObjectiveCoefficient,
)

scenario_analyzer = Analyzer(
    LinearMatrixScenarioRepresentation.from_matrix(handle.linear_representation())
)
scenario_analyzer.run_scenarios({
    "base": ScenarioCase(),
    "pricier_chairs": ScenarioCase((SetObjectiveCoefficient("chairs", 55.0),)),
    "less_material": ScenarioCase((ChangeRHS("material", delta=-20.0),)),
})
scenario_analyzer.explore_grid([
    GridAxis.rhs_change("material", [10.0, 0.0, -10.0]),
])

The typed changes include RemoveConstraint, RemoveConstraintGroup, RelaxConstraint, ChangeRHS, RemoveVariable, SetVariableBounds, ChangeVariableBounds, RelaxIntegrality, SetObjectiveCoefficient, ChangeObjectiveCoefficient, ScaleQuadraticObjective, ScaleQuadraticDiagonal, and SetParameter.

One model, many front ends

The same analysis surface is recovered no matter how the model was written. Wrap a native model in the matching handle and call linear_representation() or quadratic_representation(); everything after the handle is identical.

import cvxpy as cp
from pyoptexplain import Analyzer, CvxpyProblemHandle

chairs = cp.Variable(nonneg=True, name="chairs")
tables = cp.Variable(nonneg=True, name="tables")
labor = 2 * chairs + tables <= 100
material = chairs + 2 * tables <= 80
problem = cp.Problem(cp.Maximize(40 * chairs + 30 * tables), [labor, material])

handle = CvxpyProblemHandle(
    problem,
    variables={"chairs": chairs, "tables": tables},
    constraints={"labor": labor, "material": material},
)
Analyzer(handle.linear_representation()).duals()   # same answer as the matrix path
Front end Handle Notes
Direct matrix LinearMatrixProblemHandle, QuadraticMatrixProblemHandle structure known by construction
cvxpy CvxpyProblemHandle inspects DCP canonicalization; scenarios via cp.Parameter
Pyomo PyomoProblemHandle algebraic inspection; in-place RHS/bound/ablation scenarios
Gurobi GurobiProblemHandle direct-model scenarios (obj/bound/RHS, in-place ablation)
CPLEX CplexProblemHandle direct-model scenarios
OR-Tools OrToolsProblemHandle pywraplp (LP/MILP) or MathOpt (also QP/MIQP); scenarios via the matrix path

Where a native object cannot be extracted to a matrix (nonlinear, conic), the basic and native scenario paths still apply; structural matrix analyses do not.

Solver backends

Structured representations are provenance-agnostic and accept any compatible backend, defaulting sensibly per problem class:

Problem class Default backend Also supported
LP / MILP HiGHSBackend GurobiBackend, CPLEXBackend, SCIPBackend
QP OSQPBackend GurobiBackend, CPLEXBackend, SCIPBackend
MIQP SCIPBackend GurobiBackend, CPLEXBackend
from pyoptexplain.solvers import HiGHSBackend, GurobiBackend

Analyzer(handle.linear_representation(), backend=GurobiBackend())

Switching backend is what unlocks different diagnostics: a structured representation can be sent to whichever backend exposes the information an analysis needs. Custom backends implement the exported SolverBackend contract.

Scenarios at scale

Repeated what-if studies are the case the architecture optimizes. A scenario representation amortizes the analysis surface across a batch instead of rebuilding and re-solving from scratch each time.

  • The structured (matrix) scenario representation extracts the problem into arrays once and builds its certificate once. Structure- and dimension-preserving changes (RHS shifts, inequality relaxation, inequality removal by deactivation, variable-bound changes) edit only the touched arrays on a warm solver session; structure-changing cases fall back to a fresh derivation. Available whenever the problem is extractable.
  • The native scenario representation mutates the original modeling object in place and restores it, available even for models that cannot be extracted, and able to apply changes the live object exposes directly (e.g. a Gurobi objective-coefficient edit).

Across LP and QP batches this cuts per-scenario cost several-fold, reaching 10–25× once a batch exceeds a handful of cases. Solve-dominated integer problems see no benefit, as expected. See notebooks/scenario_scaling_study.ipynb for the full study.

Notebooks

notebooks/ holds runnable, end-to-end demonstrations: one per modeling language, the matrix LP/MILP/QP walkthroughs, the perturbation and scenario workflows, and the performance and scaling studies. They are the canonical worked examples.

Status and license

First release (0.1.0). Apache-2.0 licensed. Requires Python 3.9+.

Contributions are welcome; see CONTRIBUTING.md.

Release files for pyoptexplain 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pyoptexplain 0.1.1
File Size Uploaded
pyoptexplain-0.1.1.tar.gz 166.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pyoptexplain 0.1.1
File Interpreter ABI Platform
pyoptexplain-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 365.3 kB

Release files / pyoptexplain-0.1.1.tar.gz

Download URL pyoptexplain-0.1.1.tar.gz
Size 166.6 kB
Tags Source
SHA-256 checksum
How to use checksums
019b1e7a1422a4b60eb4ee84d933313142c8b25994d6aae585cd6bedd69aee78
BLAKE2b-256 checksum
How to use checksums
f80ca6e13811dcc315797b92c593f130fddf64e2d4c21721ad919341046daa54
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.13

Release files / pyoptexplain-0.1.1-py3-none-any.whl

Download URL pyoptexplain-0.1.1-py3-none-any.whl
Size 198.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
43d1709d6972a535c5f06e8d19bc9c3cec3598634cd3151953a127bd6a1eb231
BLAKE2b-256 checksum
How to use checksums
f2d77c3637787380a74d86d9693773302be5581dd58bbc2df515334326b72c74
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.13

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page