|
1 | 1 | import functools as ft |
2 | 2 | from collections.abc import Callable |
3 | | -from typing import Any, TypeVar |
| 3 | +from typing import Any, NamedTuple, TypeVar |
4 | 4 |
|
5 | 5 | import diffrax as dfx |
6 | 6 | import equinox as eqx |
@@ -1134,3 +1134,161 @@ def get_nn_weights(model): |
1134 | 1134 | jnp.array([0.5, 0.5]), |
1135 | 1135 | ), |
1136 | 1136 | ) |
| 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