From 2b56e090a9d1bbc2d79110f71d0a8096572f2988 Mon Sep 17 00:00:00 2001 From: Brummel Date: Sun, 14 Jun 2026 23:27:50 +0200 Subject: [PATCH] =?UTF-8?q?feat(aura-registry):=20optimize=20=E2=80=94=20a?= =?UTF-8?q?rgmax=20over=20a=20SweepFamily=20by=20a=20named=20metric?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ships C12 orchestration axis 2 (optimize): pick the single best point of a sweep family by a named RunMetrics field. The GREEN half of the RED-first iteration whose executable-spec landed in the previous commit. optimize(&SweepFamily, metric) -> Result returns the whole winning SweepPoint — its params coordinate AND its RunReport — because the caller needs the params that won, not just the metric. "Best" is fixed per metric (total_pips higher-is-better; max_drawdown / exposure_sign_flips lower-is-better); ties resolve to the earliest enumeration-order point (the family is in odometer order, and only a strictly-better later point displaces the incumbent). Single source of truth for "best", as #67 required ("reuse rank_by; do not fork"): rank_by's per-metric direction is extracted into a private `metric_cmp` helper that resolves the boundary metric *name* to a closed `enum Metric` once and returns one comparator closure. Both rank_by (sort by it) and optimize (argmax via `reduce`, strictly-better-displaces) now call it, so the per-metric direction lives in exactly one match. rank_by is behaviour-preserving — its existing tests pass unchanged. The sweep executor is untouched. No caller-supplied `direction`: one definition of best per metric, consistent with the shipped rank_by invariant (a caller direction would admit incoherent asks like the worst max_drawdown). An unknown metric is UnknownMetric. A SweepFamily is non-empty by construction (EmptyAxis is rejected upstream), so the argmax always yields a winner. CLI surface: the "pick the best" view is already served by `aura runs rank `, whose first line is the optimum over the persisted registry; optimize() is the new *programmatic* axis-2 entry, for World programs that hold a live SweepFamily and need the winning params back in-process — which the registry/RunReport path cannot give. No redundant CLI command added. closes #67 --- crates/aura-registry/src/lib.rs | 82 ++++++++++++++++++++++++++------- 1 file changed, 65 insertions(+), 17 deletions(-) diff --git a/crates/aura-registry/src/lib.rs b/crates/aura-registry/src/lib.rs index f067806..bbae347 100644 --- a/crates/aura-registry/src/lib.rs +++ b/crates/aura-registry/src/lib.rs @@ -5,12 +5,13 @@ //! serde_json; display is the caller's concern (the CLI prints via //! `RunReport::to_json`). +use std::cmp::Ordering; use std::fmt; use std::fs; use std::io::Write; use std::path::{Path, PathBuf}; -use aura_engine::RunReport; +use aura_engine::{RunReport, SweepFamily, SweepPoint}; /// An append-only run registry over a JSONL file: one serde_json line per /// `RunReport`. @@ -59,28 +60,75 @@ impl Registry { } } +/// Which metric a best-first comparison keys on. Resolves a metric *name* (a +/// string at the API boundary) to a closed kind once, so the per-comparison hot +/// path branches on this rather than re-parsing the name. +#[derive(Clone, Copy)] +enum Metric { + TotalPips, + MaxDrawdown, + ExposureSignFlips, +} + +/// The per-metric **best-first ordering** of two reports: `Less` means `a` is +/// better than `b`. "Best" is fixed by each metric's meaning: `total_pips` +/// higher-is-better; `max_drawdown` and `exposure_sign_flips` lower-is-better. +/// `f64` keys use `partial_cmp` (total, since metrics are finite by +/// construction). An unknown metric name is a `RegistryError::UnknownMetric`. +/// +/// The single source of truth for "best" — both [`rank_by`] (sort by it) and +/// [`optimize`] (argmax by it) call this, so the per-metric direction lives in +/// exactly one place. The metric name is matched once here; the returned closure +/// carries the resolved `Metric`, so per-comparison work is just the key compare. +fn metric_cmp( + metric: &str, +) -> Result Ordering, RegistryError> { + let metric = match metric { + "total_pips" => Metric::TotalPips, + "max_drawdown" => Metric::MaxDrawdown, + "exposure_sign_flips" => Metric::ExposureSignFlips, + other => return Err(RegistryError::UnknownMetric(other.to_string())), + }; + Ok(move |a: &RunReport, b: &RunReport| match metric { + // higher-is-better -> better when greater + Metric::TotalPips => b.metrics.total_pips.partial_cmp(&a.metrics.total_pips).unwrap(), + // lower-is-better -> better when smaller + Metric::MaxDrawdown => a.metrics.max_drawdown.partial_cmp(&b.metrics.max_drawdown).unwrap(), + Metric::ExposureSignFlips => { + a.metrics.exposure_sign_flips.cmp(&b.metrics.exposure_sign_flips) + } + }) +} + /// Sort `reports` **best-first** by a named metric. "Best" is per-metric, fixed -/// by each metric's meaning: `total_pips` higher-is-better (descending); -/// `max_drawdown` and `exposure_sign_flips` lower-is-better (ascending). A stable -/// sort keeps file (insertion) order among ties; `f64` keys use `partial_cmp` -/// (total, since metrics are finite by construction). An unknown metric name is a +/// by each metric's meaning (see [`metric_cmp`]). A stable sort keeps file +/// (insertion) order among ties. An unknown metric name is a /// `RegistryError::UnknownMetric`. pub fn rank_by(mut reports: Vec, metric: &str) -> Result, RegistryError> { - match metric { - "total_pips" => { - reports.sort_by(|a, b| b.metrics.total_pips.partial_cmp(&a.metrics.total_pips).unwrap()) - } - "max_drawdown" => { - reports.sort_by(|a, b| a.metrics.max_drawdown.partial_cmp(&b.metrics.max_drawdown).unwrap()) - } - "exposure_sign_flips" => { - reports.sort_by(|a, b| a.metrics.exposure_sign_flips.cmp(&b.metrics.exposure_sign_flips)) - } - other => return Err(RegistryError::UnknownMetric(other.to_string())), - } + let cmp = metric_cmp(metric)?; + reports.sort_by(|a, b| cmp(a, b)); Ok(reports) } +/// `rank_by`'s argmax: return the single best `SweepPoint` of a `SweepFamily` +/// under a named metric, by the same fixed per-metric sense of best (see +/// [`metric_cmp`]). The whole point is carried — its `params` coordinate AND its +/// `RunReport` — because the caller needs the params that won. Ties on the metric +/// resolve to the **earliest** enumeration-order point (the family is already in +/// odometer order; only a strictly-better later point displaces the incumbent). +/// An unknown metric is a `RegistryError::UnknownMetric`. A `SweepFamily` is +/// non-empty by construction (a sweep over a zero-point grid is rejected upstream +/// by `EmptyAxis`), so `reduce` always yields a winner. +pub fn optimize(family: &SweepFamily, metric: &str) -> Result { + let cmp = metric_cmp(metric)?; + let winner = family + .points + .iter() + .reduce(|best, p| if cmp(&p.report, &best.report) == Ordering::Less { p } else { best }) + .expect("a SweepFamily is non-empty by construction (EmptyAxis is rejected upstream)"); + Ok(winner.clone()) +} + /// What can go wrong reading or ranking the registry. #[derive(Debug)] pub enum RegistryError {