Skip to content

Commit 860376a

Browse files
Merge pull request #300 from haraldschilly/claude/loop-d5-slice
Let the nightly loop see d5: opt-in --aocc-extra-dims
2 parents 4ebb55f + 4d15c59 commit 860376a

7 files changed

Lines changed: 325 additions & 5 deletions

File tree

.github/workflows/self_improve_nightly.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,20 @@ jobs:
293293
--sync-eval
294294
--eps-accept 0.0125
295295
--inactivity-min-eps-accept 0.006
296+
# 2026-08-11 — widen the quick preset's dims=(2,) to
297+
# (2, 5). The loop's two sharpest measured results of
298+
# 2026-08 both lived at d5 and were invisible here: the
299+
# JSO d5 add (08-02) and the NLSHADE_LBC per-dim split
300+
# (08-11, d2 -0.0241 vs d5 +0.0080, both CIs excluding
301+
# zero). Measured cost on this battery: 8.9s -> 11.9s
302+
# (1.34x), so the nightly goes ~5.4 -> ~7.2 min against a
303+
# 90-minute timeout.
304+
#
305+
# NOTE this moves the ledger's score LEVEL, not just its
306+
# noise: d2 alone reads ~0.369, (d2, d5) reads ~0.309.
307+
# Nights before and after are not on one scale — the
308+
# per-iteration 'aocc_extra_dims' field records which.
309+
--aocc-extra-dims 5
296310
)
297311
fi
298312
"${CMD[@]}"

TODO.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@
22

33
## Recent Improvements (continued)
44

5+
### Nightly loop can see d5 — 2026-08-11 (third session)
6+
- [x] **`--aocc-extra-dims 5` in the nightly** — the quick preset's
7+
`dims=(2,)` becomes `(2, 5)` for every measurement leg. Closes
8+
the blind spot behind the JSO d5 add (08-02) and the NLSHADE_LBC
9+
per-dim split (08-11). Cost 8.9s → 11.9s (1.34×).
10+
- [x] **`with_extra_dims` composes, never edits** the frozen presets;
11+
name gains a `+d5` suffix.
12+
- [ ] **The ledger score level shifts** with this ship (d2 ~0.369 →
13+
(d2,d5) ~0.309). `aocc_extra_dims` is recorded per iteration;
14+
codify-scan and `summary` should group by it before pooling.
15+
- [ ] **Re-measure the CMA-ES arm (GOAL §5.2) at d5** with the 12-seed
16+
standard instrument — if it splits like NLSHADE_LBC did, one
17+
dimension-gating mechanism ships both arms.
18+
519
### Loop measurement fidelity: hold-out metric bug, seed rotation, sync-eval — 2026-08-11 (second session)
620
- [x] **Hold-out leg measured `composite_score` on AOCC runs** — the
721
8.5× "instance-family generalization gap" (`GOAL.md` §2 / §5.1's

panobbgo/harness_ioh.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
import hashlib
6262
import json
6363
import time
64-
from dataclasses import asdict, dataclass, field
64+
from dataclasses import asdict, dataclass, field, replace
6565
from typing import Any, Dict, List, Optional, Sequence, Tuple
6666

6767
import numpy as np
@@ -279,6 +279,39 @@ def make_quick_battery() -> IOHBatterySpec:
279279
)
280280

281281

