feat(engine,registry): SweepPoint carries RunReport; add aura-registry (cycle D / iter 2)
Iteration 2 of the run-registry cycle (#33): make the sweep family self-describing and add the registry store. Engine. SweepPoint.metrics (RunMetrics) becomes SweepPoint.report (RunReport), and the sweep closure bound is now Fn(&[Scalar]) -> RunReport. This closes the manifest-per-point gap cycle C explicitly deferred to #33 — each swept point now carries its full (manifest, metrics), the unit the registry indexes (C18). All engine-internal callers, the engine test fixture run_point, and the three sweep tests were rethreaded in the same iteration so the crate stays green; the CLI caller followed in the same change so `cargo test --workspace` is green again. CLI. The sweep_report closure builds a per-point RunReport (manifest params = param_space names zipped onto the point via scalar_as_param_f64; commit/window/ seed/broker as run_sample builds them) and prints via RunReport::to_json. The hand-rolled sweep_point_to_json/json_string/scalar_token are retired (the report renders itself), and the orphaned ParamSpec/SweepPoint imports dropped. Net: aura run, aura sweep, and (next iteration) aura runs all print the same RunReport shape. The two aura sweep goldens now pin the per-point manifest params, NOT the commit (it is the real git HEAD, volatile). Registry. New crate aura-registry: Registry::{open, append, load} over an append-only runs/runs.jsonl (one serde_json line per RunReport), free rank_by (best-first per metric — total_pips desc, max_drawdown/exposure_sign_flips asc), and RegistryError (Io / Parse{line} / UnknownMetric). Five unit tests cover append->load round-trip, missing-file = empty, corrupt-line = Parse{line}, per-metric ranking, and unknown-metric error. Built and unit-tested; NOT yet CLI-wired (aura sweep persistence + aura runs list/rank are iteration 3). Verified: cargo test --workspace green (aura-registry 5, aura-engine 93, aura-cli 7+8, others unchanged); clippy --workspace --all-targets -D warnings clean; aura run stdout shape unchanged; aura sweep now emits full RunReports. refs #33
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
//! the engine cannot generically own — C8/C18).
|
||||
|
||||
use aura_core::{ParamSpec, Scalar, ScalarKind};
|
||||
use crate::RunMetrics;
|
||||
use crate::RunReport;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
/// A validated cartesian grid over a blueprint's param-space: one discrete
|
||||
@@ -91,12 +91,14 @@ pub enum SweepError {
|
||||
EmptyAxis { slot: usize },
|
||||
}
|
||||
|
||||
/// One enumerated point and the metrics its run produced. Self-describing: the
|
||||
/// `params` vector is the point's coordinate in `param_space()` order.
|
||||
/// One enumerated point and the full `RunReport` its run produced.
|
||||
/// Self-describing: `params` is the point's coordinate in `param_space()` order,
|
||||
/// and `report` carries the run's `(manifest, metrics)` — the unit the run
|
||||
/// registry indexes (C18).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SweepPoint {
|
||||
pub params: Vec<Scalar>,
|
||||
pub metrics: RunMetrics,
|
||||
pub report: RunReport,
|
||||
}
|
||||
|
||||
/// The ordered result family of a sweep — one `SweepPoint` per grid point, in
|
||||
@@ -110,10 +112,10 @@ pub struct SweepFamily {
|
||||
/// the family in enumeration order. `run_one` builds + bootstraps + runs +
|
||||
/// summarizes one point; it shares nothing mutable, so it is `Sync` and the runs
|
||||
/// are lock-free. Parallelism is `available_parallelism()` workers — `std` only,
|
||||
/// no external dependency (C16).
|
||||
/// via `std::thread::scope`.
|
||||
pub fn sweep<F>(space: &GridSpace, run_one: F) -> SweepFamily
|
||||
where
|
||||
F: Fn(&[Scalar]) -> RunMetrics + Sync,
|
||||
F: Fn(&[Scalar]) -> RunReport + Sync,
|
||||
{
|
||||
let nthreads = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
|
||||
sweep_with_threads(space, nthreads, run_one)
|
||||
@@ -128,7 +130,7 @@ where
|
||||
/// the completion order. Only the cursor is shared; the results side is lock-free.
|
||||
fn sweep_with_threads<F>(space: &GridSpace, nthreads: usize, run_one: F) -> SweepFamily
|
||||
where
|
||||
F: Fn(&[Scalar]) -> RunMetrics + Sync,
|
||||
F: Fn(&[Scalar]) -> RunReport + Sync,
|
||||
{
|
||||
let points = space.points();
|
||||
// `points.len() >= 1` always (GridSpace rejects empty axes), so the clamp
|
||||
@@ -136,11 +138,11 @@ where
|
||||
let nthreads = nthreads.clamp(1, points.len());
|
||||
let cursor = AtomicUsize::new(0);
|
||||
|
||||
let mut results: Vec<(usize, RunMetrics)> = std::thread::scope(|scope| {
|
||||
let mut results: Vec<(usize, RunReport)> = std::thread::scope(|scope| {
|
||||
let handles: Vec<_> = (0..nthreads)
|
||||
.map(|_| {
|
||||
scope.spawn(|| {
|
||||
let mut local: Vec<(usize, RunMetrics)> = Vec::new();
|
||||
let mut local: Vec<(usize, RunReport)> = Vec::new();
|
||||
loop {
|
||||
let i = cursor.fetch_add(1, Ordering::Relaxed);
|
||||
if i >= points.len() {
|
||||
@@ -159,7 +161,7 @@ where
|
||||
SweepFamily {
|
||||
points: results
|
||||
.into_iter()
|
||||
.map(|(i, metrics)| SweepPoint { params: points[i].clone(), metrics })
|
||||
.map(|(i, report)| SweepPoint { params: points[i].clone(), report })
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
@@ -169,7 +171,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
f64_field, summarize, BlueprintNode, Composite, Edge, OutField, ParamAlias, Role,
|
||||
RunMetrics, Target,
|
||||
RunManifest, Target,
|
||||
};
|
||||
use aura_core::{Firing, ParamSpec, Scalar, ScalarKind, Timestamp};
|
||||
use aura_std::{Exposure, Recorder, SimBroker, Sma, Sub};
|
||||
@@ -327,10 +329,12 @@ mod tests {
|
||||
(bp, rx_eq, rx_ex)
|
||||
}
|
||||
|
||||
/// Build + bootstrap + run + drain + summarize one grid point. A free `fn`
|
||||
/// (Copy + Sync) so it serves as the `sweep` closure AND a direct reference
|
||||
/// for the "sweep == N independent runs" comparison.
|
||||
fn run_point(point: &[Scalar]) -> RunMetrics {
|
||||
/// Build + bootstrap + run + drain + summarize one grid point into a
|
||||
/// `RunReport`. A free `fn` (Copy + Sync) so it serves as the `sweep` closure
|
||||
/// AND a direct reference for the "sweep == N independent runs" comparison.
|
||||
/// The manifest is a minimal fixed fixture — the metrics are the run's, and
|
||||
/// determinism makes `run_point` reproduce a point's report exactly.
|
||||
fn run_point(point: &[Scalar]) -> RunReport {
|
||||
let (bp, rx_eq, rx_ex) = composite_sma_cross_harness();
|
||||
let mut h = bp
|
||||
.bootstrap_with_params(point.to_vec())
|
||||
@@ -338,7 +342,16 @@ mod tests {
|
||||
h.run(vec![synthetic_prices()]);
|
||||
let equity = f64_field(&rx_eq.try_iter().collect::<Vec<_>>(), 0);
|
||||
let exposure = f64_field(&rx_ex.try_iter().collect::<Vec<_>>(), 0);
|
||||
summarize(&equity, &exposure)
|
||||
RunReport {
|
||||
manifest: RunManifest {
|
||||
commit: "test".to_string(),
|
||||
params: Vec::new(),
|
||||
window: (Timestamp(0), Timestamp(0)),
|
||||
seed: 0,
|
||||
broker: "test".to_string(),
|
||||
},
|
||||
metrics: summarize(&equity, &exposure),
|
||||
}
|
||||
}
|
||||
|
||||
fn sma_cross_grid() -> GridSpace {
|
||||
@@ -371,8 +384,8 @@ mod tests {
|
||||
// each point's metrics equal a direct, independent run of the same point:
|
||||
// the sweep adds enumeration + execution, never a metrics change (C1).
|
||||
for pt in &family.points {
|
||||
assert_eq!(pt.metrics, run_point(&pt.params));
|
||||
assert!(pt.metrics.total_pips.is_finite());
|
||||
assert_eq!(pt.report, run_point(&pt.params));
|
||||
assert!(pt.report.metrics.total_pips.is_finite());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,9 +404,9 @@ mod tests {
|
||||
fn distinct_points_produce_distinct_metrics() {
|
||||
let family = sweep(&sma_cross_grid(), run_point);
|
||||
// not a constant family: differing SMA lengths produce differing equity
|
||||
let first = family.points[0].metrics.total_pips;
|
||||
let first = family.points[0].report.metrics.total_pips;
|
||||
assert!(
|
||||
family.points.iter().any(|p| p.metrics.total_pips != first),
|
||||
family.points.iter().any(|p| p.report.metrics.total_pips != first),
|
||||
"a 4-point SMA-length grid must not collapse to one metric",
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user