Skip to main content

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

Project description

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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyoptexplain-0.1.0.tar.gz (165.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyoptexplain-0.1.0-py3-none-any.whl (197.8 kB view details)

Uploaded Python 3

File details

Details for the file pyoptexplain-0.1.0.tar.gz.

File metadata

  • Download URL: pyoptexplain-0.1.0.tar.gz
  • Upload date:
  • Size: 165.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.13

File hashes

Hashes for pyoptexplain-0.1.0.tar.gz
Algorithm Hash digest
SHA256 161f3807434be796ae55d141c2c0ebfb35c57093354ebd52986735cbad51e20c
MD5 15e9e6f2478d32e53a40884244098ea4
BLAKE2b-256 98f3e73a08e13ec406dc7ce65c2fe9363c0d799194fc01615d6963d8681cb7e5

See more details on using hashes here.

File details

Details for the file pyoptexplain-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: pyoptexplain-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 197.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.13

File hashes

Hashes for pyoptexplain-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3742c2541c4cb115f3484eb5aa339a9c078a1e524fe7f18f9ec39c9900819e49
MD5 5de224ae2d69835554471d4c7fa29a12
BLAKE2b-256 8050d5b8bed44b60222d609034a6b4944998e63d3e2edce36f5667f400f1a765

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page