Modelling¶
A jaxglobal model is plain Python. There is no modelling language: the
objective and the constraints are functions of one 1-D array x, written with
jax.numpy, and the search space is a box lo <= x <= hi.
minimize(f, lo, hi, ineq=g, eq=h, integer=[...])
Writing the objective¶
f(x) must be traceable by jax.make_jaxpr and return a scalar. Inside it you
can use:
- arithmetic,
jnp.exp,log,sqrt, powers,abs, trigonometric and hyperbolic functions,erf,jax.nn.*activations, and any other smooth primitive with a known range (its relaxation is derived automatically by αBB from interval bounds of its second derivative); jnp.where,jnp.maximum/minimum,clip, comparisons: these are resolved on boxes where the interval decides the branch, and relaxed by the hull otherwise;- array operations: indexing, slicing, reshape, broadcast, concatenate,
reductions,
dot/matmul/einsumwith constant matrices, and 1-D inner products between two variable-dependent arrays; - Python loops and helper functions, and
jit/custom_jvpsub-calls: tracing removes them; jax.lax.scan, andjax.lax.fori_loopwith Pythonintbounds (it lowers to a scan): the bounding interpreters unroll the loop (at most 10,000 iterations per scan), so trace and compile time grow with the trip count, as with a Python loop;lax.cond/switchon a constant index.
Not supported: lax.while_loop (and fori_loop with traced bounds, which
lowers to one), lax.cond whose predicate depends on x (use jnp.where),
and dot_general between two variable-dependent arrays beyond 1-D inner
products. An unsupported primitive raises NotImplementedError naming the
primitive. Python control flow on static values is fine.
import jax
import jax.numpy as jnp
W1, b1, W2, b2 = ... # trained weights (constants)
def surrogate(x): # a tanh MLP is just a function
return (W2 @ jnp.tanh(W1 @ x + b1) + b2)[0]
def cost(x):
friction = jnp.where(x[0] < 2300.0, 64.0 / x[0], 0.316 * x[0] ** -0.25)
return friction * x[1] ** 2 + surrogate(x)
Bounds¶
lo and hi are sequences or arrays of length n. Bounds may be infinite:
finite ones are derived first (jaxglobal/domain.py) by FBBT from the
declared box, fixing unused variables, and then FBBT against f <= UB once a
local solve finds a feasible point. A variable that nothing bounds gives status
"unbounded_domain" (with message naming the variables) rather than a
guessed box. FBBT cannot prove bounds that need joint reasoning over several
constraints, so give finite bounds whenever you know them: tighter boxes give
tighter relaxations.
Constraints¶
minimize(f, lo, hi, ineq=g, eq=h) handles g(x) <= 0 and h(x) == 0, where
g and h are plain JAX functions returning a scalar or a 1-D array.
- Lower bound (
jaxglobal/lp.py): the relaxations give convex functionscv_f,cv_g,cv_h,-cc_h.jax.jacfwdturns them into affine cuts at the box center, 2n axis points, and 4 Kelley points. The small LP is solved by a primal–dual interior point method in JAX (Mehrotra predictor–corrector steps in awhile_loopthat stops early once converged), so itvmaps over boxes. The bound does not trust the LP solver. Weak duality gives a valid bound for any multipliers y ≥ 0, and the best value over all IPM iterates is kept. A badly converged LP gives a weaker bound, never a wrong one. Interval arithmetic prunes boxes that are provably infeasible. Problems whose functions are all quadratic, or polynomial up to degree 6, are bounded instead over a lifted sparse LP with one column per product, McCormick facets and RLT rows (jaxglobal/quadratic.py,quadratic=Trueby default). With binary variables the lifted LP also gets cuts on products of binaries (static rows plus a separation pool) and bound-factor RLT rows. Functions that are structurally convex on the box (jaxglobal/convexity.py) are cut by themselves, which is outer approximation. - Upper bound: a local NLP solve with JAX gradients and Jacobians, started
from the LP solutions: pounce (default when installed), optax or scipy
SLSQP (
local_solver=). A point counts as feasible when the violation is ≤feas_tol. - Branching only on variables that appear nonlinearly, read off the jaxpr (sampled Hessians as a fallback).
statusis"optimal","infeasible","max_iter","time_limit","unbounded_domain","unverified"or"unproven"(see Correctness).
| problem | n | f* | found | proven LB | nodes | time |
|---|---|---|---|---|---|---|
| G06 | 2 | −6961.813876 | −6961.813876 | −6961.817327 | 11 | 18.0s |
| G08 | 2 | −0.095825 | −0.095825 | −0.095830 | 15 | 28.2s |
| G11 (equality) | 2 | 0.750000 | 0.750000 | 0.749999 | 7 | 6.2s |
| G24 (disconnected feasible set) | 2 | −5.508013 | −5.508013 | −5.508016 | 43 | 8.7s |
| G04 | 5 | −30665.538672 | −30665.538672 | −30665.540938 | 19 | 28.3s |
| Haverly pooling | 9 | −400.000000 | −400.000000 | −400.000076 | 17 | 11.3s |
| Reactor network | 6 | −0.388811 | −0.388811 | −0.388821 | 159 | 19.7s |
Equality optima can sit slightly below f* because of feas_tol (1e-6).
Integer variables¶
minimize(f, lo, hi, ineq=g, integer=[0, 3]) (indices or a boolean mask)
makes those variables integer. Their bounds are rounded inward, FBBT rounds
their ranges, branching splits them at integer values, and integer dives and
root probing look for incumbents. The returned x is integral, and
Result.verified checks that. The node bounds are the relaxations of the
continuous problem; only the lifted LP of polynomial problems adds cuts on
products of binaries. Integer polynomial problems also get symmetry
(lex-leader) pruning (jaxglobal/symmetry.py), and an unconstrained binary
polynomial of small induced width is solved exactly by variable elimination
(jaxglobal/pseudoboolean.py) with no search. .nl files with integer
variables are read with read_nl(path, integers="mask"), which puts the mask
in m.integer; the default raises NLError rather than silently solve the
continuous relaxation.
Gaussian-process surrogates¶
A GP prediction written in jax.numpy is already a reduced-space model
(Schweidtmann et al. 2021). Write its kernel with jaxglobal.kernels
(matern12, matern32, matern52, se, each a function of the weighted
squared distance d) instead of the jnp formula:
from jaxglobal import kernels
def gp_mean(x):
d = jnp.sum(((x - X_train) / lengthscales) ** 2, axis=-1)
return y_mean + alpha @ kernels.matern52(d)
Each kernel is a JAX primitive that is one convex, nonincreasing function of
d, so it is relaxed by its envelopes; written in jnp, the Matérn kernels
are relaxed factor by factor. Its derivatives are primitives too, so αBB gets
a finite Hessian at the training points (Matérn 5/2 and SE).
The mean k(x)ᵀα needs more than its kernels: relaxed term by term, the signed
sum loses the cancellation between its terms. jaxglobal.gp.gp_mean(x, Z,
lengthscale, alpha, sf2, kernel=...) has the same value. For Matérn 5/2 and SE its
relaxation also uses a first-order Taylor model around the box center whose
remainder is bounded by the mean's RKHS norm,
|μ(x) − μ(c) − ∇μ(c)·δ| ≤ ‖μ‖_H e(δ), with e in closed form and of order
|δ|². On the GP mean of the peaks function (Matérn 5/2, abs/rel gap 1e-3),
same answers:
| training points | nodes, jnp formula |
nodes, kernels.matern52 |
nodes, kernels.matern52 + gp_mean |
|---|---|---|---|
| 20 | 1079* | 81 | 81 |
| 50 | 3477 | 411 | 423 |
| 100 | 6165* | 647† | 407 |
| 200 | 22449 | 2253 | 497 |
| 400 | 91289* | 5519† | 725 |
Nodes are the stored outputs of notebook 06, re-run on 2026-10-21 on a 4-core
cloud VM; the N = 400 solve takes 10 s there, compilation included (21 s
without gp_mean). †From the notebook's previous stored run (2026-10-20),
before gp_mean was a primitive. *Not reproduced by the stored notebook:
from runs on or before 2026-10-17, as is the jnp count for Matérn 3/2 at
N = 50, 1917 nodes (429 with kernels.matern32, as in the notebook). The
jnp formulas of SE and Matérn 1/2 were already relaxed exactly; SE gains
from gp_mean (439 → 361 nodes at N = 50). A/B runs of the mean alone:
benchmarks/results/gp_mean_ab.csv.
The posterior standard deviation needs more: it is a quadratic form in the
kernel vector, and bounds through K⁻¹ inherit its conditioning.
jaxglobal.gp.gp_std(x, Z, lengthscale, A, sf2, kernel=...), with A = K⁻¹, has the same value, and its relaxation uses the GP's structure: the
quadratic form as a sum of squares; the bound
|σ(x) − σ(c)| ≤ sd_prior(f(x) − f(c)), which holds because the posterior
is itself a Gaussian process and does not involve K⁻¹; and, for Matérn 5/2
and SE, the first-order version |σ(x) − √q(δ)| ≤ e(δ), with q the
posterior variance of f(c) + ∇f(c)·δ, a quadratic in closed form whose
square root is convex. On a 95% chance constraint (mean + 1.96 std ≤ c)
the solve is certified in 153 nodes (notebook 06's stored output; 2759
without the two first-order models). The formula did not close in 120 s
(140k nodes; a run on or before 2026-10-17, not reproduced by the stored
notebook). PI without a margin, which needs a positive lower bound on σ near
the data, takes 719 nodes instead of 14359. The larger rule costs a few
seconds of compilation (benchmarks/results/gp_std_ab.csv).
The acquisition functions are primitives too:
expected_improvement(mu, sigma, fmin) is relaxed as the jointly convex,
monotone function it is (the perspective of zΦ(z) + φ(z)), with the concave
envelope over the (mu, sigma) box, and
probability_of_improvement(mu, sigma, target) from its monotonicity plus the
exact envelopes of Φ, instead of Φ, φ and the division one by one. On
the 20-point GP of notebook 06 they took EI from 1401 to 683 nodes and PI from
727 to 491 (the formula counts are from runs on or before 2026-10-17, the
primitive counts from the notebook's 2026-10-20 run). With the first-order
models of the mean and σ, the stored outputs are 527 and 375. Bounding μ and
σ jointly at the box vertices was tried for LCB and EI and gave nothing
beyond the two models (docs/PLAN-acquisition-relaxations.md). Notebook 06
repeats the paper's case studies: GP means, acquisition functions, a chance
constraint and a Bayesian-optimization loop.
Reading AMPL .nl files¶
Any AMPL .nl model, text or binary, as written by AMPL, Pyomo, GAMS (convert)
or downloaded from MINLPLib, can be loaded with jaxglobal.nl.read_nl(path).
It returns an NLModel with f, ineq, eq, lo, hi in the form
minimize takes. The expression trees are compiled to plain jax.numpy code,
so the solver sees an ordinary traced function. Integer variables are read as a
mask with integers="mask" (the default raises an error on integer models;
integers="relax" reads the continuous relaxation). Imported functions,
suffixes, logical constraints and complementarity are not supported.
The reverse direction, writing a JAX model to .nl or to SCIP, is covered in
Export to SCIP and .nl.