Native HPO — Phase 1 implementation progress log

Branch: feat/native-hpo-phase1 · Worktree: _worktrees/native-hpo-phase1 · Scope: nirs4all-methods only (stop before dag-ml / nirs4all-core / any other repo). Plan: FINETUNING_ROADMAP.md §6 (two-phase + parallel tracks) · FINETUNING_F0_PR.md (F0 detail). Rule: each big block is Codex-reviewed before moving on. Everything stays green (ctest dev-release) + ABI-gated.

Status board

Block

State

Codex review

Notes

Setup (worktree, baseline build)

✅ done

isolated worktree; docs committed; toolchain OK

F0 — ABI surface + random/none slice + scaffolding

🟢 done+reviewed

✅ Codex

365/365 tests; ABI 2.1; Codex-reviewed (14 findings all applied). Catalog --strict-abi reconcile + HPO parity CI (Track Q) deferred.

F1 — samplers: sobol, lhs, ternary

✅ done

🟢 ternary+lhs

ternary + lhsCodex-reviewed (7 findings applied); sobolTier-A bit-exact vs scipy (embedded Joe–Kuo table), 380 tests + Python parity

F2/F5 — pruners: median, asha, hyperband, racing (+ fidelity engine)

✅ done (pruners)

🟢 median+asha+hyperband

median+asha+racing+hyperbandall 4 implemented + Codex-reviewed (385 tests). n_components fidelity engine (F2c) → Phase 2 (the pruner ABI already consumes any host-supplied rung stream; wiring a PLS learning-curve into a pruned finetune loop is dag-ml’s orchestration job per the North Star).

F3 — RNG consolidation → ga, pso

✅ done (samplers)

🟢 Codex

ga + pso samplers ✅ (376 tests, shared decode_candidate); RNG-consolidation of the feature-selection loops deferred (HPO samplers are clean fresh impls)

F4 — cmaes, tpe, gp_ei

✅ done

🟢 cmaes+tpe

cmaes + tpeCodex-reviewed; gp_ei ✅ (GP+EI, from-scratch Cholesky, 381 tests, converges <0.03 across 10 seeds). All 8 sampler kinds implemented.

Q — HpoSpec + comparators + parity CI

✅ done

parity/hpo/ — 12 HpoSpecs, golden traces, Sobol Tier-A vs scipy, pruner decisions vs independent pure-Python rules; 16 pytest + CLI gate wired into parity-gate.yml.

B — bindings python→R/MATLAB/WASM + cross-binding gate

🟡 python done

