soar-lite 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- soar_lite-0.1.0/.gitignore +37 -0
- soar_lite-0.1.0/CHANGELOG.md +30 -0
- soar_lite-0.1.0/CITATION.cff +25 -0
- soar_lite-0.1.0/CONTEXT.md +325 -0
- soar_lite-0.1.0/CONTRIBUTING.md +110 -0
- soar_lite-0.1.0/DEPRECATED.md +636 -0
- soar_lite-0.1.0/LICENSE +21 -0
- soar_lite-0.1.0/PKG-INFO +269 -0
- soar_lite-0.1.0/README.md +241 -0
- soar_lite-0.1.0/docs/README.md +14 -0
- soar_lite-0.1.0/docs/figures/README.md +92 -0
- soar_lite-0.1.0/docs/figures/headline-batches.png +0 -0
- soar_lite-0.1.0/docs/figures/headline-points.png +0 -0
- soar_lite-0.1.0/docs/figures/invariance-units.png +0 -0
- soar_lite-0.1.0/docs/figures/lr-sweep.png +0 -0
- soar_lite-0.1.0/docs/figures/multimodal-batches.png +0 -0
- soar_lite-0.1.0/docs/figures/multimodal-points.png +0 -0
- soar_lite-0.1.0/docs/figures/smooth-batches.png +0 -0
- soar_lite-0.1.0/docs/figures/smooth-points.png +0 -0
- soar_lite-0.1.0/docs/site/explanation/how-it-works.md +880 -0
- soar_lite-0.1.0/docs/site/hooks.py +23 -0
- soar_lite-0.1.0/docs/site/how-to/speed.md +85 -0
- soar_lite-0.1.0/docs/site/how-to/surrogate-restarts.md +170 -0
- soar_lite-0.1.0/docs/site/index.md +138 -0
- soar_lite-0.1.0/docs/site/reference/api.md +164 -0
- soar_lite-0.1.0/docs/site/tutorial/getting-started.md +370 -0
- soar_lite-0.1.0/pyproject.toml +111 -0
- soar_lite-0.1.0/soar_lite/__init__.py +77 -0
- soar_lite-0.1.0/soar_lite/domain.py +368 -0
- soar_lite-0.1.0/soar_lite/engine/__init__.py +64 -0
- soar_lite-0.1.0/soar_lite/engine/contracts.py +761 -0
- soar_lite-0.1.0/soar_lite/engine/schedule.py +1296 -0
- soar_lite-0.1.0/soar_lite/global_model/__init__.py +39 -0
- soar_lite-0.1.0/soar_lite/global_model/blr.py +507 -0
- soar_lite-0.1.0/soar_lite/global_model/descent.py +98 -0
- soar_lite-0.1.0/soar_lite/global_model/projection.py +266 -0
- soar_lite-0.1.0/soar_lite/global_model/sample.py +56 -0
- soar_lite-0.1.0/soar_lite/local_solver.py +366 -0
- soar_lite-0.1.0/soar_lite/py.typed +0 -0
- soar_lite-0.1.0/tests/__init__.py +1 -0
- soar_lite-0.1.0/tests/flock.py +59 -0
- soar_lite-0.1.0/tests/test_arm_roster.py +31 -0
- soar_lite-0.1.0/tests/test_docstring_references.py +88 -0
- soar_lite-0.1.0/tests/test_domain.py +160 -0
- soar_lite-0.1.0/tests/test_front_doors_agree.py +134 -0
- soar_lite-0.1.0/tests/test_global_model.py +57 -0
- soar_lite-0.1.0/tests/test_inner_minimize.py +72 -0
- soar_lite-0.1.0/tests/test_local_solver.py +664 -0
- soar_lite-0.1.0/tests/test_nonfinite_matrix.py +219 -0
- soar_lite-0.1.0/tests/test_paths.py +535 -0
- soar_lite-0.1.0/tests/test_plugin_seam.py +840 -0
- soar_lite-0.1.0/tests/test_pointwise.py +206 -0
- soar_lite-0.1.0/tests/test_scheduler.py +1448 -0
- soar_lite-0.1.0/tests/test_spherical_linear.py +589 -0
- soar_lite-0.1.0/tests/test_trace_budget_guard.py +134 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
build/
|
|
6
|
+
dist/
|
|
7
|
+
/site/
|
|
8
|
+
|
|
9
|
+
# Local scratch / working notes (never committed)
|
|
10
|
+
.scratch/
|
|
11
|
+
|
|
12
|
+
# Virtual environments. `.venv*/` also covers a side env built on another
|
|
13
|
+
# backend (e.g. `.venv-cuda/`). uv writes its own `*` .gitignore inside a venv,
|
|
14
|
+
# but that file is disposable, so the top-level rule is what we rely on.
|
|
15
|
+
.venv*/
|
|
16
|
+
venv/
|
|
17
|
+
|
|
18
|
+
# Tooling caches
|
|
19
|
+
.ruff_cache/
|
|
20
|
+
.pytest_cache/
|
|
21
|
+
.mypy_cache/
|
|
22
|
+
.pyright/
|
|
23
|
+
.mypy/
|
|
24
|
+
|
|
25
|
+
# Editors / OS
|
|
26
|
+
.DS_Store
|
|
27
|
+
*.swp
|
|
28
|
+
|
|
29
|
+
# Byte-compiled / native
|
|
30
|
+
*.so
|
|
31
|
+
|
|
32
|
+
# Jupyter Notebooks. The notebook is source; the checkpoints beside it are not.
|
|
33
|
+
.ipynb_checkpoints/
|
|
34
|
+
|
|
35
|
+
# Experiment run artifacts (regenerated, not source)
|
|
36
|
+
experiments/results/
|
|
37
|
+
experiments/figures/
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
User-visible changes, newest first. This file starts at the first public
|
|
4
|
+
release: what happened before it is in `git log`, and the internal record
|
|
5
|
+
(a rename sweep, a default flipped three times before anyone could have
|
|
6
|
+
depended on it) is not a thing a user upgrading between versions needs to read.
|
|
7
|
+
|
|
8
|
+
## 0.1.0
|
|
9
|
+
|
|
10
|
+
First public release. `README.md` and the site describe what this version is, so
|
|
11
|
+
this entry is the inventory rather than a diff against something earlier.
|
|
12
|
+
|
|
13
|
+
- `n_path_schedule` — the whole algorithm behind one call: `n_paths` descents
|
|
14
|
+
advance in lockstep under one budget, each re-drawn from the global model once
|
|
15
|
+
its own stopping test stalls it (`restart_stalled`, on by default), and the
|
|
16
|
+
report names the best point evaluated as `x`/`f` beside the flock the run
|
|
17
|
+
ended on.
|
|
18
|
+
- `DiagonalPreconditioner` — a per-coordinate inverse-curvature scale estimated
|
|
19
|
+
from successive gradients, plus a momentum `mu` (0 by default). No matrix and
|
|
20
|
+
no learning rate to tune, and once the first secant pair exists the step is
|
|
21
|
+
invariant to rescaling of the objective.
|
|
22
|
+
- Two plugin seams, `LocalSolver` and `GlobalModel` — `SampleGlobalModel` draws
|
|
23
|
+
restarts from the domain; `BLRGlobalModel` accumulates the run's evidence into
|
|
24
|
+
a Bayesian surrogate whose acquisition descent is injected.
|
|
25
|
+
- `pointwise` and a batched `eval_f_g` — value and gradient in one call, with an
|
|
26
|
+
optional feasibility mask, so one engine serves a scalar objective, a batched
|
|
27
|
+
one, or a penalized transform of a constrained problem.
|
|
28
|
+
|
|
29
|
+
Below `1.0.0` a breaking change bumps the minor version: `0.2.0` may not run code
|
|
30
|
+
written for `0.1.0`, and only `1.0.0` starts the compatibility promise.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: If soar-lite does useful work in something you publish, cite it with this entry.
|
|
3
|
+
title: soar-lite
|
|
4
|
+
abstract: >-
|
|
5
|
+
A JAX optimizer for objectives that are differentiable but expensive to
|
|
6
|
+
evaluate. A flock of n paths descends in parallel with a unit-invariant
|
|
7
|
+
diagonal secant preconditioner; a stalled path restarts from a Bayesian
|
|
8
|
+
linear surrogate fit on projected coordinates; and the report keeps every
|
|
9
|
+
served point, so an outside budget is spent against counted work.
|
|
10
|
+
authors:
|
|
11
|
+
- family-names: Tree
|
|
12
|
+
given-names: Watson
|
|
13
|
+
email: watson.tree@outlook.com
|
|
14
|
+
version: 0.1.0
|
|
15
|
+
license: MIT
|
|
16
|
+
type: software
|
|
17
|
+
repository-code: https://github.com/watson-tree/soar_lite
|
|
18
|
+
url: https://watson-tree.github.io/soar_lite/
|
|
19
|
+
keywords:
|
|
20
|
+
- optimization
|
|
21
|
+
- first-order
|
|
22
|
+
- quasi-newton
|
|
23
|
+
- multi-start
|
|
24
|
+
- nonconvex
|
|
25
|
+
- jax
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
# soar-lite
|
|
2
|
+
|
|
3
|
+
A first-order multi-path optimizer: value and gradient come back together from
|
|
4
|
+
the caller; `n_paths` independent descent paths advance in lockstep under one shared
|
|
5
|
+
budget, each re-drawn from a global model once its own stopping test stalls it
|
|
6
|
+
(the `restart_stalled` knob, on by default).
|
|
7
|
+
|
|
8
|
+
## Glossary
|
|
9
|
+
|
|
10
|
+
### Engine
|
|
11
|
+
|
|
12
|
+
**path**:
|
|
13
|
+
One independent descent: a current point plus that descent's own solver
|
|
14
|
+
state. The engine runs `n_paths` of them in lockstep. A restart replaces its
|
|
15
|
+
current point with a fresh draw, so the points one path holds across a run are
|
|
16
|
+
continuous in identity, not in space — which is why `best`, the reported answer,
|
|
17
|
+
need not be a point any path still holds.
|
|
18
|
+
_Avoid_: chain, trajectory, thread, particle
|
|
19
|
+
|
|
20
|
+
**n_paths**:
|
|
21
|
+
The engine's width — the number of paths the engine drives together.
|
|
22
|
+
_Avoid_: population size, swarm size
|
|
23
|
+
|
|
24
|
+
**flock**:
|
|
25
|
+
The paths' current points as one `(n_paths, d)` array: exactly one point per path,
|
|
26
|
+
so a flock is the run's state at one moment, not the set of paths. Every path
|
|
27
|
+
keeps its row whether or not it holds a usable point (`flock_f` is `nan` there),
|
|
28
|
+
so a flock always has `n_paths` of them. A *batch* is a set of points too and is
|
|
29
|
+
deliberately a different one: it is the work a single pass serves — a step's
|
|
30
|
+
trials, or the settle set of redraw starts plus winners — so it holds no point
|
|
31
|
+
for an idle path and is not the state. Iteration 0 places the flock from `x0`
|
|
32
|
+
where one was handed in, and otherwise from the global model; the engine's copy
|
|
33
|
+
is the flock as the run ended.
|
|
34
|
+
_Avoid_: the set of paths, population, ensemble, all the paths
|
|
35
|
+
|
|
36
|
+
**iteration**:
|
|
37
|
+
One pass of the engine's main loop, counted in `n_iters`. Per iteration the engine
|
|
38
|
+
splits the paths into a redraw set (no point yet, or stalled with
|
|
39
|
+
`restart_stalled` on) and a cohort that steps: a path either redraws or steps
|
|
40
|
+
this iteration, never both. The parallel wall-clock unit; an iteration serves
|
|
41
|
+
one or more batches (the steppers' trials as the method reads them, and the one
|
|
42
|
+
settle batch that buys whatever the iteration kept still owes). A flock in
|
|
43
|
+
which every path is parked — nothing left to do, since restarts are off — ends
|
|
44
|
+
the run on `all_stalled` instead of entering a batchless iteration.
|
|
45
|
+
Iteration 0 is the flock's placement: an empty `x` redraws unconditionally,
|
|
46
|
+
so starting the flock is the same operation as any later redraw. `n_iters`
|
|
47
|
+
counts finished iterations only — one cut short by a refused batch is not
|
|
48
|
+
counted, so a refused placement reports `n_iters = 0` and a refused first
|
|
49
|
+
step `n_iters = 1`.
|
|
50
|
+
_Avoid_: round (retired), epoch, iterations (as a name for the count)
|
|
51
|
+
|
|
52
|
+
**redraw set**:
|
|
53
|
+
The paths an iteration gives a fresh start: every path holding no point — always,
|
|
54
|
+
whatever `restart_stalled` says, which is how iteration 0 places the flock and
|
|
55
|
+
how a refused start eventually gets replaced — plus every stalled path when the
|
|
56
|
+
knob is on. Its size is what the engine asks the model for, one start per path,
|
|
57
|
+
so iteration 0 is the whole flock and a later one is only the paths that
|
|
58
|
+
stalled. Iteration 0 with an `x0` is the one redraw set whose starts the caller
|
|
59
|
+
supplied: `x0` is consumed there and no model is asked.
|
|
60
|
+
_Avoid_: restart list, restart batch
|
|
61
|
+
|
|
62
|
+
**cohort**:
|
|
63
|
+
The paths that step this iteration: neither in the redraw set nor stalled. One
|
|
64
|
+
`local_solver.propose` call is handed exactly this set, so a method never sees a
|
|
65
|
+
path the engine has stopped — not even a step that would answer `None`. It is
|
|
66
|
+
not the flock, and not just the redraw set's complement: a path parked by
|
|
67
|
+
`restart_stalled=False` belongs to neither, and sits out while the rest moves.
|
|
68
|
+
_Avoid_: the flock, active paths
|
|
69
|
+
|
|
70
|
+
**best**:
|
|
71
|
+
The run's running best, a single `f`/`x`: the best *feasible* point — the
|
|
72
|
+
run's reported answer, and what `NPathResult` calls `f`/`x`. The engine is the
|
|
73
|
+
sole owner. It starts at `nan` and stays there while no finite feasible value
|
|
74
|
+
has arrived, so `nan` means "no answer" and `inf` is never a sentinel - it is
|
|
75
|
+
left to mean a value an objective actually took. A *feasible* `-inf` is not
|
|
76
|
+
weighed but taken: the objective has no floor there, so it becomes `f`/`x` and
|
|
77
|
+
ends the run with `status` `unbounded`; a `nan`, a `+inf` or an infeasible
|
|
78
|
+
`-inf` changes nothing.
|
|
79
|
+
_Avoid_: incumbent (retired), best-so-far, current optimum
|
|
80
|
+
|
|
81
|
+
**stall / stalled**:
|
|
82
|
+
A path that moved nowhere this iteration — it neither redrew nor committed a
|
|
83
|
+
step: its local method answered no winner (`None` — its own stopping test
|
|
84
|
+
fired), or the point it was given was refused as not usable, whether the winner
|
|
85
|
+
the method named or the start the model drew: a non-finite value or gradient in
|
|
86
|
+
either.
|
|
87
|
+
Stalled is not dead: unless the run turned `restart_stalled` off, the path is
|
|
88
|
+
re-drawn by the global model on the next iteration; with the knob off it stays
|
|
89
|
+
parked — still counted, still readable for `best` — but never moved again. A
|
|
90
|
+
turn of the loop with every path parked that way is `all_stalled` — reachable
|
|
91
|
+
only when `restart_stalled` is off, since with it on a stalled path is re-drawn
|
|
92
|
+
rather than parked: no path can move and none is owed a start, so the run stops.
|
|
93
|
+
A path with no point at all is a redraw that ignores `restart_stalled`: the
|
|
94
|
+
machine's own initial state is not a stall. A start refused while the path held
|
|
95
|
+
nothing sets the bit anyway, but the no-point rule is asked first, so the flag
|
|
96
|
+
never sees that case.
|
|
97
|
+
_Avoid_: paused, idle, dead
|
|
98
|
+
|
|
99
|
+
**domain** (type `Domain`):
|
|
100
|
+
The object that carries everything the engine knows about the search space: the
|
|
101
|
+
per-dimension `(lb, ub)` sides (either may be unbounded), the per-dimension
|
|
102
|
+
`scale` the draws use, and the injected per-dimension samplers. It is the sole
|
|
103
|
+
authority on the dimension `d`, and the box every point the run serves must lie
|
|
104
|
+
inside: the engine checks each point a seam hands over and refuses one that
|
|
105
|
+
misses, instead of moving it onto a wall. Bounds are data, never a unit-cube
|
|
106
|
+
assumption.
|
|
107
|
+
_Avoid_: box (the box is only the bounded-sides part of the domain)
|
|
108
|
+
|
|
109
|
+
**sampler**:
|
|
110
|
+
The injected per-dimension callable that draws starts; the default is uniform on
|
|
111
|
+
a bounded side, `N(0, scale²)` on a free one, and that same normal truncated to
|
|
112
|
+
the declared side on a half-bounded one. The *domain* remains the sole
|
|
113
|
+
authority on `d`.
|
|
114
|
+
_Avoid_: initializer, prior (as a name for the *callable*)
|
|
115
|
+
|
|
116
|
+
### Evaluation
|
|
117
|
+
|
|
118
|
+
**batch**:
|
|
119
|
+
The `(M, d)` points served by one pass — fused (`eval_f_g`) or value-only
|
|
120
|
+
(`eval_f`). The engine composes each batch from either one trial round — one
|
|
121
|
+
point per path the round names — or the settle set: the redraw starts and
|
|
122
|
+
the winners still owing a half.
|
|
123
|
+
_Avoid_: group, lot, chunk
|
|
124
|
+
|
|
125
|
+
**point**:
|
|
126
|
+
One candidate — one row of a batch. The four counters (below) count the billed
|
|
127
|
+
ones. Once settled it is a `PathPoint`: the `x`/`f`/`g` triple a path commits,
|
|
128
|
+
assembled only once both halves exist (see *settle*), so a point that arrived
|
|
129
|
+
fused travels on as one object rather than three arrays.
|
|
130
|
+
_Avoid_: query, sample, evaluation (retired)
|
|
131
|
+
|
|
132
|
+
**settle**:
|
|
133
|
+
The iteration's one fused batch that buys whatever a kept point still owes, and
|
|
134
|
+
the only place a produced `Point` becomes a settled `PathPoint`. A redraw start
|
|
135
|
+
and a winner the method never read owe both halves and take f and g from this
|
|
136
|
+
one pass; a winner the method read through `serve_trials` owes only its gradient
|
|
137
|
+
when the `eval_f` seam served it value-only — with no such seam the same call
|
|
138
|
+
served the fused pair, so the winner already carries both and passes through as
|
|
139
|
+
it stands, never served twice. Everything is bought before anything is placed,
|
|
140
|
+
so nothing commits before it is settled.
|
|
141
|
+
_Avoid_: commit batch, gradient round
|
|
142
|
+
|
|
143
|
+
**four counters** (`n_f_g_points` / `n_f_g_batches` / `n_f_points` / `n_f_batches`, all four in one `Work`):
|
|
144
|
+
The run's reported work, on two axes and two seams. `points` counts billed
|
|
145
|
+
points (a batch bills its actual size), `batches` counts billed batches (a batch
|
|
146
|
+
bills 1); `f_g` is the fused value+gradient pair, `f` the value-only pair. All
|
|
147
|
+
four are counted — "billed" means "actually served out" — and none of them
|
|
148
|
+
decides the stop. Points and batches are different numbers on different axes
|
|
149
|
+
and must never share a chart, and the `f` pair must never be read as the `f_g`
|
|
150
|
+
pair. They replace scipy's single `nfev`. The four live in one frozen `Work`,
|
|
151
|
+
reached as `result.work` and read flat on the result as well; that object is
|
|
152
|
+
what the budget gate is handed — as it stands at the loop head, and as a
|
|
153
|
+
pending batch would leave it at a serve site — so a gate reads counts on their
|
|
154
|
+
way into the report and cannot write them.
|
|
155
|
+
_Avoid_: nfev (retired)
|
|
156
|
+
|
|
157
|
+
**objective**:
|
|
158
|
+
The scalar function being minimized; its per-point values are the f values the
|
|
159
|
+
engine records. Distinct from the *cost model* (the four counters), which
|
|
160
|
+
counts work.
|
|
161
|
+
_Avoid_: loss, cost, fitness
|
|
162
|
+
|
|
163
|
+
**eval_f_g**:
|
|
164
|
+
The caller's per-batch callable (type `EvalFG`): points in,
|
|
165
|
+
`(f, gradient, feas?)` out. The seam to the objective's values and
|
|
166
|
+
gradients; the engine is blind to it otherwise.
|
|
167
|
+
_Avoid_: oracle, evaluator, black box
|
|
168
|
+
|
|
169
|
+
**eval_f**:
|
|
170
|
+
The optional value-only sibling (type `EvalF`): points in, `(f, feas?)` out,
|
|
171
|
+
no gradient.
|
|
172
|
+
It exists so a step's trials are read by value alone and only the winner's
|
|
173
|
+
gradient is bought once at *settle*, instead of once per trial. Without it
|
|
174
|
+
every trial is itself a fused batch.
|
|
175
|
+
_Avoid_: value oracle, cheap evaluator
|
|
176
|
+
|
|
177
|
+
**feasibility mask**:
|
|
178
|
+
The optional per-point boolean saying which served points may count toward the
|
|
179
|
+
reported best (`f`/`x`). It is
|
|
180
|
+
read as 0/1 and nothing else: only `0` (or `False`) marks a point infeasible, so
|
|
181
|
+
a mask holding anything else — a signed constraint margin, whose violated points
|
|
182
|
+
are all negative, or a `nan` — is refused with a `ValueError` instead of being
|
|
183
|
+
read as truthiness, where every one of those flags would have counted as
|
|
184
|
+
feasible and quietly promoted a point into the answer.
|
|
185
|
+
_Avoid_: validity mask
|
|
186
|
+
|
|
187
|
+
### Local method
|
|
188
|
+
|
|
189
|
+
**step**:
|
|
190
|
+
One move along a single path: propose → (trial)× → a winner, or none (`None`).
|
|
191
|
+
Each path takes at most one move per iteration — a step, or a redraw start; a
|
|
192
|
+
step consumes zero or more trials. The method lands it through `step(points)`: the
|
|
193
|
+
engine hands back the settled `PathPoint` `(x, f, g)` the path now stands at, so
|
|
194
|
+
the method moves the matching state forward. The **method** named the winner in
|
|
195
|
+
`propose`; the engine bought its owed halves in *settle* and commits here, so
|
|
196
|
+
`step` is the only commit and the only place a method may advance its model — a
|
|
197
|
+
secant pair is a move that was taken, and only this callback is told one was. It
|
|
198
|
+
is never called for a winner the engine refused (not usable) or for a path whose
|
|
199
|
+
stopping test held.
|
|
200
|
+
_Avoid_: update, move; accept, commit (as names for this handoff)
|
|
201
|
+
|
|
202
|
+
**round**:
|
|
203
|
+
One `serve_trials` call: one point per path it names, served and reported as
|
|
204
|
+
one batch. It is the unit of both price and memory — a method that enumerates
|
|
205
|
+
reads its candidates as successive rounds, so however far it looks it holds one
|
|
206
|
+
point per path at a time and pays one batch per look. The method sets how many
|
|
207
|
+
rounds a step reads; the engine sets the width of each.
|
|
208
|
+
_Avoid_: trial (that is one point inside a round)
|
|
209
|
+
|
|
210
|
+
**trial**:
|
|
211
|
+
One served proposal within a step — this path's point on one round the method
|
|
212
|
+
reads through `serve_trials`. What a trial carries is the seam's business: with
|
|
213
|
+
`eval_f` connected it is the coords and value alone — the winner's gradient is
|
|
214
|
+
bought later (see *settle*), so a method that needs a gradient to rule needs the
|
|
215
|
+
fused seam — and without `eval_f` the same round serves the fused pair, gradient
|
|
216
|
+
included. There is no engine-side cap: the method's own stopping test (or its
|
|
217
|
+
own trial budget) ends the enumeration, one batch per round. A step that reads no
|
|
218
|
+
trial — a single full proposal, like the shipped method's — serves none at all.
|
|
219
|
+
_Avoid_: line-search round (retired)
|
|
220
|
+
|
|
221
|
+
**secant pair**:
|
|
222
|
+
The `(Δx, Δg)` between two consecutive points of one path — the only curvature
|
|
223
|
+
evidence the local method reads.
|
|
224
|
+
_Avoid_: gradient difference, step pair
|
|
225
|
+
|
|
226
|
+
**diagonal preconditioner**:
|
|
227
|
+
The local method itself: a per-coordinate inverse-curvature estimate read off
|
|
228
|
+
the secant pair, applied as `-precon ⊙ m` where `m` is the momentum (a blend of
|
|
229
|
+
the gradient and its history; the bare gradient `g` at `mu = 0`). No matrix, no
|
|
230
|
+
linear solve, no scalar learning rate.
|
|
231
|
+
_Avoid_: diagonal Newton (it is a biased `1/H` estimate, not Newton)
|
|
232
|
+
|
|
233
|
+
**floor (λ)**:
|
|
234
|
+
The guard that keeps a coordinate's denominator positive when the secant
|
|
235
|
+
vanishes (a flat but unconverged coordinate).
|
|
236
|
+
_Avoid_: epsilon, addend, regularizer
|
|
237
|
+
|
|
238
|
+
**warmup**:
|
|
239
|
+
A path's first step, before any secant pair exists: one full proposal along
|
|
240
|
+
`-g₀` at `alpha0`, committed as proposed. It carries no value guard of its own —
|
|
241
|
+
the phase enters only the direction; like the steady step, it is read by no
|
|
242
|
+
value and compared against no start.
|
|
243
|
+
_Avoid_: initialization step
|
|
244
|
+
|
|
245
|
+
### Restart
|
|
246
|
+
|
|
247
|
+
**global model** (type `GlobalModel`):
|
|
248
|
+
The engine's second seam, owner of *where* a redrawn path starts. It observes
|
|
249
|
+
every served batch as a mapping from path to the point served there
|
|
250
|
+
(`observe(served)`, one entry per path, so a model that only wants stacked rows
|
|
251
|
+
stacks them itself) and answers `propose(paths, domain)`
|
|
252
|
+
with one fresh start per path it was asked about, keyed by that path. `None` for
|
|
253
|
+
a path is the model declining it — its own stopping rule, as `None` from a
|
|
254
|
+
method's `propose` is the method's — and that path starts nowhere this
|
|
255
|
+
iteration. The default `SampleGlobalModel` fits nothing: it draws starts
|
|
256
|
+
from the `domain`. An `x0` the caller hands the entry states the first flock
|
|
257
|
+
directly and the model is not asked; with `x0=None` it draws that flock too, as
|
|
258
|
+
iteration 0's one ask. `BLRGlobalModel`
|
|
259
|
+
fits a Bayesian linear posterior and proposes the local minimum of `-log EI` —
|
|
260
|
+
EI there is an acquisition function deciding *where is worth trying*, not a
|
|
261
|
+
restart decision.
|
|
262
|
+
_Avoid_: restart rule, sampler (the sampler is a per-dimension draw inside the
|
|
263
|
+
domain)
|
|
264
|
+
|
|
265
|
+
**inner minimizer** (`minimize`, type `MinimizeFn`):
|
|
266
|
+
The global model's own seam, one level further in, owning *how* to descend the
|
|
267
|
+
acquisition the model builds (`-log EI` for `BLRGlobalModel`). The model holds
|
|
268
|
+
the acquisition and never searches it itself: `propose` hands the acquisition,
|
|
269
|
+
the ask's `domain`, the paths and a fresh subkey to `minimize`, and carries its
|
|
270
|
+
answer out as one start per path, `None` for a path it could not place. The
|
|
271
|
+
shipped default is `descent_minimize`, the engine run restart-free at the
|
|
272
|
+
cohort's width, so the acquisition is descended by the method it will later
|
|
273
|
+
drive; a caller that needs an inner knob the default does not expose — e.g. a
|
|
274
|
+
`time_limit`, another local solver, `budget_check`, restarts inside — passes its
|
|
275
|
+
own `MinimizeFn` instead.
|
|
276
|
+
_Avoid_: inner loop, acquisition optimizer (the model does not optimize the
|
|
277
|
+
acquisition, it asks for one)
|
|
278
|
+
|
|
279
|
+
**restart_stalled**:
|
|
280
|
+
The run's one restart bit, deciding *whether* a stall restarts: on (the
|
|
281
|
+
default), the stalled path is re-drawn by the global model next iteration; off,
|
|
282
|
+
it stays parked, and a flock that is all parked is a deadlock — the run ends on
|
|
283
|
+
`all_stalled` rather than idling to its horizon. It is orthogonal to *when* (the
|
|
284
|
+
local method's own stopping test) and *where* (the global model). A run that
|
|
285
|
+
never restarts needs no model of its own:
|
|
286
|
+
`restart_stalled=False` with the default `SampleGlobalModel` is pure descent
|
|
287
|
+
from the initial flock — the model runs once, drawing that flock, when `x0` is
|
|
288
|
+
left `None`, and never again.
|
|
289
|
+
_Avoid_: restart check, reset decision
|
|
290
|
+
|
|
291
|
+
### Cost model
|
|
292
|
+
|
|
293
|
+
**horizon (`max_iters`)**:
|
|
294
|
+
The engine's own stop, counted in iterations: every completed iteration costs
|
|
295
|
+
one, and iteration 0 places the flock. A deterministic bound, not a cost
|
|
296
|
+
proxy — one iteration serves however many trial rounds the method's own search
|
|
297
|
+
reads, plus the one settle batch that buys what it kept still owing, while a
|
|
298
|
+
flock with nothing left to do stops the run
|
|
299
|
+
(`all_stalled`) instead of billing a batchless iteration — so the four counters
|
|
300
|
+
report what a run actually billed. At least one of `max_iters`/`time_limit` must
|
|
301
|
+
be given, validated once at entry: `stop` asks the caller's gate like the other
|
|
302
|
+
two, but a gate is the caller's own predicate and the engine cannot see it
|
|
303
|
+
advance, so it cannot tell a gate that will close from one that never does — a
|
|
304
|
+
run may not rest on that alone.
|
|
305
|
+
_Avoid_: eval budget, cost estimate, time limit
|
|
306
|
+
|
|
307
|
+
**deadline (`time_limit`)**:
|
|
308
|
+
The same stop in seconds, for callers whose per-iteration cost is too uneven to
|
|
309
|
+
count iterations. Checked before every batch (fused or value-only) and at the
|
|
310
|
+
loop head, like the horizon, so the over-run is under one batch plus the one
|
|
311
|
+
call in flight.
|
|
312
|
+
_Avoid_: budget, timeout (that is a per-call concern)
|
|
313
|
+
|
|
314
|
+
**budget gate** (`budget_check`):
|
|
315
|
+
The caller's predicate, asked as one of the run's three stops at every loop head
|
|
316
|
+
and again before each batch — serve the whole batch or nothing, never a partial
|
|
317
|
+
prefix. It is handed the run's `Work`, the same four counts the result reports,
|
|
318
|
+
so it gates on any axis it likes (points, batches, both seams together) without
|
|
319
|
+
keeping a tally of its own. Before a batch it is handed the ledger that batch
|
|
320
|
+
would leave, and a batch it refuses is never charged: a ceiling on points is one
|
|
321
|
+
the run cannot cross, met from below by up to one batch width since a batch is
|
|
322
|
+
served whole or not at all. At the loop head it is handed the ledger as it
|
|
323
|
+
stands. It exists for a budget the engine cannot see (points, wall-clock,
|
|
324
|
+
dollars). It is an extra gate, never a horizon.
|
|
325
|
+
_Avoid_: stopping budget
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Welcome. Whether you have a question, a bug report or a change in mind, feel
|
|
4
|
+
free to open an issue or a pull request — the sections below say what a change
|
|
5
|
+
has to clear.
|
|
6
|
+
|
|
7
|
+
## Proposing a change
|
|
8
|
+
|
|
9
|
+
Open a pull request; GitHub runs the checks below on it. An issue is the right
|
|
10
|
+
place for a question, a bug report, or an idea that has no implementation yet.
|
|
11
|
+
|
|
12
|
+
## The checks a change has to pass
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
uv sync --all-groups # jax, the dev tools, and the docs build
|
|
16
|
+
uv run python -m pytest -q # all tests in one go
|
|
17
|
+
uv run --no-sync python -m tests.test_local_solver # or one module at a time,
|
|
18
|
+
uv run --no-sync python -m tests.test_global_model # each module also runs
|
|
19
|
+
uv run --no-sync python -m tests.test_scheduler # as a plain script
|
|
20
|
+
uv run --no-sync python -m tests.test_plugin_seam
|
|
21
|
+
uv run ruff check && uv run ruff format --check && uv run pyright
|
|
22
|
+
uv run mkdocs build --strict # the docs site; a warning fails the build
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The tests are assert-style self-checks: plain `assert` in `test_*` functions,
|
|
26
|
+
pytest-collectable, no recorded golden numbers that would encode this
|
|
27
|
+
repository's history. The four `python -m` modules above double as plain
|
|
28
|
+
scripts; the rest, like everything under `experiments/tests`, are pytest-only
|
|
29
|
+
and some use fixtures.
|
|
30
|
+
|
|
31
|
+
## Commits and releases
|
|
32
|
+
|
|
33
|
+
A commit subject names the kind of change before it describes it —
|
|
34
|
+
`type(scope): what changed` — so `engine: settle only the points that still owe
|
|
35
|
+
a gradient` becomes `fix(engine): settle only the points that still owe a
|
|
36
|
+
gradient`. The types are `feat`, `fix`, `docs`, `test`, `refactor`, `perf`,
|
|
37
|
+
`build`, `ci` and `chore`; the scope is optional — the module or the area the
|
|
38
|
+
change lands in, added as `fix(engine): …` when the type alone does not say
|
|
39
|
+
where. The rule starts with this release: earlier subjects are a mix of untyped
|
|
40
|
+
`area: …` and occasional typed ones, and they stay as written, so read them as
|
|
41
|
+
prose and type every subject you author from here on. The type earns its place
|
|
42
|
+
because a user is the intended reader of `CHANGELOG.md`: `docs`, `test`, `ci`,
|
|
43
|
+
`build` and `chore` never reach them, and that is the set a changelog generated
|
|
44
|
+
from the log skips.
|
|
45
|
+
|
|
46
|
+
Versions are SemVer read under its `0.x` rule: below `1.0.0`, any change a caller
|
|
47
|
+
can notice — including one that breaks their code — bumps the minor version, and
|
|
48
|
+
a patch is only for a fix that leaves the API as it was. `1.0.0` is where the
|
|
49
|
+
compatibility promise starts.
|
|
50
|
+
|
|
51
|
+
A release is four steps, in this order:
|
|
52
|
+
|
|
53
|
+
1. Put the version in every place it is written: `version` in `pyproject.toml`,
|
|
54
|
+
`version` in `CITATION.cff`, the `## ` heading of a new top entry in
|
|
55
|
+
`CHANGELOG.md`, and a `uv lock` so `uv.lock` re-syncs it. The heading
|
|
56
|
+
carries no date; the tag is the date.
|
|
57
|
+
2. Land it behind the checks above.
|
|
58
|
+
3. Tag that commit on `main` `v<version>` and push the tag. Publishing is a
|
|
59
|
+
tag's business: nothing ships from a branch or a pull request, and the tag
|
|
60
|
+
must start with `v` — a tag like `0.1.0` matches no workflow and ships
|
|
61
|
+
nothing.
|
|
62
|
+
4. Watch `publish.yml` run the tests, build and upload.
|
|
63
|
+
|
|
64
|
+
PyPI never lets a version number be used twice — a published version can be
|
|
65
|
+
yanked but not replaced, so an upload is the irreversible step. That is what the
|
|
66
|
+
release-tag check in `publish.yml` stands in front of: a tag that does not name
|
|
67
|
+
the version in `pyproject.toml` fails the build before anything is uploaded, and
|
|
68
|
+
the tag can be deleted and pushed again.
|
|
69
|
+
|
|
70
|
+
## Front doors
|
|
71
|
+
|
|
72
|
+
`docs/site/index.md` is the canonical home page and the root `README.md` mirrors it
|
|
73
|
+
for the sections both carry. A fact in the opening, or in any section either one
|
|
74
|
+
carries from the other — `Install`, `Minimal run`, `What's inside`, `Citing` — is
|
|
75
|
+
corrected in `docs/site/index.md` and copied across;
|
|
76
|
+
`tests/test_front_doors_agree.py` fails if the two drift apart, or if a link in
|
|
77
|
+
either one stops resolving. Link destinations are the sole exception — the site
|
|
78
|
+
addresses its neighbours from inside `docs/`, the README from the root.
|
|
79
|
+
|
|
80
|
+
## Names
|
|
81
|
+
|
|
82
|
+
Names have fixed meanings: `CONTEXT.md` is the glossary, including the terms to
|
|
83
|
+
avoid. Consult it before introducing one.
|
|
84
|
+
|
|
85
|
+
## Records are append-only
|
|
86
|
+
|
|
87
|
+
`DEPRECATED.md`, `experiments/DEPRECATED.md`, `experiments/RESULTS.md` and
|
|
88
|
+
`CHANGELOG.md` are records, not living documents: nothing in them is deleted or
|
|
89
|
+
rewritten to say something else, and a measured number is never re-edited — a
|
|
90
|
+
new measurement or a superseded direction is a new entry, and the old one stays
|
|
91
|
+
as written. `experiments/DESIGN.md` is the opposite: a current-state document,
|
|
92
|
+
so a passage that is no longer true there is rewritten in place.
|
|
93
|
+
|
|
94
|
+
## The benchmark suite
|
|
95
|
+
|
|
96
|
+
`experiments/` holds the comparison harness behind the figures in the README. It
|
|
97
|
+
is a sub-library, not a dependency: it imports `soar_lite`, nothing in the
|
|
98
|
+
package or the docs imports it, and neither the wheel nor the sdist ships it. Run
|
|
99
|
+
instructions are in `experiments/README.md`, what it measures and the rules it
|
|
100
|
+
follows in `experiments/DESIGN.md`, and the numbers in `experiments/RESULTS.md`.
|
|
101
|
+
|
|
102
|
+
## Open directions
|
|
103
|
+
|
|
104
|
+
The two plugin seams (`LocalSolver`, `GlobalModel`) are the natural extension
|
|
105
|
+
points. Writing your own is currently documented only as the contracts in
|
|
106
|
+
`docs/site/reference/api.md` and the `Protocol`s in `soar_lite/engine/contracts.py`,
|
|
107
|
+
not yet as a how-to guide. `BLRGlobalModel`'s acquisition has a known upgrade
|
|
108
|
+
(see the `shortcut:` note in `soar_lite/global_model/blr.py`), and more global
|
|
109
|
+
models or local solvers (e.g. line search) are welcome — `experiments/RESULTS.md`
|
|
110
|
+
records where the tuned baselines stand.
|