282+
def with_extra_dims(battery: IOHBatterySpec, extra_dims: Sequence[int]) -> IOHBatterySpec:
283+
"""Return ``battery`` widened with ``extra_dims``, preserving everything else.
284+
285+
The battery presets are frozen contracts (``planning/GOAL.md`` §4:
286+
"extend via opt-in flags, never edit"), so a caller that wants a
287+
regime the preset cannot reach composes one instead of editing the
288+
factory. Dims already present are ignored and the result is sorted,
289+
so the call is idempotent and order-insensitive.
290+
291+
The name gains a ``+d<k>`` suffix per added dim so a report or
292+
ledger record cannot silently conflate a widened battery with the
293+
preset it came from — the two measure different things and their
294+
mean AOCC is not comparable.
295+
296+
This exists because the nightly loop runs the quick battery, which
297+
is ``dims=(2,)``. Two of the sharpest measured results of 2026-08
298+
(the JSO d5 add on 2026-08-02 and the NLSHADE_LBC per-dim split on
299+
2026-08-11, where d2 lost 0.0241 while d5 gained 0.0080) lived
300+
entirely at d5 — invisible to the regime the loop actually samples.
301+
302+
Note the budget interaction: ``budget_for`` is
303+
``budget_multiplier * dim``, so adding dim 5 to the quick battery
304+
(multiplier 100) buys 500-eval runs alongside the 200-eval ones.
305+
The added dim costs more per run than the ones already there.
306+
"""
307+
merged = tuple(sorted(set(battery.dims) | {int(d) for d in extra_dims}))
308+
if merged == battery.dims:
309+
return battery
310+
added = [d for d in merged if d not in battery.dims]
311+
suffix = "".join(f"+d{d}" for d in added)
312+
return replace(battery, name=f"{battery.name}{suffix}", dims=merged)
313+
314+
282315
def make_standard_battery() -> IOHBatterySpec:
283316
"""Mid-sized battery: a meaningful AOCC estimate without overnight runs."""
284317
return IOHBatterySpec(

panobbgo/self_improve.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3198,6 +3198,30 @@ class LoopConfig:
31983198
#: against a non-sync-eval one** — the noise floors differ, so a
31993199
#: mixed-mode A/B reads the mode change as a spec effect.
32003200
sync_eval: bool = False
3201+
#: Extra dimensions appended to the AOCC battery for *every*
3202+
#: measurement the loop makes — screening, confirm, guard, hold-out.
3203+
#:
3204+
#: The mode presets are frozen contracts, so this composes a widened
3205+
#: battery via :func:`~panobbgo.harness_ioh.with_extra_dims` instead
3206+
#: of editing them. ``(5,)`` on the quick preset turns the nightly's
3207+
#: ``dims=(2,)`` regime into ``dims=(2, 5)``.
3208+
#:
3209+
#: Motivation: the loop samples quick-2-D, but the two sharpest
3210+
#: measured results of 2026-08 both lived at d5 — the JSO d5 add
3211+
#: (2026-08-02) and the NLSHADE_LBC per-dim split (2026-08-11, d2
3212+
#: −0.0241 against d5 +0.0080, both CIs excluding zero). A regime
3213+
#: the loop cannot see is a regime it cannot optimise, and worse, a
3214+
#: change that helps there reads as noise or as a loss in the
3215+
#: aggregate.
3216+
#:
3217+
#: Cost scales super-linearly: ``budget_for`` is
3218+
#: ``budget_multiplier * dim``, so adding dim 5 to the quick battery
3219+
#: doubles the run count *and* the added runs are 2.5x longer.
3220+
#:
3221+
#: Empty (default) leaves every existing invocation byte-identical.
3222+
#: Inert under ``metric="composite"``, which reaches higher dims
3223+
#: through ``--extra-highdim`` / :attr:`extra_families` instead.
3224+
aocc_extra_dims: Tuple[int, ...] = ()
32013225

32023226
def __post_init__(self) -> None:
32033227
if self.iterations < 0:
@@ -3475,6 +3499,17 @@ class LoopIterationRecord:
34753499
#: pooled CI narrower than either mode justifies. Consumers that
34763500
#: aggregate across nights should group by this field.
34773501
sync_eval: bool = False
3502+
#: Dimensions appended to the mode's AOCC battery for this iteration
3503+
#: (:attr:`LoopConfig.aocc_extra_dims`). Empty on legacy records and
3504+
#: on composite runs.
3505+
#:
3506+
#: Recorded for the same reason as :attr:`sync_eval`: widening the
3507+
#: battery moves the *level* of the score, not just its noise —
3508+
#: measured on the quick preset, d2 alone reads 0.3685 while
3509+
#: (d2, d5) reads ~0.31 — so a night before the widening and a night
3510+
#: after are not on one scale. Cross-night consumers must group by
3511+
#: this field as well.
3512+
aocc_extra_dims: Tuple[int, ...] = ()
34783513

34793514
def to_dict(self) -> Dict[str, Any]:
34803515
d: Dict[str, Any] = {
@@ -3502,6 +3537,7 @@ def to_dict(self) -> Dict[str, Any]:
35023537
"bandit_reward": self.bandit_reward,
35033538
"confirmed": self.confirmed,
35043539
"sync_eval": self.sync_eval,
3540+
"aocc_extra_dims": list(self.aocc_extra_dims),
35053541
}
35063542
return d
35073543

@@ -4502,6 +4538,7 @@ def _run_internal(
45024538
bandit_reward=bandit_reward,
45034539
confirmed=confirmed_flag,
45044540
sync_eval=bool(self.config.sync_eval),
4541+
aocc_extra_dims=tuple(self.config.aocc_extra_dims),
45054542
)
45064543
records.append(rec)
45074544
ledger.write(rec)
@@ -4665,6 +4702,7 @@ def _measure_aocc(
46654702
make_quick_battery,
46664703
make_standard_battery,
46674704
run_ioh_harness,
4705+
with_extra_dims,
46684706
)
46694707

46704708
battery_factories = {
@@ -4673,6 +4711,12 @@ def _measure_aocc(
46734711
"full": make_full_battery,
46744712
}
46754713
battery = battery_factories[self.config.mode]()
4714+
if self.config.aocc_extra_dims:
4715+
# Widen *every* measurement identically — screening,
4716+
# confirm, guard and hold-out all route through here, so
4717+
# the loop can never compare a 2-D baseline against a
4718+
# (2, 5)-D candidate.
4719+
battery = with_extra_dims(battery, self.config.aocc_extra_dims)
46764720
if verbose:
46774721
print(f"[self_improve] iter={iteration} measuring {label} (AOCC, battery={battery.name})")
46784722
# Mix the iteration into the base seed so each iteration draws
@@ -4718,6 +4762,7 @@ def _skip_record(
47184762
effective_eps_accept=effective_eps_accept,
47194763
iters_since_accept=iters_since_accept,
47204764
sync_eval=bool(self.config.sync_eval),
4765+
aocc_extra_dims=tuple(self.config.aocc_extra_dims),
47214766
)
47224767

47234768
def _stop_requested(self) -> bool:

planning/SELF_IMPROVEMENT_LOG.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,54 @@ Conventions:
1717
* Graduate items from "Next iteration ideas" to a dated entry when
1818
shipped.
1919

20+
### 2026-08-11 (third session) — the nightly loop can finally see d5
21+
22+
* **What** — an opt-in `--aocc-extra-dims` widens the AOCC battery for
23+
*every* measurement the loop makes (screening, confirm, guard,
24+
hold-out), and the nightly turns it on with `5`. The quick preset's
25+
`dims=(2,)` becomes `(2, 5)`. Mode presets stay frozen per GOAL §4 —
26+
the widened battery is *composed* by
27+
:func:`~panobbgo.harness_ioh.with_extra_dims`, which appends, sorts,
28+
and suffixes the name (`ioh-quick+d5`) so a report can never conflate
29+
it with the preset.
30+
31+
* **Why** — the loop has been optimising a regime that hides its own
32+
best results. Two measured cases, both outside the nightly battery:
33+
34+
| date | change | where the effect lived |
35+
|---|---|---|
36+
| 2026-08-02 | JSO `add_heuristic` | d5 |
37+
| 2026-08-11 | NLSHADE_LBC `add_heuristic` | d2 −0.0241 [−0.0401,−0.0080] **vs** d5 +0.0080 [+0.0007,+0.0154] |
38+
39+
The second is the sharper lesson: the two dims moved in *opposite*
40+
directions with both CIs excluding zero. A 2-D-only loop reads that
41+
change as a loss and rejects it; a (2, 5) loop at least sees the
42+
conflict. GOAL §4's own cadence guardrail — "if the quick-battery
43+
score stalls for >1 week with evidence banked, the bottleneck is the
44+
*measurement regime*" — has been triggered for five weeks.
45+
46+
* **Cost** — measured on the quick battery, 2 specs, sync-eval on:
47+
**8.9 s → 11.9 s (1.34×)**, 6 runs → 12. Much less than the 2.5×
48+
the budget rule (`budget_multiplier * dim`) suggests, because run
49+
time here is not budget-bound. The nightly goes ~5.4 → ~7.2 min
50+
against a 90-minute timeout.
51+
52+
* **Scale discontinuity, deliberate and recorded** — widening moves the
53+
*level*, not just the noise: on this battery d2 alone reads 0.3685
54+
while (d2, d5) reads ~0.309, because d5 is simply harder. Nights
55+
before and after the switch are therefore **not on one scale**. The
56+
per-iteration `aocc_extra_dims` field records which battery produced
57+
each record, exactly as `sync_eval` records which evaluation mode did;
58+
any cross-night consumer has to group by both. Recording this is the
59+
same discipline whose absence produced the hold-out metric-mismatch
60+
bug fixed earlier today.
61+
62+
* **Validation** — 1967 passed, 1 skipped; `ruff format --check` clean;
63+
pyright 0 errors. 9 new tests, including that every measurement leg
64+
widens identically (a 2-D baseline against a (2,5)-D candidate would
65+
be a silent catastrophe) and that re-widening does not stack name
66+
suffixes.
67+
2068
### 2026-08-11 (second session) — measurement fidelity: hold-out metric-mismatch bug fixed, confirm gate crosses a base seed, nightly seed rotation, `--sync-eval`, eps recalibration
2169

2270
* **What** — a review of the full 34-night AOCC ledger (952 records,

scripts/self_improve.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,26 @@ def _build_parser() -> argparse.ArgumentParser:
308308
),
309309
)
310310
run_p.set_defaults(extra_highdim=False)
311+
run_p.add_argument(
312+
"--aocc-extra-dims",
313+
dest="aocc_extra_dims",
314+
default="",
315+
metavar="D[,D...]",
316+
help=(
317+
"Append these dimensions to the AOCC battery for every "
318+
"measurement (screening, confirm, guard, hold-out). "
319+
"'--aocc-extra-dims 5' turns the nightly's quick dims=(2,) "
320+
"regime into dims=(2, 5). The mode presets stay frozen; "
321+
"the widened battery is composed, not edited, and its name "
322+
"gains a '+d5' suffix so it cannot be confused with the "
323+
"preset. Motivation: the two sharpest measured results of "
324+
"2026-08 (JSO d5 add, NLSHADE_LBC per-dim split) both lived "
325+
"at d5, invisible to a 2-D-only loop. Costs more than "
326+
"linearly — budget is budget_multiplier*dim, so d5 runs are "
327+
"2.5x longer than d2 ones. AOCC path only; the composite "
328+
"path reaches higher dims via --extra-highdim."
329+
),
330+
)
311331
run_p.add_argument(
312332
"--timeout",
313333
type=float,
@@ -1181,13 +1201,15 @@ def _run_subprocess(cmd: Sequence[str]) -> "subprocess.CompletedProcess[Any]":
11811201
return subprocess.run(list(cmd), check=False)
11821202

11831203

1184-
def _parse_seed_list(raw: str) -> tuple:
1185-
"""Parse a comma-separated seed list (e.g. ``"1234,5678,9012"``).
1204+
def _parse_seed_list(raw: str, flag: str = "--holdout-base-seeds") -> tuple:
1205+
"""Parse a comma-separated integer list (e.g. ``"1234,5678,9012"``).
11861206
11871207
Empty / blank → empty tuple. Whitespace around entries is tolerated
11881208
so command-line callers can write ``"1234, 5678"`` without quoting
11891209
surprises. Non-integer entries raise ``ValueError`` with the
1190-
offending token for ergonomic error messages.
1210+
offending token for ergonomic error messages; ``flag`` names the
1211+
option in that message so the parser can be shared by more than one
1212+
comma-separated-int option.
11911213
"""
11921214
s = (raw or "").strip()
11931215
if not s:
@@ -1200,7 +1222,7 @@ def _parse_seed_list(raw: str) -> tuple:
12001222
try:
12011223
out.append(int(token))
12021224
except ValueError as e:
1203-
raise ValueError(f"--holdout-base-seeds: invalid integer {token!r}") from e
1225+
raise ValueError(f"{flag}: invalid integer {token!r}") from e
12041226
return tuple(out)
12051227

12061228

@@ -1220,6 +1242,17 @@ def _cmd_run(args: argparse.Namespace) -> int:
12201242
except ValueError as e:
12211243
print(f"Error: {e}", file=sys.stderr)
12221244
return 1
1245+
try:
1246+
aocc_extra_dims = _parse_seed_list(getattr(args, "aocc_extra_dims", ""), flag="--aocc-extra-dims")
1247+
except ValueError as e:
1248+
print(f"Error: {e}", file=sys.stderr)
1249+
return 1
1250+
if any(d < 1 for d in aocc_extra_dims):
1251+
print(
1252+
f"Error: --aocc-extra-dims must be positive, got {list(aocc_extra_dims)}",
1253+
file=sys.stderr,
1254+
)
1255+
return 1
12231256
try:
12241257
extra_families = None
12251258
if getattr(args, "extra_highdim", False):
@@ -1271,6 +1304,7 @@ def _cmd_run(args: argparse.Namespace) -> int:
12711304
confirm_accepts=args.confirm_accepts,
12721305
confirm_iteration_offset=args.confirm_iteration_offset,
12731306
sync_eval=args.sync_eval,
1307+
aocc_extra_dims=aocc_extra_dims,
12741308
)
12751309
except ValueError as e:
12761310
print(f"Error: {e}", file=sys.stderr)

0 commit comments

Comments
 (0)