Python binding ✅ (ask/tell wrapper + smoke/parity vs dev .so; cross-binding gate ✅ = Track Q golden traces). R/MATLAB/WASM: blocked on runtimes — not installable in this sandbox (no Rscript/Octave/EMSDK), so writing their C-shim marshallers blind can’t be validated; they follow the Python thin-marshaller + established R .Call dispatch pattern and their acceptance test is the committed parity/hpo/golden/*.json. Left for a runtime-equipped env / CI.

catalog YAML

⬜ optional

HPO methods are enum-dispatched behind the shared n4m_optimizer_* surface (no per-method ABI symbols), so they don’t fit the per-method-symbol catalog model. Docs + bib + Track-Q parity already cover “comme pour le reste”. Deliberately deferred.

Phase 1 — native HPO in libn4m

merged to main

🟢

9 samplers + 5 pruners + finetune_estimator + Python binding + Track-Q parity CI; ABI 2.1 (733 symbols). Merged f69a2ec7..1733c0d9.

Phase 1.5 / E2 — active-aware conditional cascade

merged to main

🟢 Codex

Deactivate-only nested conditional activation (the substrate for sub-pipeline search). 387 tests. Merged f53f2854. E1 (multi-parent) / E3 (choice node) not needed by the nirs4all integration → deferred.

Phase 2 / nirs4all integrationengine:"n4m" finetuning

done (nirs4all repo)

🟢 Codex

N4MFinetuneManager (drop-in Optuna peer) + when/when_not conditional DSL + example U08 + docs + 22 tests. Merged to nirs4all main (bff6710c). Native TPE matches Optuna’s optimum.

catalog YAML

⬜ optional

HPO methods are enum-dispatched (no per-method symbols); docs + bib + Track-Q parity cover “comme pour le reste”. Deferred.

Remaining (cross-language)

R/MATLAB/WASM finetuning bindings (thin marshallers over the native optimizer; acceptance = Track-Q golden traces) + the dag-ml cross-language pipeline compiler (docs/PIPELINE_FINETUNING.md P1–P6).

Legend: ⬜ todo · 🟡 in progress · ✅ done · 🟢 done+reviewed

Log

2026-07-11 — Phase 1.5 (E2) + Phase 2 (nirs4all native finetuning) shipped

  • Green light received (ecosystem released, no other agent) → merged Phase 1 to nirs4all-methods main, then built the pipeline-finetuning path.

  • E2 (native enabler, merged): rewrote apply_conditions to be deactivate-only — a param is active iff its own condition holds AND its conditional parent is active, resolved up the forest; the # cascade can no longer reactivate a dead branch regardless of declaration order. This is the correctness nested sub-pipeline search needs. Codex-reviewed (transcript 09: 1 Major + 2 Minor applied — order-independence, NOT_IN fail-closed, flat-space perf). 387 C++ tests; ABI unchanged.

  • Phase 2 (nirs4all repo): nirs4all/optimization/n4m_engine.pyN4MFinetuneManager, a drop-in peer of OptunaManager selected by finetune_params={"engine":"n4m"}. Same DSL/approaches/metrics/FinetuneResult; 9 samplers + 4 pruners; wired into BaseModelController.finetune. when/when_not conditional clause added (object__attribute conditional finetuning → uses E2). Example examples/user/04_models/U08_native_finetuning.py, user-guide section, 22 unit tests. Codex-reviewed (9 Major + 3 Minor all applied — sorted_tuple, nested-static, approach routing, non-finite pruning, untold-trial, direction-blind aggregation, unknown-engine, seed=None, log-flag). Native TPE matches Optuna’s optimum (n_components=14, identical best_value). 218 finetune tests pass. Merged to nirs4all main (bff6710c).

  • Deferred: E1/E3 native enablers (not needed by the integration); R/MATLAB/WASM finetuning bindings (runtimes absent here); the dag-ml cross-language pipeline compiler (PIPELINE_FINETUNING.md P1–P6) for full operator/sub-pipeline materialisation.

2026-07-10 — Pipeline finetuning design (object__attribute over sub-pipelines)

  • User asked: can we finetune a pipeline directly with the operators in the search space (tune sub-pipelines / whole pipelines, Optuna object__attribute style), not just scalars? Verified the native substrate in code and wrote docs/PIPELINE_FINETUNING.md.

  • Verdict: the substrate is already there for a flat pipeline, one operator-choice per slot — categorical slot + CONDITION_IN/_NOT_IN gating + # name-cascade + is_active readback + TPE tree-sampler (exactly the kernel/gamma test). What’s missing: (a) the pipeline⇄space compiler + materialiser (the bulk → dag-ml graph compile/materialise + a nirs4all Python controller for the object__attribute UX → Phase 2), and (b) a few native enablers for deep/branchy spaces — E2 active-state-aware topological cascade (behavioural, no ABI), E1 multi-parent conditions (ABI minor), E3 first-class choice/branch node (ABI minor), E4 bounded variable-length (convention). None needs new numerical kernels. Full phased plan (E1–E4 native; P1–P6 dag-ml/nirs4all) + dependency DAG in the doc. Planned only — not implemented; handed back to the user.

2026-07-10 — Phase-1 (verifiable) consolidation — full green, ABI invariant held

  • Full project gate green: ctest --preset dev-release = 2/2 binaries pass (n4m_tests 385 + n4m_internal_tests; labels abi/parity/smoke). version-sync OK (project 1.0.9, ABI 2.1.0). ABI symbol surface unchanged: 733 == 733, 0 added / 0 removed — proves the enum-value-only invariant held across all of F1–F5: the entire sampler+pruner library plus the two hyperband option fields (carved from reserved[]) added zero public symbols beyond the single F0 ABI freeze. sizeof(n4m_optimizer_options_t) verified C == Python == 120.

  • What’s done & verified (nirs4all-methods, this worktree): 9 sampler kinds (random, sobol[Tier-A], lhs, ternary, ga, pso, cmaes, tpe, gp_ei) · 5 pruner kinds (none, median, asha, hyperband, racing) · n4m_finetune_estimator · Python binding · Track-Q HPO parity CI (golden traces + Sobol-vs-scipy + pruner-vs-independent-reference, wired into parity-gate.yml) · per-method docs + bibliography · 7 Codex reviews applied (F0/F1/F2/F3/F4/sobol-gp/hyperband).

  • Honest remaining scope (NOT sandbox-verifiable or Phase-2-coupled): (a) R/MATLAB/WASM bindings — need Rscript/Octave/EMSDK, absent here; pattern-ready, acceptance test = the committed golden traces; (b) n_components fidelity engine — the pruner ABI already consumes any host rung stream, so the PLS-learning-curve-driven pruned finetune loop is dag-ml orchestration (Phase 2, North Star); (c) catalog YAML — HPO methods are enum-dispatched (no per-method symbols), awkward fit, optional. Stopping here per the goal: nothing touched outside nirs4all-methods; awaiting green light to merge + start Phase 2.

2026-07-10 — Track Q: HPO cross-binding parity CI + hyperband Codex review applied

  • Track Q (parity/hpo/) — the HPO parity harness, the analogue of the numeric fixtures. specs.py (HpoSpec + a 12-cell REGISTRY: every sampler ≥1×, mixed/categorical, + median/asha/hyperband pruner cells), objectives.py (portable closed-form objectives so every binding computes the same tell() score), run_native.py (spec → StudyTrace via the Python binding), comparators.py, references.py (scipy Sobol + independent pure-Python reimplementations of the median/asha/hyperband rules), run.py (CLI gate) + golden/*.json (committed cross-binding contract) + tests/test_hpo_parity.py. Checks three things: golden-trace stability (same seed → same proposals, the cross-binding guarantee the R/MATLAB/WASM bindings must meet), Sobol Tier-A == scipy, and native pruner verdicts == the pure-Python reference (a second implementation guarding the native one). 16 pytest pass + CLI gate green; wired into .github/workflows/parity-gate.yml (the job that already builds libn4m). The pruner-decision cross-check passing (median 9, asha 11, hyperband 10 pruned, all matching an independent reimplementation) is strong evidence the native pruners — including brand-new hyperband — are correct.

  • Hyperband Codex review (transcript 08: 0 Blocker, 3 Major, 1 Minor — all applied). Major: (1) removed the moving-high-water-mark R derivation — max_resource is now required (>0) (make_prunerN4M_ERR_INVALID_ARGUMENT for 0) so the bracket count is stable for the study’s lifetime; (2) int64_t resource/R (no step+1 overflow) + step<0 guard; (3) k > k_max guard so rungs above R never prune. Minor: test_hyperband_edges (reject max_resource=0; no-prune above R). Pure-Python reference mirrors the same rule; hyperband_prune golden unchanged. 385 passed, 0 failed.

2026-07-10 — sobol+gp Codex review applied (transcript 07)

  • Codex review of the sobol+gp block: 0 Blocker, 3 Major, 1 Minor — all applied (docs/reviews/finetuning-roadmap/codex-review-07-sobol-gp.md). Major: (1) Sobol OOB after 2^30 asks — added an exhausted_ guard; once the Gray-code column would exceed 30 bits the sampler falls back to base random (no OOB read). (2) GP fake-distinct observations — the GP now stores the decoded (snapped) continuous-axis coords, so two asks that decode to the same integer share one coordinate (models real observations, not fakes); kept int axes in the GP — n_components is an int and continuous relaxation is standard (consistent with the approved CMA-ES) — which fixes Codex’s exact failure without crippling integer HPO. (3) M_PI → local constexpr kPi (C++17-portable). Minor: added test_gp_ei_maximize (EI-sign) + test_gp_ei_edge_cases (pure-categorical fallback + constant-objective/duplicate-coord Cholesky stability). 384 passed, 0 failed; Sobol Tier-A + Python smoke still green.

2026-07-10 — F5: hyperband pruner (completes the pruner set)

  • Added N4M_PRUNER_HYPERBAND (cpp/src/core/optimization/pruners.cpp): Hyperband as an early-stopping pruner — multiple successive-halving brackets that hedge the early-stopping rate. Rungs at geometric resource levels eta^k (decision only on a rung boundary); each trial assigned a bracket s round-robin by stable ask order, with a grace period exempting it below rung s; at each rung, survive only if in the top 1/eta of same-bracket peers that reached it (ASHA-style, ties survive). Configured from two new forward-compat option fields carved from reserved[]max_resource (top rung R; 0 = derive from largest reported step) and reduction_factor (eta; 0 = default 3) — so no new ABI symbol and sizeof(n4m_optimizer_options_t) is unchanged (verified C == Python == 120 bytes; ABI symbol surface diff = 733↔733, zero adds/removes). asha’s eta is now also taken from reduction_factor (default 3). Decision-level test proves bracket differentiation (worst-in-bracket-0 pruned at rung 0; bracket-1 trial with a far worse score survives via grace; non-rung step never prunes) + Python end-to-end. 382→384 passed. Docs docs/methods/hyperband.md, updated asha.md + docs/abi/changes_log.md. All 5 pruner kinds implemented.

2026-07-10 — F4: gp_ei sampler (completes F4 + all 8 samplers)

  • Added N4M_SAMPLER_GP_EI (cpp/src/core/optimization/gp.cpp): Bayesian optimization with a Gaussian-process surrogate + Expected Improvement. After n_startup_trials random trials, each ask fits an RBF GP on the completed+scored history over the continuous axes (unit space) and returns the max-EI candidate over a 64-point random acquisition batch. Dependency-free: RBF kernel with a median-distance lengthscale heuristic (no marginal-likelihood inner loop), K+1e-6·I solved by a from-scratch dense Cholesky (forward/back substitution) — fine at NIRS trial counts. Direction-symmetric EI (MAXIMIZE negates the posterior mean), ξ=0.01. Non-continuous axes drawn by the shared decode (independent fallback); enqueue unsupported; pure-categorical spaces degrade to random. Stores its own per-trial proposals (id → cont-axis coords) so future fits read exact coordinates.

  • Convergence test on a smooth 2-D objective in 60 evals (best < 0.5); measured best < 0.03 across seeds 1–10 — much more sample-efficient than random/CMA (300 evals). 381 passed, 0 failed. Reserved-sampler test now uses an out-of-range enum (all 8 kinds are implemented). ABI unchanged (enum-value-only). Doc docs/methods/gp_ei.md + rasmussen2006gp bib. F4 complete; the full sampler library — random, sobol, lhs, ternary, ga, pso, cmaes, tpe, gp_ei — is done.

2026-07-10 — F1: sobol sampler (Tier-A, completes F1)

  • Added N4M_SAMPLER_SOBOL (cpp/src/core/optimization/sobol.cpp + sobol_direction.hpp): Sobol low-discrepancy sequence, one Sobol dimension per parameter, unscrambled Gray-code recursion over the embedded Joe–Kuo new-joe-kuo-6.21201 direction numbers (52 dims × 30 bits, extracted from scipy.stats.qmc.Sobol._sv). Numeric axes map the unit coord through numeric_from_unit (log/step/int aware); categoricals bucket it; params beyond dim 52 and conditional/sorted-tuple axes fall back to base random. Plugs into the base per-parameter hooks (override_numeric/override_categorical), so constraints/conditions/forced are handled by the base sampler.

  • Tier-A bit-exact parity verified two ways. Nailed the Gray-code algorithm in Python first (np.array_equal vs scipy, diff 0.0). C++ test test_sobol_sequence_parity asserts the first 5 points of a 3-D space equal the known dyadic reference exactly (==, not approx). Python test_sobol_parity.py drives the sampler through the binding and asserts np.array_equal vs scipy.stats.qmc.Sobol(scramble=False) for d {1,3,6,10}, N up to 32 — all exact. 380 passed, 0 failed (C++); Python smoke + parity green. ABI unchanged (enum-value-only). Doc docs/methods/sobol.md. Scrambled (Owen) variant is a later Tier-B addition. F1 complete.

  • Fixed the stale reserved sampler NOT_IMPLEMENTED test to use gp_ei (sobol is now implemented; only the F4 GP surrogate stays reserved).

2026-07-10 — F6: Python binding

  • Added the Python binding for the native optimizer: ctypes decls for all 31 n4m_optimizer_*/n4m_search_space_*/n4m_trial_*/n4m_finetune_estimator symbols (_ffi_decls.py), an OptimizerOptions ctypes struct mirroring n4m_optimizer_options_t (native alignment; _types.py), and an idiomatic wrapper n4m/model_selection/optimizer.py (SearchSpace, Trial, Optimizer + Sampler/Pruner/Direction/Metric enums). Exported from n4m.model_selection.

  • Smoke test (bindings/python/tests/test_optimizer_smoke.py): random converges on a quadratic, TPE converges on a mixed continuous+categorical space and picks the right category, median pruner decisions, seed determinism — all pass against the dev .so (N4M_LIB_PATH=…/libn4m.so.2.1.0). Proves the ABI is usable from a consumer (struct layout, ask/tell, result round-trip). (Env note: two .so versions coexisted in the dev build — the stale pre-bump 2.0.0 and current 2.1.0; point N4M_LIB_PATH at 2.1.0.)

