Skip to content

Commit d2bf7fc

Browse files
johannahaffnerJohanna Haffner
andauthored
Mini PR: test cases for bounded minimisation (#176)
* test cases for bounded minimisation * add longer explanation of what is meant by Cauchy poitns --------- Co-authored-by: Johanna Haffner <johanna.haffner@bsse.ethz.ch>
1 parent b3f628a commit d2bf7fc

1 file changed

Lines changed: 159 additions & 1 deletion

File tree

tests/helpers.py

Lines changed: 159 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import functools as ft
22
from collections.abc import Callable
3-
from typing import Any, TypeVar
3+
from typing import Any, NamedTuple, TypeVar
44

55
import diffrax as dfx
66
import equinox as eqx
@@ -1134,3 +1134,161 @@ def get_nn_weights(model):
11341134
jnp.array([0.5, 0.5]),
11351135
),
11361136
)
1137+
1138+
1139+
# Easy tests for bounded and constrained optimisation. These are all smoke tests, but
1140+
# they are varied enough that they help catch quite a few different cases early.
1141+
# For example, initial points may be on the constraint boundary, or be a (general)
1142+
# Cauchy point, upper as well as lower bounds may be blocking, etc.
1143+
1144+
1145+
def _paraboloid(y, args):
1146+
del args
1147+
squares = jtu.tree_map(lambda x: x**2, y)
1148+
squares, _ = jfu.ravel_pytree(squares)
1149+
return jnp.sum(squares)
1150+
1151+
1152+
class _Point(NamedTuple):
1153+
a: float
1154+
b: float
1155+
1156+
1157+
# Vary cases and pytree types: smoke tests with trivial quadratic function
1158+
# What do we mean by Cauchy point? This point is the first local minimiser
1159+
# of a piecewise linear path along the surface of a hypercube defined by the
1160+
# bounds on the optimisation problem, identified by solving for the minimum
1161+
# of the quadratic approximation to the target function on each of the segments
1162+
# making up the piecewise linear path. This point is used e.g. in BFGS-B and
1163+
# its variants to identify the set of active bound constraints, and then solve
1164+
# for a new direction in the unconstrained subspace.
1165+
# In the literature, it is sometimes also called the "generalised Cauchy point"
1166+
# (e.g. in Trust Region Methods by Conn, Gould, Toint).
1167+
# Below, if locations of the Cauchy point are indicated, these are given with
1168+
# respect to the initial point. For example, if the Cauchy point is at the
1169+
# minimum, then we expect to encounter it in the first step due to the projection
1170+
# onto the hypercube, even if a full gradient or Newton step would have led us
1171+
# to leave the feasible set.
1172+
bounded_paraboloids = (
1173+
# fn, y0, args, bounds, expected result
1174+
# No bounds active at (0.0, 0.0), bounds far from minimum
1175+
(
1176+
_paraboloid,
1177+
jnp.array([-1.0, -5.0]),
1178+
None,
1179+
(jnp.array([-jnp.inf, -jnp.inf]), jnp.array([1.0, 1.0])),
1180+
jnp.array([0.0, 0.0]),
1181+
),
1182+
# One upper bound active at (0.0, 0.0), Cauchy point is at minimum
1183+
(
1184+
_paraboloid,
1185+
jnp.array([-4.0, -1.0]),
1186+
None,
1187+
(jnp.array([-jnp.inf, -jnp.inf]), jnp.array([0.0, 1.0])),
1188+
jnp.array([0.0, 0.0]),
1189+
),
1190+
# Two upper bounds active at (0.0, 0.0), Cauchy point is at minimum
1191+
(
1192+
_paraboloid,
1193+
[-1.0, -1.0],
1194+
None,
1195+
([-jnp.inf, -jnp.inf], [0.0, 0.0]),
1196+
[0.0, 0.0],
1197+
),
1198+
# One bound active at (-1.0, 0.0)
1199+
(
1200+
_paraboloid,
1201+
{"a": -3.0, "b": -1.0},
1202+
None,
1203+
({"a": -jnp.inf, "b": -jnp.inf}, {"a": -1.0, "b": 1.0}),
1204+
{"a": -1.0, "b": 0.0},
1205+
),
1206+
# Two bounds active at (-1.0, 0.0), initial point at minimum and Cauchy point
1207+
(
1208+
_paraboloid,
1209+
(-1.0, 0.0),
1210+
None,
1211+
((-jnp.inf, -jnp.inf), (-1.0, 0.0)),
1212+
(-1.0, 0.0),
1213+
),
1214+
# One bound active at (0.0, -1.0), initial point out of bounds
1215+
(
1216+
_paraboloid,
1217+
(0.0, {"b": -2.0}),
1218+
None,
1219+
((-jnp.inf, {"b": -jnp.inf}), (1.0, {"b": -1.0})),
1220+
(0.0, {"b": -1.0}),
1221+
),
1222+
# Two bounds active at (0.0, -1.0)
1223+
(
1224+
_paraboloid,
1225+
_Point(-1.0, -7.0),
1226+
None,
1227+
(_Point(-jnp.inf, -jnp.inf), _Point(0.0, -1.0)),
1228+
_Point(0.0, -1.0),
1229+
),
1230+
# Two bounds active at (-1.0, -1.)
1231+
(
1232+
_paraboloid,
1233+
jnp.array([-3.0, -1.0]),
1234+
None,
1235+
(jnp.array([-jnp.inf, -jnp.inf]), jnp.array([-1.0, -1.0])),
1236+
jnp.array([-1.0, -1.0]),
1237+
),
1238+
# Two bounds active at (1, 1), lower bound blocking
1239+
(
1240+
_paraboloid,
1241+
jnp.array([2.0, 3.0]),
1242+
None,
1243+
(jnp.array([1.0, 1.0]), jnp.array([jnp.inf, jnp.inf])),
1244+
jnp.array([1.0, 1.0]),
1245+
),
1246+
)
1247+
1248+
1249+
def _scalar_rosenbrock(y, args):
1250+
out = rosenbrock(y, args)
1251+
flat, _ = jfu.ravel_pytree(out)
1252+
return jnp.sum(flat)
1253+
1254+
1255+
minimise_bounded_with_local_minima = (
1256+
# fn, y0, args, bounds, expected result
1257+
(
1258+
_himmelblau,
1259+
[4.0, 1.0], # Initialise between two minima
1260+
(jnp.array(11.0), jnp.array(7.0)),
1261+
([0.0, 0.0], [5.0, 5.0]), # Quadrant: I
1262+
[3.0, 2.0],
1263+
),
1264+
(
1265+
_himmelblau,
1266+
jnp.array([4.0, -1.0]),
1267+
(jnp.array(11.0), jnp.array(7.0)),
1268+
(jnp.array([0.0, -5.0]), jnp.array([5.0, 0.0])), # II
1269+
jnp.array([3.584428, -1.848126]),
1270+
),
1271+
(
1272+
_himmelblau,
1273+
(-3.0, -2.0), # TODO: This problem is sensitive to initialisation
1274+
# This makes it a good test case for inertia correction and initialisation of
1275+
# dual variables. It converges to a stationary point when started at (-3, -1).
1276+
(jnp.array(11.0), jnp.array(7.0)),
1277+
((-5.0, -5.0), (0.0, 0.0)), # Quadrant: III
1278+
(-3.779310, -3.283186),
1279+
),
1280+
(
1281+
_himmelblau,
1282+
[jnp.array(-3.0), jnp.array(2.0)],
1283+
(jnp.array(11.0), jnp.array(7.0)),
1284+
([jnp.array(-8.0), jnp.array(0.0)], [jnp.array(0.0), jnp.array(8.0)]), # IV
1285+
[jnp.array(-2.805118), jnp.array(3.131312)],
1286+
),
1287+
(
1288+
_scalar_rosenbrock,
1289+
(0.4, 0.0),
1290+
None,
1291+
((-5.0, -6.0), (4.0, 5.0)),
1292+
(1.0, 1.0),
1293+
),
1294+
)

0 commit comments

Comments
 (0)