optimization-dev / backend /src /manager.py
nhamid289's picture
Deploy optimisation from main (6727a1a)
c964813 verified
Raw History Blame Contribute Delete
4.6 kB
import json
from pathlib import Path
import numpy as np
from logic import OPTIMISERS, Objective, Run, parse_function
ALGORITHMS = {
spec["id"]: spec
for spec in json.loads((Path(__file__).parent / "algorithms.json").read_text())["algorithms"]
}
# Iterations a run can take before stepping stops
MAX_ITERATIONS = 1000
# Sample points for the plotted curve (one variable) or surface (two, per side)
CURVE_POINTS = 401
GRID_POINTS = 101
# Gradient arrows per side
ARROW_POINTS = 15
class UserError(Exception):
"""A mistake the student can fix; its message is shown in the UI."""
def _json_values(values: np.ndarray) -> list:
"""Values as JSON numbers, with null where f has no finite value."""
return [float(v) if np.isfinite(v) else None for v in values]
class Manager:
def __init__(self):
self._objective: Objective | None = None
self._runs: list[Run] = []
def handle_set_function(self, args: dict) -> None:
"""{variables: 1 | 2, expression}; clears the runs."""
self._objective = None
self._runs = []
n = int(args["variables"])
try:
expr = parse_function(str(args["expression"]), n)
except ValueError as e:
raise UserError(str(e))
try:
self._objective = Objective(expr, n)
except Exception:
raise UserError("This function can't be differentiated here. Try writing it another way.")
def handle_function_values(self, args: dict) -> dict | None:
"""f sampled over the plot's {x_range, y_range}; None with no function."""
f = self._objective
if f is None:
return None
with np.errstate(all="ignore"):
if f.n == 1:
x = np.linspace(*args["x_range"], CURVE_POINTS)
return {"x": x.tolist(), "f": _json_values(f.values_on(x))}
x = np.linspace(*args["x_range"], GRID_POINTS)
y = np.linspace(*args["y_range"], GRID_POINTS)
z = f.values_on(*np.meshgrid(x, y))
ax, ay = np.meshgrid(np.linspace(*args["x_range"], ARROW_POINTS), np.linspace(*args["y_range"], ARROW_POINTS))
u, v = f.gradient_on(ax, ay)
return {
"x": x.tolist(),
"y": y.tolist(),
# Rows follow y, columns follow x
"f": [_json_values(row) for row in z],
"arrows": {
"x": ax.ravel().tolist(),
"y": ay.ravel().tolist(),
"u": _json_values(u.ravel()),
"v": _json_values(v.ravel()),
},
}
def handle_set_runs(self, args: dict) -> None:
"""{runs: [{algorithm, start, params}]}: the optimisers to step together."""
self._runs = []
f = self._objective
if f is None:
raise ValueError("Function not set")
runs = []
for run in args["runs"]:
spec = ALGORITHMS[run["algorithm"]]
given = dict(run.get("params", {}))
params = {p["name"]: float(given.get(p["name"], p["default"])) for p in spec["params"]}
start = np.array(run["start"], dtype=float)
if start.shape != (f.n,):
raise ValueError(f"Start point needs {f.n} coordinates, got {start.tolist()}")
with np.errstate(all="ignore"):
if not np.isfinite(f.value(start)):
point = ", ".join(f"{c:g}" for c in start)
raise UserError(f"The function has no finite value at the initial point ({point}).")
runs.append(Run(f, OPTIMISERS[spec["id"]](f, **params), start))
self._runs = runs
def handle_state(self, args: dict) -> dict | None:
"""Every run after {iteration} iterations; None once all have stopped.
"stopped" says why a run ended, at its last point and after it (where
point is None). It is None while the run carries on.
"""
k = int(args["iteration"])
if not self._runs or k > MAX_ITERATIONS:
return None
points = [run.point(k) for run in self._runs]
if all(p is None for p in points):
return None
# Look one iteration ahead to know whether this is a run's last point
ended = [p is None or run.point(k + 1) is None for run, p in zip(self._runs, points)]
return {
"runs": [
{"point": p, "stopped": run.stopped if end else None}
for run, p, end in zip(self._runs, points, ended)
]
}