2026-07-10 — F4 samplers Codex review applied

  • Codex review of cmaes+tpe: 3 Blocker, 3 Major, 1 Minor — all applied (docs/reviews/finetuning-roadmap/codex-review-06-F4-samplers.md). Blockers: TPE now requires n≥2 + a non-degenerate split before activating (was an OOB read at n_startup_trials=1); CMA-ES updates from completed+scored trials only (weights renormalised over the scored count; skips the update when none scored) so pruned/failed members never corrupt the mean/covariance. Major: CMA-ES step-size path computed from the repaired step yw/√C (not the raw z) — consistent after box-clipping; CMA-ES restricted to the continuous axes (cont_axes_ map) with non-continuous axes sampled independently (Optuna-style fallback) instead of polluting the covariance; TPE categorical proposal sampled proportional to l/g (not argmax) so the constraint-retry loop can’t livelock. Documented (consistent with ga/pso): hard mutex/requires/exclude handled via fitness for population/CMA samplers; TPE stepped-axis snap at decode. CMA-ES and TPE still converge after the restructure. 379 passed, 0 failed.

2026-07-10 — F4: TPE sampler

  • Added N4M_SAMPLER_TPE (cpp/src/core/optimization/tpe.cpp): univariate Tree-structured Parzen Estimator (Optuna default). Per param: split the completed history into good (top γ=0.25) / bad, build Parzen l(x)/g(x) (KDE in unit space for numeric via a new unit_from_numeric inverse; Laplace-smoothed category frequencies for categorical), draw n_ei=24 candidates from l and keep argmax l/g. Added a base override_categorical() hook (parallel to override_numeric) so TPE plugs into the base sampler’s constraint/condition/forced machinery. Handles mixed/conditional spaces. Convergence test on a continuous+categorical objective (finds x≈3, category ‘a’). Also fixed a stale test (reserved sampler now uses sobol, since TPE is implemented). 379 passed, 0 failed. ABI unchanged. Doc docs/methods/tpe.md.

