A hybrid Mixed-Integer Nonlinear Programming (MINLP) solver combining a Rust backend, JAX automatic differentiation, and Python orchestration. Solves MINLP problems via NLP-based spatial Branch and Bound with JIT-compiled objective/gradient/Hessian evaluation.
- Algebraic modeling API -- continuous, binary, and integer variables with operator overloading
- Spatial Branch and Bound -- Rust-powered node pool, branching, and pruning
- JIT-compiled NLP evaluation -- objective, gradient, Hessian, and constraint Jacobian via JAX
- Three NLP backends -- pure-JAX interior-point method (default, vmap-batched), POUNCE (pure-Rust Ipopt port), cyipopt (Ipopt)
- Convex relaxations -- McCormick envelopes (21 functions including sigmoid/softplus/tanh), piecewise McCormick, alphaBB underestimators
- Neural network embedding -- embed trained feedforward networks (ReLU, sigmoid, tanh, softplus) as MINLP constraints via big-M, full-space, and reduced-space strategies; interval arithmetic bound propagation; ONNX import (
pip install discopt[nn]) - Generalized disjunctive programming --
BooleanVar, propositional logic operators (land,lor,lnot,atleast,atmost,exactly),either_or(),if_then(); reformulated via big-M, multiple big-M (LP-tightened), hull, or Logic-based Outer Approximation (gdp_method="loa") - Presolve -- FBBT (interval arithmetic, probing, Big-M simplification), OBBT with LP warm-start
- Cutting planes -- reformulation-linearization (RLT) and outer approximation (OA)
- GNN branching policy -- bipartite graph neural network trained on strong branching data
- Primal heuristics -- multi-start NLP, feasibility pump
- Differentiable optimization -- parameter sensitivity via envelope theorem and KKT implicit differentiation
- .nl file import -- read AMPL-format models via Rust parser
- Dynamic optimization -- DAE collocation (Radau/Legendre) and finite differences for optimal control, parameter estimation, and PDE-constrained optimization
- CUTEst interface -- NLP benchmarking against the CUTEst test set
- LLM integration (optional) -- conversational model building, diagnostics, and reformulation suggestions
- 1650+ tests -- 141 Rust + 1510+ Python
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}Model.solve() --> Python orchestrator --> Rust TreeManager (B&B engine)
| |
JAX NLPEvaluator Node pool / branching / pruning
NLP backends: Zero-copy numpy arrays (PyO3)
pounce (pure-Rust Ipopt port)
ipm (pure-JAX, vmap batch) [default]
cyipopt (Ipopt)
Rust backend (crates/discopt-core): Expression IR, Branch and Bound tree (node pool, branching, pruning), .nl file parser, FBBT/presolve (interval arithmetic, probing, Big-M simplification).
Rust-Python bindings (crates/discopt-python): PyO3 bindings with zero-copy numpy array transfer for the B&B tree manager, expression IR, batch dispatch, and .nl parser.
JAX layer (python/discopt/_jax): DAG compiler mapping modeling expressions to JAX primitives, JIT-compiled NLP evaluator (objective, gradient, Hessian, constraint Jacobian), McCormick convex/concave relaxations (21 functions), and a relaxation compiler with vmap support.
Solver wrappers (python/discopt/solvers): POUNCE (pure-Rust Ipopt port), cyipopt NLP wrapper for Ipopt, HiGHS LP and MILP wrappers with warm-start support.
CUTEst interface (python/discopt/interfaces/cutest.py): PyCUTEst-based evaluator for NLP benchmarking against the CUTEst test set.
Orchestrator (python/discopt/solver.py): End-to-end Model.solve() connecting all components. At each B&B node: solve continuous NLP relaxation with tightened bounds, prune infeasible nodes, fathom integer-feasible solutions, branch on most fractional variable.
| Backend | Implementation | Use Case |
|---|---|---|
ipm (default) |
Pure-JAX IPM | B&B inner loop; GPU-batched via jax.vmap |
pounce |
Pure-Rust Ipopt port | Single-problem NLP; fastest wall-clock |
cyipopt |
Ipopt via cyipopt | Single-problem NLP; most robust |
For single continuous solves the ipm default is promoted to a KKT-valid
backend, resolving to POUNCE when installed and falling back to cyipopt.
result = model.solve(nlp_solver="ipm") # Pure-JAX (default)
result = model.solve(nlp_solver="pounce") # POUNCE (pure-Rust Ipopt port)
result = model.solve(nlp_solver="cyipopt") # IpoptPerformance measured on Apple M4 Pro (CPU, JAX 0.8.2). "Warm" times exclude JIT compilation. All solvers produce matching objective values.
| Problem Class | discopt | Comparison | Notes |
|---|---|---|---|
| LP (n=100) | 0.015s warm | HiGHS 0.002s, scipy 0.002s | Algebraic extraction, no autodiff |
| QP (n=100) | 0.04s warm | scipy SLSQP 0.02s | Was 66s before algebraic extraction |
| MILP (n=25) | 0.002s | HiGHS MIP 0.002s | B&B + LP relaxation, correct objectives |
| MIQP (n=10) | 0.004s | NLP path 4.9s | QP-specialized path: 1000x+ speedup |
| NLP (n=20, Rosenbrock) | IPM 1.1s warm, POUNCE 0.42s, Ipopt 0.43s | -- | POUNCE fastest single-solve; IPM best for batched B&B |
| MINLP (n=10) | 0.9s (batch=1) | 0.9s (batch=16) | vmap batching helps with deeper B&B trees |
See the benchmark notebooks for full scaling plots and details:
- Benchmarks by Problem Class -- LP, QP, MILP, MIQP, NLP (3 backends), MINLP
- IPM vs POUNCE vs Ipopt -- detailed NLP backend comparison
- Batch IPM vs Ipopt -- vmap-batched IPM for B&B inner loops
Requires Rust 1.84+ and Python 3.10+. POUNCE (the default single-solve NLP backend) is a pure-Rust Ipopt port with no system dependencies; cyipopt is an optional fallback that needs the Ipopt C library.
# Install the POUNCE NLP backend (pure-Rust Ipopt port)
pip install pounce-solver
# Optional cyipopt fallback (needs the Ipopt C library; macOS: brew install ipopt)
pip install "discopt[ipopt]"
# Build Rust-Python bindings
cd crates/discopt-python && maturin develop && cd ../..
# Run the fast default PR battery
cargo test -p discopt-core
JAX_PLATFORMS=cpu JAX_ENABLE_X64=1 make testmake test matches the PR CI gate: ordinary non-slow tests plus the
pr_correctness subset. Full correctness, integration, and benchmark markers
remain available through the explicit Make targets.
For problems with nonconvex nonlinearities (bilinear, trilinear, signomial, trig), the default branch-and-bound path only certifies optimality when the relaxation is convex. The Adaptive Multivariate Partitioning (AMP) solver gives discopt a certified-global path for these problems:
import discopt.modeling as dm
m = dm.Model("concave_qp")
c = [-1.0, 0.5, 1.5]
xs = [m.continuous(f"x{i}", lb=-2.0, ub=2.0) for i in range(3)]
m.subject_to(sum(xs) >= -1.0)
m.subject_to(sum(xs) <= 3.0)
m.minimize(sum(-((xs[i] - c[i]) ** 2) for i in range(3))) # concave
result = m.solve(solver="amp", rel_gap=1e-4)
print(result.status, result.objective, result.gap)AMP iterates a piecewise-McCormick / convex-hull MILP relaxation against an
NLP subproblem (Ipopt) and refines the partition where the relaxation gap is
largest. At every iteration LB_k <= global_opt <= UB_k, so termination at
gap <= rel_gap yields a certified global optimum.
Common tuning knobs (all keyword-only on Model.solve(solver="amp", ...)):
| Option | Default | Effect |
|---|---|---|
rel_gap |
1e-4 |
Relative optimality gap stop criterion |
max_iter |
100 |
Hard cap on partition-refinement iterations |
n_init_partitions |
4 |
Initial partitions per discretized variable |
convhull_formulation |
"disaggregated" |
"sos2" or "facet" for tighter relaxations |
convhull_ebd |
False |
Logarithmic Gray-code embedded SOS2 binaries |
presolve_bt |
True |
OBBT/FBBT bound tightening before the first MILP |
obbt_at_root |
True |
Strengthen variable bounds at the root |
partition_method |
"adaptive" |
How to pick which variable/interval to refine |
A worked end-to-end example with a non-trivially nonconvex model and the
tuning knobs above is in docs/notebooks/amp_global_minlp.ipynb.
Routine AMP development uses a fast default regression battery. The fast
environment uses solver-independent checks plus HiGHS-backed MILP relaxations,
and excludes optional cyipopt, longer Alpine, MINLPTests, and incidence-style
AMP benchmark coverage. AMP and PR-fast Make targets run pytest through
scripts/run_memory_capped_pytest.sh, which applies a 16 GB address-space cap
with prlimit when available. Override with PYTEST_MEMORY_LIMIT_MB=..., or
set PYTEST_MEMORY_LIMIT_MB=0 to disable the cap. The broad make test-quick
dev-loop target remains uncapped and excludes memory_heavy tests.
make test-amp-fastAlpine-reference, MINLPTests, cyipopt, and incidence-style AMP checks are opt-in because they can require optional solvers and longer solve budgets:
# Uses a fresh .venv and pixi-provided solver libraries rather than a local Python env.
pixi exec -s python=3.12 -s ipopt -s pkg-config -s c-compiler -s cxx-compiler -s gfortran -- \
uv venv --allow-existing .venv
source .venv/bin/activate
uv pip install maturin pytest pytest-timeout numpy scipy jax jaxlib highspy cyipopt
uv pip install -e ".[dev,ipopt,highs]"
maturin develop
make test-amp-integrationFor WSL or memory-constrained machines, keep broad AMP/JAX runs capped and use a
bounded xdist worker count rather than -n auto:
PYTEST_MEMORY_LIMIT_MB=16384 PYTEST_XDIST_WORKERS=2 make test
PYTEST_MEMORY_LIMIT_MB=16384 make test-amp-integrationWSL users should also set explicit memory and swap limits in .wslconfig so a
single uncapped compile-heavy test cannot restart the host session. A stricter
12 GB cap is useful for reproducing memory pressure, but the current JAX/XLA
CPU stack can reserve more than 12 GB of virtual address space during AMP runs;
use the memory_heavy marker selection when running with tighter caps.
The full Python test suite remains available with make test-all.
After installation, the discopt command is available on your PATH:
discopt about # Version and installation info
discopt test # Smoke-test the install
discopt convert in.gms out.nl
discopt install-skills # Install Claude Code slash commands and agents
discopt doe ... # Model-based design of experiments (5-verb loop)discopt doe drives a model-based design-of-experiments loop around a
single .xlsx workbook that travels between the lab bench and the CLI.
Five verbs cover the full cycle:
# 1. List the built-in templates (linear, polynomial-1d,
# response-surface-2d, response-surface-3d).
discopt doe templates
# 2. Generate an initial optimal design.
discopt doe new response-surface-2d \
--input temp:50:100 --input ph:3:9 \
--response yield --error 0.5 --n 6 -o campaign.xlsx
# 3. (Run the experiments; fill the `yield` column in campaign.xlsx; save.)
discopt doe status campaign.xlsx
# 4. Fit parameters from the completed runs (writes parameters + FIM sheets).
discopt doe fit campaign.xlsx
# 5. Append a next batch of D-optimal runs that reuse the fitted FIM.
discopt doe extend campaign.xlsx --n 4Every verb also takes --json for LLM agent or GUI consumption. The
workbook is the single source of truth — status, fit, and
extend only need the file path. An --module pkg.mod:MyExperiment
escape hatch on new swaps the template for any custom Experiment
subclass. Install the optional dependency with
pip install discopt[doe].
A Streamlit GUI over the same workflow ships under
pip install 'discopt[doe-gui]':
discopt doe gui # blank slate; create or open from sidebar
discopt doe gui campaign.xlsx # drop straight into an existing campaignThe GUI binds directly to the same do_* functions the CLI uses, so
both surfaces stay in lockstep automatically.
A separate discopt-dev script ships developer-only commands used from inside
a discopt source checkout (literature scanner, adversary tester, the arXiv /
OpenAlex search helpers and the report writer they call):
# Search arXiv for recent papers
discopt-dev search-arxiv 'all:"spatial branch and bound"' --max-results 10 --start-date 2026-01-01
# Search OpenAlex
discopt-dev search-openalex "McCormick relaxation" --from-date 2026-01-01 --to-date 2026-03-31
# Write a report from stdin
echo "report content" | discopt-dev write-report reports/output.mdAll discopt-dev search subcommands output structured JSON. The /discoptbot
literature-scanner slash command uses them to automatically find and summarize
relevant new papers from arXiv and OpenAlex.
Tutorial notebooks are available in docs/notebooks/:
- Quickstart -- basic modeling and solving
- MINLP Examples -- mixed-integer nonlinear programs
- Advanced Features -- relaxations, presolve, cutting planes, branching policies
- IPM vs Ipopt -- backend comparison
- Batch IPM -- vmap-batched interior-point solving
- Dynamic Optimization -- DAE collocation for optimal control, parameter estimation, and PDEs
- Neural Network Embedding -- optimize over trained ML surrogates as MINLP constraints
- Decision-Focused Learning -- differentiable optimization in ML pipelines
- GDP Tutorial -- disjunctive programming, logical constraints, big-M/hull/LOA reformulations
Full documentation is built with Jupyter Book: jupyter-book build docs/
Last updated: 2026-02-16
| Category | Count |
|---|---|
Python source (python/discopt/) |
65 files, ~27,200 lines |
Rust source (crates/) |
19 files, ~10,700 lines |
Test code (python/tests/) |
41 files, ~24,500 lines |
| Total source + tests | 125 files, ~62,400 lines |
| Python tests | 1,510+ |
| Rust tests | 141 |
Tutorial notebooks (docs/notebooks/) |
21 |
| Git commits | 99 |
See ROADMAP.md for the full development roadmap and task history.
