Skip to content

Tighter relaxations

Intersections of relaxations

The same function can often be written in several forms, and each form relaxes differently. jaxormick.intersect_relaxations combines relaxations of the same function over the same box into one. It takes the larger convex side, the smaller concave side and the intersection of the intervals. The result is a valid relaxation and at least as tight as each of its inputs.

On \([-1, 2]\), \(x^3 - x\) and \(x (x^2 - 1)\) are the same function. The sum of powers has tighter convex and concave sides, because each power gets its envelope. The product has a tighter upper bound, \(6\) instead of \(9\), which is the maximum of the function on the box.

import jax.numpy as jnp

import jaxormick as jmk

reference = jnp.array(0.5)
lower = jnp.array(-1.0)
upper = jnp.array(2.0)

expanded = jmk.relax(lambda x: x**3 - x)(reference, lower, upper)
factored = jmk.relax(lambda x: x * (x**2 - 1))(reference, lower, upper)
both = jmk.intersect_relaxations(expanded, factored)

assert both.convex == expanded.convex > factored.convex
assert both.concave == expanded.concave < factored.concave
assert both.upper == factored.upper == 6.0

The relaxations may also be pytrees of Relaxation objects with the same structure, such as the outputs of two forms of a function with several outputs. The derivatives of the result follow the input that attains the maximum or the minimum, so jax.grad of the intersected convex side is a subgradient.

Each form costs a relaxation of its own. Relaxing two forms costs about twice as much as relaxing one, and both run in the same compiled program when the call is inside jax.jit.

αBB relaxations

relax(f, alpha_bb=True) intersects the McCormick relaxation with the αBB relaxation of Adjiman et al. (1998). αBB adds a quadratic to f that is just large enough to make it convex, based on bounds of the Hessian of f over the box. It is often tighter for smooth functions whose factors relax poorly one at a time. The indefinite quadratic form below is one.

import jax.numpy as jnp

import jaxormick as jmk

matrix = jnp.array([[2.0, -1.0], [0.5, 1.0]])


def quadratic_form(x):
    return x @ (matrix @ x)


reference = jnp.array([0.1, -0.2])
lower = -jnp.ones(2)
upper = jnp.ones(2)

mccormick = jmk.relax(quadratic_form)(reference, lower, upper)
both = jmk.relax(quadratic_form, alpha_bb=True)(reference, lower, upper)

assert both.convex > mccormick.convex
assert both.concave < mccormick.concave

If the bounds of the Hessian show that f is convex on the box, the convex side is f itself. The same holds for the concave side of a concave function.

convex_side, concave_side and affine_relaxation take the same keyword. αBB needs a function that returns one array, and a rule for every primitive in its Hessian. Functions built with jnp.abs, jnp.maximum or jnp.clip have comparisons in their Hessians, and alpha_bb=True raises UnsupportedPrimitiveError for them. The interval bounds are the McCormick bounds, since αBB only changes the convex and concave sides.

The Hessian of a function of \(n\) variables has \(n^2\) entries, and each costs an interval evaluation of a second derivative. See performance for measurements.

Subgradient-tightened bounds

relax(f, tighten_bounds=True) narrows the interval of every intermediate value with its affine relaxations at the middle of the box, after Najman and Mitsos (2019). A narrower interval of an inner value also tightens the relaxations of the values computed from it. In \(e^{x - x^2}\) on \([0, 2]\), the rules bound the exponent by \([-4, 2]\), and the tightened bounds by \([-2, 1]\).

import jax.numpy as jnp
import numpy as np

import jaxormick as jmk


def function(x):
    return jnp.exp(x[0] - x[0] ** 2)


lower = jnp.array([0.0])
upper = jnp.array([2.0])

plain = jmk.interval(function)(lower, upper)
tightened = jmk.interval(function, tighten_bounds=True)(lower, upper)

np.testing.assert_allclose(plain, (np.exp(-4.0), np.exp(2.0)), rtol=1e-6)
np.testing.assert_allclose(tightened, (np.exp(-2.0), np.e), rtol=1e-6)

interval, convex_side, concave_side and affine_relaxation take the same keyword. The tightened bounds are never wider than the plain ones. Values inside the bodies of lax.scan and lax.cond keep their plain bounds, but the outputs of the loop or branch are tightened.

Each relaxed value carries one subgradient per variable, so the cost grows with the number of variables, like forward-mode differentiation. See performance for measurements.