2026-07-10 — F4: CMA-ES sampler

  • Added N4M_SAMPLER_CMAES (cpp/src/core/optimization/cma.cpp): separable (diagonal) CMA-ES (Ros & Hansen 2008) over the unit hypercube — the canonical mean/covariance/step-size/evolution-path update with a diagonal covariance (no eigendecomposition). Reuses the async-population lifecycle + boundary guard + shared decode (non-continuous axes bucketed = Optuna’s independent fallback). Convergence test asserts best < 0.1 on a smooth 2D objective (a broken CMA-ES would not converge tightly). 378 passed, 0 failed. ABI unchanged. Doc docs/methods/cmaes.md.

2026-07-10 — F3 samplers Codex review applied

  • Codex review of ga+pso: 1 Blocker, 2 Major, 2 Minor — all applied (docs/reviews/finetuning-roadmap/codex-review-05-F3-samplers.md). Blocker: population samplers refuse to cross a generation/iteration boundary until it is fully resolved (synchronous LIAR_NONE evolution) — ask_batch returns a partial batch at the boundary instead of evolving on unscored members (added resolved_in_range). Major: enqueue/warm-start rejected for population samplers (N4M_ERR_UNSUPPORTED via an allow_enqueue() hook) — a forced candidate can’t be inverse-encoded into the population; constraint handling documented as fitness-only. Minor: PSO velocity clamp (vmax=0.5). +1 regression test (batch boundary + enqueue reject). 377 passed, 0 failed.

