discopt#
A Mixed-Integer Nonlinear Programming (MINLP) solver built on a Rust core with Python orchestration. Solves MINLPs by spatial Branch & Bound [Belotti et al., 2013, Land and Doig, 1960] over rigorous convex relaxations, with an in-house primal/dual simplex for the per-node LPs and a Rust automatic-differentiation tape for objective/gradient/Jacobian/Hessian evaluation.
Architecture#
Model.solve() --> Python orchestrator --> Rust B&B kernel / TreeManager
| |
NLP evaluation: Node pool / branching / pruning
POUNCE AD tape In-house primal/dual simplex (node LPs)
(default, JAX-free) Zero-copy numpy arrays (PyO3)
NLP backends:
pounce (pure-Rust Ipopt port) [default]
cyipopt (Ipopt) [fallback]
Rust backend (crates/discopt-core): Expression IR, Branch & Bound tree (node pool, branching, pruning), the native spatial B&B kernel, in-house primal/dual simplex with a sparse LU basis (feral), .nl file parser, FBBT/presolve (interval arithmetic, probing, Big-M simplification).
NLP evaluation (python/discopt/_tape_nlp_evaluator.py, _nl_expr_compiler.py): objective, gradient, constraints, Jacobian, and Lagrangian Hessian (dense and sparse) from a POUNCE Rust AD tape, with no JAX on the path. Expressions with no tape opcode fall back to the JAX evaluator, and DISCOPT_NLP_EVAL=jax selects it wholesale. A tape-representable solve does not import JAX – not on the LP, QP, MIQP and simplex-MILP paths, and not on the nonlinear ones either (measured over eight nonlinear corpus instances: zero jax modules in sys.modules after each solve). Those eight are all tape-representable, so they do not license the unconditional claim: the fallback lives on the default path, and a plain Model.solve() of a dm.external nested inside another one’s argument, with no caller-side import jax, takes sys.modules from 0 to 222 jax entries. (dm.norm(X, 2) of a matrix used to be the example; since #1903 the spectral norm and dm.external are numpy-evaluated holes in the tape and import no JAX.) JAX is the optional discopt[jax] extra (#1873): without it that model refuses with an ImportError naming the construct the tape has no opcode for and the extra, and every other JAX-only feature refuses the same way.
Relaxation layer (python/discopt/_relax): DAG compiler, the uniform factorable relaxation engine, the LP-form McCormick relaxation [McCormick, 1976] with a univariate envelope table (sigmoid, softplus, tanh, …), alphaBB, and cutting planes. The default uniform factorable engine is numpy, not JAX: a nonconvex MINLP whose expressions are tape-representable does not import JAX when solved through Model.solve() (measured — "jax" in sys.modules is False after a spatial-B&B solve to certified optimality). 16 of the 75 top-level modules in this directory import JAX, 12 of them at module scope (measured after #1873 retired the McCormick-NLP route and 22 JAX modules with it; it was 41 of 85 before), and they are off the default path, with one exception that matters: _relax/nlp_evaluator is the legacy JAX evaluator the tape falls back to, so a model with no tape opcode reaches it from an ordinary Model.solve(). The directory was named _jax before the removal, so its name is not a guide to what it runs on. dm.register_function(name, lower) lets a caller declare a named univariate composite the engine envelopes as a single atom instead of relaxing its expansion term by term — the route a plugin uses to hand the relaxer structure that generic factorable reformulation would throw away.
Solver wrappers (python/discopt/solvers): POUNCE (pure-Rust Ipopt port) for LP/QP/NLP, the in-house simplex LP/MILP backends, cyipopt for Ipopt [Wächter and Biegler, 2006], AMP, the MIP-NLP decomposition family, GDPopt-LOA, the derivative-free backends, and an optional Gurobi backend. highspy is used only on the OA/GDP paths.
ML model embedding (python/discopt/ml): embeds trained ML predictors as algebraic MINLP constraints [Ceccon et al., 2022] — feedforward networks via full-space (smooth activations), ReLU big-M MILP [Anderson et al., 2020], and reduced-space strategies, and decision trees/ensembles via a per-leaf MILP encoding [Mišić, 2020]; interval arithmetic bound propagation; ONNX, scikit-learn and PyTorch model import. (Imported as discopt.nn before 0.9.0; that name still works as a deprecated alias.)
Generalized disjunctive programming (python/discopt/_relax/gdp_reformulate.py): reformulates GDP models — BooleanVar, propositional logic operators, either_or(), if_then() — into standard MINLP via big-M, multiple big-M (LP-tightened), convex hull, or Logic-based Outer Approximation.
Piecewise-linear functions (python/discopt/modeling/_piecewise.py): Model.piecewise(x, breakpoints, values) declares a tabulated univariate function and lowers it, at declaration, to an exact MILP encoding — incremental [Markowitz and Manne, 1957], logarithmic [Vielma and Nemhauser, 2011], disaggregated convex combination, or a native SOS2 set [Beale and Tomlin, 1970] — following Vielma et al. [2010]. Because the table is the function, the model solved is the model declared and the certificate carries over; an input whose domain escapes the breakpoint span is refused, not clamped. Its approximating counterpart, dm.nonlinear_to_pwl(model) (python/discopt/modeling/_pwl_transform.py), replaces univariate nonlinear terms by piecewise-linear constructs: mode="outer" builds a rigorous outer approximation (interval-arithmetic error bands with interval-AD mean-value tapers), so its bound is valid for the original model and a certificate requires a verified incumbent to meet it, with adaptive partition refinement [Misener et al., 2011, Nagarajan et al., 2019]; mode="approximate" never claims a bound. See Piecewise-Linear Functions.
Modeling surface (python/discopt/modeling): the expression DAG, plus the blocks that let one model stand inside another. dm.argmin(inner, ...) embeds an inner NLP as a block of an outer model — its forward pass is a POUNCE solve and its derivatives are sIPOPT KKT sensitivities — with dm.argmin_kkt as the lowered arm of the same block. Model.mcp(F, z, lb=..., ub=...) declares the box-bounded mixed-complementarity relation (represented, and deliberately refused by solve rather than lowered through a two-branch encoding; see Complementarity semantics — the source contract). dm.solve_batch(models, workers=8) runs many small independent global solves and returns one SolveResult per model in input order. Saved .dopt models and result files carry FAIR provenance and a validation report.
Orchestrator (python/discopt/solver.py): End-to-end Model.solve() connecting all components. At each B&B node: solve continuous NLP relaxation with the interior-point method [Nocedal and Wright, 2006], prune infeasible nodes, fathom integer-feasible solutions, branch on most fractional variable. solve(warm_start=prev) now reuses the dual side as well as the primal point: on the single-NLP routes whose backend reports them, SolveResult.kkt carries the terminal KKT residuals (scaled and _unscaled) and the interior-point barrier_parameter that seeds the next solve; it is None on every branch-and-bound route.
Certified-global MINLP via AMP (python/discopt/solvers/amp.py): Adaptive Multivariate Partitioning [Nagarajan et al., 2019] for nonconvex MINLPs (bilinear, signomial, concave). Iterates a piecewise-McCormick / convex-hull MILP relaxation against an NLP subproblem (Ipopt), refining the partition where the relaxation gap is largest. At every iteration LB_k <= global_opt <= UB_k, so termination yields a certified suboptimality bound. Invoked with Model.solve(solver="amp"); see Certified-global MINLP with AMP.
Parameter estimation (python/discopt/estimate.py): Model-based parameter estimation via weighted least-squares NLP with exact sensitivity Jacobians via JAX autodiff (no finite differences), Fisher-Information-based covariance, and confidence intervals [Franceschini and Macchietto, 2008]. Optimal design of experiments (FIM-based D/A/E-optimal design, sequential DoE, identifiability analysis) lives in the standalone discopt-doe plugin, which shares the same Experiment interface.
Try it without installing anything#
discopt runs in the browser — the whole solver, compiled to WebAssembly, with an editor and a console. See Run discopt in your browser, or go straight to the solver page:
Quick Start#
from discopt import Model
m = Model("example")
x = m.continuous("x", lb=0, ub=5)
y = m.continuous("y", lb=0, ub=5)
z = m.binary("z")
m.minimize(x**2 + y**2 + z)
m.subject_to(x + y >= 1)
m.subject_to(x**2 + y <= 3)
result = m.solve()
print(result.status) # "optimal"
print(result.objective) # 0.5
print(result.x) # {"x": 0.5, "y": 0.5, "z": 0.0}
Parameter Estimation#
discopt includes model-based parameter estimation using exact JAX autodiff for
Fisher Information Matrix computation. (Optimal experimental design on the same
Experiment interface is provided by the
discopt-doe plugin:
pip install discopt-doe.)
from discopt.estimate import Experiment, ExperimentModel, estimate_parameters
import discopt.modeling as dm
import numpy as np
# Define an experiment: y = k * x
class MyExperiment(Experiment):
def create_model(self, **kwargs):
m = dm.Model("exp")
k = m.continuous("k", lb=0.01, ub=20)
x = m.continuous("x", lb=0.1, ub=10)
return ExperimentModel(
model=m,
unknown_parameters={"k": k},
design_inputs={"x": x},
responses={"y": k * x},
measurement_error={"y": 0.1},
)
# Estimate k from data
exp = MyExperiment()
data = {"y": 6.0} # observed at some x
result = estimate_parameters(exp, data)
print(result.parameters) # {"k": ...}
print(result.confidence_intervals)
NLP Backend Comparison#
Model.solve() accepts these nlp_solver selectors:
Backend |
Implementation |
Use Case |
|---|---|---|
|
Pure-Rust Ipopt port |
Universal default: LP/QP/MILP/MIQP/NLP/MINLP |
|
Ipopt via cyipopt |
NLP node and continuous solves; most robust |
|
Pure-Rust warm-started simplex B&B |
MILP; the fully JAX-free MILP path |
|
Back-compat aliases |
Simplex-first LP/MILP routing; resolve to POUNCE for NLP/MINLP |
The pure-JAX interior-point method has been retired. nlp_solver="ipm" is kept as
an alias so existing scripts keep working: it selects the simplex-first matrix
routing for LP/MILP and resolves to POUNCE for NLP/MINLP. See
NLP Backend Comparison: POUNCE vs Ipopt for a measured comparison.
result = model.solve() # default: POUNCE
result = model.solve(nlp_solver="pounce") # POUNCE (pure-Rust Ipopt port)
result = model.solve(nlp_solver="ipopt") # Ipopt via cyipopt
result = model.solve(nlp_solver="simplex") # pure-Rust simplex MILP B&B
Contents#
Tutorials — Problem Classes
- Linear Programming
- Quadratic Programming
- Mixed-Integer Linear Programming (MILP)
- 0-1 Knapsack: Packing a Backpack
- Mixed-Integer Quadratic Programming (MIQP)
- Mixed-Integer Nonlinear Programming (MINLP)
- Generalized Disjunctive Programming (GDP)
- Dynamic Optimization with DAE Collocation
- Robust Optimization
- Adjustable Robust Optimization
- Multi-Objective Optimization
- Complementarity and MPEC Models
- Bilevel Optimization (Stackelberg games)
- Bilevel via KKT +
Model.complementarity() - Stochastic Programming (two-stage recourse)
- The pooling problem and the pq-formulation (
discopt.pooling) - Geometric programming
Tutorials — Solver Backends
- Outer Approximation Solver
- MIP-NLP decomposition
- Benders Decomposition
- Generalized Benders Decomposition
- Lagrangian Relaxation
- The Automatic Decomposition Advisor
- Certified-global MINLP with AMP
- Black-box global search with DIRECT (
solver="direct") - Expensive black boxes: the surrogate backend (
solver="surrogate") - POUNCE: Pure-Rust Ipopt Port
- POUNCE: Parametric Sensitivity Analysis (sIPOPT)
- Differentiable optimization layers (POUNCE-backed)
- Differentiable MILP / MIQP
- cyipopt: The Ipopt Solver Backend
- Solver Backend Selection Guide
Tutorials
Advanced Features
- Disjunction semantics — the source contract
- Complementarity semantics — the source contract
- Global optimization: what discopt can and can’t certify
- discopt Advanced Features
- Solver Internals: BARON-Parity Features
- Presolve
- Bound tightening: FBBT, row activity, and OBBT
- Debugging infeasible models with an IIS
- Conflict analysis and no-good cuts
- Primal Improvement Heuristics
- Derivatives of models
- Parametric Sensitivity Analysis (sIPOPT-style)
- LLM Integration
- LLM Integration
- Warm-Starting with Initial Solutions
- Convex NLP Fast Path
- Convexity detection
- Deriving envelopes & relaxations with SymPy
- Reduced-Space Global Optimization of Hidden-Function Models
- Head-to-head:
discoptvs. SUSPECT - Nonlinear Branch & Bound (NLP-BB)
- Implicit-Function Node: Recycle Streams and Algebraic Loops
- External Functions: Grey-Box Models with Your Own Derivatives
- Grey-Box Design: Sizing a Pipeline Around an Iterative Friction Solver
- Exporting Models to MPS and LP Formats
- Saving and loading models
- Rich model representations
- Using discopt as a Pyomo solver
- Callbacks and User-Defined Cuts
- Interactive Branch-and-Bound Debugger
Solver Backends
Applications
- Neural Network Embedding
- Scaling ReLU Network Embeddings: discopt vs OMLT, and Depth vs Width
- Hybrid physics + ML: training neural DAEs
- Minimum-Energy Crossing Points (MECP / MECI)
- AC optimal power flow (
discopt.opf) - Decision-Focused Learning with discopt
- Parameter Estimation
- One model, four jobs: simulate, fit, design, optimize
API Reference
- API Reference
- discopt
- discopt.batch
- discopt.benchmarks
- discopt.bilevel
- discopt.block_structure
- discopt.callbacks
- discopt.cli
- discopt.conflict
- discopt.constants
- discopt.convexity_report
- discopt.dae
- discopt.daemon
- discopt.debug
- discopt.decomposition
- discopt.dev
- discopt.estimate
- discopt.export
- discopt.gams
- discopt.gdp
- discopt.gp
- discopt.infeasibility
- discopt.interfaces
- discopt.llm
- discopt.ml
- discopt.mo
- discopt.modeling
- discopt.mpec
- discopt.mpec_report
- discopt.nn
- discopt.operators
- discopt.parametric
- discopt.profiles
- discopt.provenance
- discopt.pyomo
- discopt.qubo_primal
- discopt.result_io
- discopt.ro
- discopt.sensitivity
- discopt.serialize
- discopt.skills
- discopt.solver
- discopt.solver_tuning
- discopt.solvers
- discopt.solvers.amp
- discopt.solvers.convex_ipm_pounce
- discopt.solvers.direct
- discopt.solvers.gdpopt_loa
- discopt.solvers.gurobi
- discopt.solvers.lp_backend
- discopt.solvers.lp_milp_highs
- discopt.solvers.lp_pounce
- discopt.solvers.lp_simplex
- discopt.solvers.milp_highs
- discopt.solvers.milp_pounce
- discopt.solvers.milp_simplex
- discopt.solvers.mip_nlp
- discopt.solvers.mip_nlp_candidates
- discopt.solvers.mip_nlp_options
- discopt.solvers.mip_nlp_rootsearch
- discopt.solvers.nlp_backend
- discopt.solvers.nlp_ipopt
- discopt.solvers.nlp_native
- discopt.solvers.nlp_pounce
- discopt.solvers.oa
- discopt.solvers.qp_pounce
- discopt.solvers.scip
- discopt.solvers.sipopt
- discopt.solvers.surrogate
- discopt.status
- discopt.stochastic
- discopt.symbolic
- discopt.tightening
- discopt.transformations
- discopt.validation
- discopt.warm_start
- discopt