2026-07-10 — F3: PSO sampler + shared decode

  • Added N4M_SAMPLER_PSO (cpp/src/core/optimization/pso.cpp): particle-swarm optimization over the unit hypercube (Clerc–Kennedy w=0.729, c1=c2=1.494) — swarm of 16 particles with position/velocity/personal-best, global best = best personal best, positions clamped to [0,1). Factored the unit-vector→trial decode into Optimizer::decode_candidate() (shared by ga + pso; GA simplified to use it, still green). Convergence test on a 2D continuous quadratic. 376 passed, 0 failed. ABI unchanged. Doc docs/methods/pso_search.md.

2026-07-10 — F3: GA sampler

  • Added N4M_SAMPLER_GA (cpp/src/core/optimization/ga.cpp): real-coded genetic algorithm over the unit hypercube [0,1)^P, decoded per parameter (numeric via numeric_from_unit, categorical/ordinal bucketed) — handles mixed spaces uniformly. Generational: a population of 16 is asked out, then tournament selection + uniform crossover + Gaussian mutation + elitism produce the next generation once scores arrive (keyed on trial-id ranges; the async-population lifecycle will be reused by pso/cmaes). Convergence test on a 2D continuous quadratic. 375 passed, 0 failed. ABI unchanged. Doc docs/methods/ga_search.md. (This is the HPO-sampler GA over the typed space — distinct from the feature-selection ga_select; sharing the RNG-consolidated loops is a later refinement.)

2026-07-10 — F2: racing pruner

  • Added N4M_PRUNER_RACING (Hoeffding racing) into the reviewed pruner architecture: each trial’s intermediate scores are repeated observations; a trial is pruned once its Hoeffding confidence interval (δ=0.05) no longer overlaps the best trial’s. This is the fold-safe early-stop (roadmap §2c) — correct for exchangeable CV folds where successive-halving’s rank-preservation assumption fails. Decision-level test (clearly-worse trial pruned once enough observations accumulate). 374 passed, 0 failed. ABI unchanged. Doc docs/methods/racing.md. Only hyperband remains reserved (needs the bracket scheduler / total budget → F5).

2026-07-10 — F2 pruners Codex review applied

  • Codex review of the pruner block: 1 Blocker, 3 Major, 3 Minor — all applied (docs/reviews/finetuning-roadmap/codex-review-04-F2-pruners.md). Blocker: terminal state is now terminal — tell_intermediate/tell_result reject reports on a non-RUNNING trial (only an idempotent same-status re-report is accepted), so an auto-pruned trial can no longer be overwritten to COMPLETED and win best(). Major: true 50th-percentile median (mean of the two middle values for even n) — direction-symmetric; finite-score validation on both tell paths (rejects NaN/Inf before it corrupts std::sort); centralized pruner-kind validation in make_optimizer (out-of-range/unimplemented pruner is rejected, not silently degraded to none). Minor: one value per (trial, step) (update-in-place); ASHA reduction_factor documented as fixed-at-3 for F2 + ties-survive semantics. +2 regression tests (pruned-is-terminal, invalid-pruner + NaN). 373 passed, 0 failed.

2026-07-10 — F2: ASHA pruner

  • Added N4M_PRUNER_ASHA (asynchronous successive halving) into the same factory: at each rung a trial survives only if in the top 1/reduction_factor (=3) of the peers at that rung, decided asynchronously (sound for a per-tell verdict). Decision-level test on a canned history. 371 passed, 0 failed. ABI unchanged. Doc docs/methods/asha.md. (Hyperband bracket-scheduler + racing + the n_components fidelity engine remain in F2.)

2026-07-10 — F2 opener: pruner architecture + median pruner

  • Added the Pruner abstraction (cpp/src/core/optimization/pruners.cpp): Optimizer holds a unique_ptr<Pruner> set by a make_pruner() factory from opts.pruner; tell_intermediate() delegates the keep/prune verdict and marks pruned trials N4M_TRIAL_PRUNED. This makes pruners orthogonal to samplers (composed via the options struct), matching the roadmap’s sampler ⟂ pruner split. make_optimizer now accepts NONE+MEDIAN; asha/hyperband/racing slot into the same factory later.

  • Implemented N4M_PRUNER_MEDIAN (Vizier median stopping rule): prune when a trial’s intermediate score is worse than the median of peer scores at the same step; never before min_peers (= n_startup_trials) peers. Decision-level test on a canned history. 370 passed, 0 failed. ABI unchanged. Doc docs/methods/median_pruner.md.

2026-07-10 — F1 samplers Codex review applied

  • Codex review of ternary+lhs: 0 Blocker, 5 Major, 2 Minor — all applied (docs/reviews/finetuning-roadmap/codex-review-03-F1-samplers.md). Ternary reworked into grid-index space: honours step (proposes only on-grid values), reserves RUNNING trials so batched asks don’t collide, skips inactive/off-domain history, and keeps arithmetic bounded (index space, guard against absurdly wide ranges). Base enqueue() now validates numeric ranges + categorical indices (rejects out-of-range warm-starts). LHS: seed domain-separated from the base RNG; size_t index comparison; clearer Fisher-Yates. +2 regression tests (stepped ternary + batch reservation distinctness, enqueue out-of-range rejection). 369 passed, 0 failed.

2026-07-10 — F1: lhs sampler

  • Added N4M_SAMPLER_LHS (cpp/src/core/optimization/lhs.cpp): Latin Hypercube over the numeric axes for the first n_startup_trials asks (one independent permutation per dimension + per-cell jitter), random beyond the batch and for categoricals. Refactored the unit→value mapping into Optimizer::numeric_from_unit() so random/lhs/(future) sobol share it. Test asserts each decile is hit exactly once across the startup batch. 367 passed, 0 failed. ABI unchanged. Doc docs/methods/lhs.md.

  • sobol is intentionally deferred: a useful Tier-A Sobol must bit-match scipy.stats.qmc.Sobol(scramble=False), which requires the exact Joe–Kuo new-joe-kuo-6.21201 direction-number table. Rather than ship an approximate/incorrect sequence, it will be done deliberately with the real table (fetch + embed a modest dimension subset) as a dedicated block.

2026-07-10 — F1: ternary sampler

  • Added N4M_SAMPLER_TERNARY (cpp/src/core/optimization/ternary.cpp): unimodal-integer ternary search porting the nirs4all BinarySearchSampler (triplet anchor low/high/mid → bisect the larger gap toward the current best). Introduced a small base-class hook override_numeric() so adaptive samplers reuse the constraint/loop/conditions machinery without duplicating sample(). Proposal is a pure function of the completed history (idempotent within an ask). Tunes the first integer axis; others stay random.

  • Test: converges to k∈{6,7,8} on a unimodal objective in ≤25 trials. 366 passed, 0 failed. ABI snapshot unchanged (enum-value-only, no new symbol) — confirms the “later samplers add no ABI symbol” design. Doc docs/methods/ternary.md.

2026-07-10 — F0 Codex review applied

  • Codex read-only review of F0: 14 findings (4 Blocker, 9 Major, 1 Minor), all applied (transcript docs/reviews/finetuning-roadmap/codex-review-02-F0.md). Highlights: made optimization.h independently includable (moved the N4M_STATIC_ASSERT macro above the role-header includes and relocated the HPO enum asserts into optimization.h) + added C/C++ compile-only include guards; hardened the ABI boundary (try/catch on every name-based trial accessor, std::string_view lookups, struct_size default-preserving copy with a < 8 guard); made constraints authoritative (condition constraints reject unsupported shapes with N4M_ERR_UNSUPPORTED, enqueue validates param names, sampling skips RNG for forced dims and re-checks constraints, ask returns an error on constraint-exhaustion instead of a silent invalid trial); validated numeric ranges (reject NaN/Inf, log needs positive bounds); default direction = AUTO (derive from metric); n4m_finetune_estimator now rejects unsupported params, returns the full trial trace, and returns NOT_FITTED when nothing completes; implemented timeout/duration via steady_clock; documented MUTEX_GROUP (nirs4all _mutex_ issubset) semantics.

  • 6 new regression tests lock the fixes (invalid ranges, struct_size guard, enqueue warm-start, conditional activation + conflicting-parent rejection, finetune unsupported-param rejection, AUTO+R2 maximization). 365 passed, 0 failed. ABI snapshot unchanged (internal fixes), version-sync green.

2026-07-10 — F0 green

  • Build clean, all tests pass: 359 passed, 0 failed (7 new optimization tests incl. a real n4m_finetune_estimator PLS-CV run; abi_version_compatible_with_header green after the bump).

  • ABI snapshots regenerated (linux 734 + derived macos/windows 733) — drift is exactly the 31 new additive n4m_* symbols, nothing removed. ABI minor bumped 2.0 → 2.1; docs/abi/changes_log.md entry added; CHANGELOG updated.

  • Docs: docs/methods/{optimization,random}.md + _finetuning_bibliography.bib (14 refs).

  • Deferred to Track Q (not blocking F0 landing): catalog optimization.{random,none}.yaml + validate.py --strict-abi reconcile (non-strict passes; new symbols are warnings), and the HpoSpec + parity comparators + CI job. The C++ doctest is F0’s correctness gate with a single sampler.

  • Next: commit F0, Codex review, then F1 (sobol/lhs/ternary).

2026-07-10 — F0 code + env fix

  • Wrote F0: cpp/include/n4m/optimization.h (frozen ABI), cpp/src/core/optimization/optimizer.{hpp,cpp} (SearchSpace + Optimizer: random sampler, none pruner, constraint rejection, conditional activation, ask/tell), cpp/src/c_api/c_api_optimization.cpp (all wrappers + n4m_finetune_estimator internal PLS-CV driver), cpp/tests/test_optimization.cpp (7 cases). Wired n4m.h include + 4-byte enum asserts, CMakeLists (core .cpp), tests CMake + main.cpp registration.

  • Toolchain/env fix (matters for every build here): the conda-linked ~/.local/bin/gfortran used by FITPACK pulls a libm.so linker script referencing the non-existent /lib64/libm.so.6, breaking the libn4m.so link. FITPACK (spline smoothing) has a non-Fortran fallback and is irrelevant to HPO, so configure with -DCMAKE_Fortran_COMPILER=NOTFOUND to disable it. C/C++ compiler is system /usr/bin/{cc,c++}; real libm is /usr/lib/x86_64-linux-gnu. Apply the same flag to the dev-release build used for ABI snapshots.

2026-07-10 — setup

  • Created isolated worktree _worktrees/native-hpo-phase1 on branch feat/native-hpo-phase1 from main (f69a2ec7). Main checkout left clean/untouched.

  • Moved + committed the 4 design docs (strategy, roadmap, F0 PR, Codex review) into the branch (e39ac038).

  • Toolchain: cmake 4.3.2, ninja, g++ (no clang++; g++ path).

  • Next: baseline build sanity, then F0.