feat(aura-engine): sweep named-binding — zip_params + SweepFamily.named_params (C12 #57)
Thread a derived named view of a sweep point so consumers stop hand-zipping param_space() names onto the bare &[Scalar]. The free function zip_params(space, point) -> Vec<(String, Scalar)> in aura-core is the one shared projection; GridSpace retains the ParamSpec list it already receives in new() (it was validated then discarded); SweepFamily carries it (stamped once in sweep_with_threads) and exposes named_params(i). The three CLI hand-zip sites collapse to one zip_params call each. The run-closure signature `Fn(&[Scalar]) -> RunReport` is byte-unchanged — the named view is a derived projection (C23: slot is identity, name is derived), not a closure-currency change — so #52's RandomSpace plugs into the same sweep() execution layer with zero signature reconciliation (#71 firewall held). sim_optimal_manifest now takes typed Vec<(String, Scalar)> and does the lossy f64 collapse internally (one place; the manifest's f64-precursor field owns its own lossiness — the typed-manifest upgrade stays the deferred typed-param-space item, a non-goal here). All eight call sites migrated: three sweep closures pass zip_params, five hand-listing callers (run_sample, run_sample_real, mc_family, run_macd, run_sample_seeded) pass Scalar::F64 literals. Behaviour-preserving: the collapse output is identical to the old per-site zip, so aura sweep / walkforward / run output is byte-identical; SweepFamily.space threaded into the aura-registry optimize test literal. Verified by the orchestrator: cargo test --workspace green (4 new tests: zip_params x2, sweep_family_carries_param_space, family_named_params_round_trips), cargo clippy --workspace --all-targets -D warnings clean. (rust-analyzer emitted stale mid-edit diagnostics; the real cargo build/test is clean.) refs #57
This commit is contained in:
+24
-36
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
mod render;
|
mod render;
|
||||||
|
|
||||||
use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
|
use aura_core::{zip_params, Firing, Scalar, ScalarKind, Timestamp};
|
||||||
use aura_engine::{
|
use aura_engine::{
|
||||||
f64_field, monte_carlo, param_stability, summarize, walk_forward, window_of, Composite, Edge,
|
f64_field, monte_carlo, param_stability, summarize, walk_forward, window_of, Composite, Edge,
|
||||||
FlatGraph, GraphBuilder, Harness, McAggregate, McFamily, RollMode, RunManifest, RunReport,
|
FlatGraph, GraphBuilder, Harness, McAggregate, McFamily, RollMode, RunManifest, RunReport,
|
||||||
@@ -128,10 +128,13 @@ fn sample_harness() -> (
|
|||||||
/// label keeps it a single source (and a single edit point for #22's per-asset
|
/// label keeps it a single source (and a single edit point for #22's per-asset
|
||||||
/// pip work).
|
/// pip work).
|
||||||
fn sim_optimal_manifest(
|
fn sim_optimal_manifest(
|
||||||
params: Vec<(String, f64)>,
|
params: Vec<(String, Scalar)>,
|
||||||
window: (Timestamp, Timestamp),
|
window: (Timestamp, Timestamp),
|
||||||
seed: u64,
|
seed: u64,
|
||||||
) -> RunManifest {
|
) -> RunManifest {
|
||||||
|
// The lossy f64 collapse lives here — the manifest field (the deferred typed
|
||||||
|
// param-space precursor) owns its own lossiness; callers pass typed Scalars.
|
||||||
|
let params = params.into_iter().map(|(n, s)| (n, scalar_as_param_f64(&s))).collect();
|
||||||
RunManifest {
|
RunManifest {
|
||||||
commit: option_env!("AURA_COMMIT").unwrap_or("unknown").to_string(),
|
commit: option_env!("AURA_COMMIT").unwrap_or("unknown").to_string(),
|
||||||
params,
|
params,
|
||||||
@@ -160,9 +163,9 @@ fn run_sample() -> RunReport {
|
|||||||
RunReport {
|
RunReport {
|
||||||
manifest: sim_optimal_manifest(
|
manifest: sim_optimal_manifest(
|
||||||
vec![
|
vec![
|
||||||
("sma_fast".to_string(), 2.0),
|
("sma_fast".to_string(), Scalar::F64(2.0)),
|
||||||
("sma_slow".to_string(), 4.0),
|
("sma_slow".to_string(), Scalar::F64(4.0)),
|
||||||
("exposure_scale".to_string(), 0.5),
|
("exposure_scale".to_string(), Scalar::F64(0.5)),
|
||||||
],
|
],
|
||||||
window,
|
window,
|
||||||
0,
|
0,
|
||||||
@@ -223,9 +226,9 @@ fn run_sample_real(symbol: &str, from_ms: Option<i64>, to_ms: Option<i64>) -> Ru
|
|||||||
RunReport {
|
RunReport {
|
||||||
manifest: sim_optimal_manifest(
|
manifest: sim_optimal_manifest(
|
||||||
vec![
|
vec![
|
||||||
("sma_fast".to_string(), 2.0),
|
("sma_fast".to_string(), Scalar::F64(2.0)),
|
||||||
("sma_slow".to_string(), 4.0),
|
("sma_slow".to_string(), Scalar::F64(4.0)),
|
||||||
("exposure_scale".to_string(), 0.5),
|
("exposure_scale".to_string(), Scalar::F64(0.5)),
|
||||||
],
|
],
|
||||||
window,
|
window,
|
||||||
0,
|
0,
|
||||||
@@ -383,13 +386,8 @@ fn sweep_family() -> SweepFamily {
|
|||||||
h.run(sources);
|
h.run(sources);
|
||||||
let equity = f64_field(&rx_eq.try_iter().collect::<Vec<_>>(), 0);
|
let equity = f64_field(&rx_eq.try_iter().collect::<Vec<_>>(), 0);
|
||||||
let exposure = f64_field(&rx_ex.try_iter().collect::<Vec<_>>(), 0);
|
let exposure = f64_field(&rx_ex.try_iter().collect::<Vec<_>>(), 0);
|
||||||
let params = space
|
|
||||||
.iter()
|
|
||||||
.zip(point)
|
|
||||||
.map(|(ps, v)| (ps.name.clone(), scalar_as_param_f64(v)))
|
|
||||||
.collect();
|
|
||||||
RunReport {
|
RunReport {
|
||||||
manifest: sim_optimal_manifest(params, window, 0),
|
manifest: sim_optimal_manifest(zip_params(&space, point), window, 0),
|
||||||
metrics: summarize(&equity, &exposure),
|
metrics: summarize(&equity, &exposure),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -500,13 +498,8 @@ fn sweep_over(from: Timestamp, to: Timestamp) -> SweepFamily {
|
|||||||
h.run(sources);
|
h.run(sources);
|
||||||
let equity = f64_field(&rx_eq.try_iter().collect::<Vec<_>>(), 0);
|
let equity = f64_field(&rx_eq.try_iter().collect::<Vec<_>>(), 0);
|
||||||
let exposure = f64_field(&rx_ex.try_iter().collect::<Vec<_>>(), 0);
|
let exposure = f64_field(&rx_ex.try_iter().collect::<Vec<_>>(), 0);
|
||||||
let params = space
|
|
||||||
.iter()
|
|
||||||
.zip(point)
|
|
||||||
.map(|(ps, v)| (ps.name.clone(), scalar_as_param_f64(v)))
|
|
||||||
.collect();
|
|
||||||
RunReport {
|
RunReport {
|
||||||
manifest: sim_optimal_manifest(params, window, 0),
|
manifest: sim_optimal_manifest(zip_params(&space, point), window, 0),
|
||||||
metrics: summarize(&equity, &exposure),
|
metrics: summarize(&equity, &exposure),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -527,13 +520,8 @@ fn run_oos(params: &[Scalar], from: Timestamp, to: Timestamp) -> (Vec<(Timestamp
|
|||||||
h.run(sources);
|
h.run(sources);
|
||||||
let equity = f64_field(&rx_eq.try_iter().collect::<Vec<_>>(), 0);
|
let equity = f64_field(&rx_eq.try_iter().collect::<Vec<_>>(), 0);
|
||||||
let exposure = f64_field(&rx_ex.try_iter().collect::<Vec<_>>(), 0);
|
let exposure = f64_field(&rx_ex.try_iter().collect::<Vec<_>>(), 0);
|
||||||
let named = space
|
|
||||||
.iter()
|
|
||||||
.zip(params)
|
|
||||||
.map(|(ps, v)| (ps.name.clone(), scalar_as_param_f64(v)))
|
|
||||||
.collect();
|
|
||||||
let report = RunReport {
|
let report = RunReport {
|
||||||
manifest: sim_optimal_manifest(named, window, 0),
|
manifest: sim_optimal_manifest(zip_params(&space, params), window, 0),
|
||||||
metrics: summarize(&equity, &exposure),
|
metrics: summarize(&equity, &exposure),
|
||||||
};
|
};
|
||||||
(equity, report)
|
(equity, report)
|
||||||
@@ -612,9 +600,9 @@ fn mc_family() -> McFamily {
|
|||||||
RunReport {
|
RunReport {
|
||||||
manifest: sim_optimal_manifest(
|
manifest: sim_optimal_manifest(
|
||||||
vec![
|
vec![
|
||||||
("sma_fast".to_string(), 2.0),
|
("sma_fast".to_string(), Scalar::F64(2.0)),
|
||||||
("sma_slow".to_string(), 4.0),
|
("sma_slow".to_string(), Scalar::F64(4.0)),
|
||||||
("exposure_scale".to_string(), 0.5),
|
("exposure_scale".to_string(), Scalar::F64(0.5)),
|
||||||
],
|
],
|
||||||
window,
|
window,
|
||||||
seed,
|
seed,
|
||||||
@@ -868,10 +856,10 @@ fn run_macd() -> RunReport {
|
|||||||
RunReport {
|
RunReport {
|
||||||
manifest: sim_optimal_manifest(
|
manifest: sim_optimal_manifest(
|
||||||
vec![
|
vec![
|
||||||
("ema_fast".to_string(), 2.0),
|
("ema_fast".to_string(), Scalar::F64(2.0)),
|
||||||
("ema_slow".to_string(), 4.0),
|
("ema_slow".to_string(), Scalar::F64(4.0)),
|
||||||
("ema_signal".to_string(), 3.0),
|
("ema_signal".to_string(), Scalar::F64(3.0)),
|
||||||
("exposure_scale".to_string(), 0.5),
|
("exposure_scale".to_string(), Scalar::F64(0.5)),
|
||||||
],
|
],
|
||||||
window,
|
window,
|
||||||
0,
|
0,
|
||||||
@@ -969,9 +957,9 @@ mod tests {
|
|||||||
let report = RunReport {
|
let report = RunReport {
|
||||||
manifest: sim_optimal_manifest(
|
manifest: sim_optimal_manifest(
|
||||||
vec![
|
vec![
|
||||||
("sma_fast".to_string(), 2.0),
|
("sma_fast".to_string(), Scalar::F64(2.0)),
|
||||||
("sma_slow".to_string(), 4.0),
|
("sma_slow".to_string(), Scalar::F64(4.0)),
|
||||||
("exposure_scale".to_string(), 0.5),
|
("exposure_scale".to_string(), Scalar::F64(0.5)),
|
||||||
],
|
],
|
||||||
window,
|
window,
|
||||||
seed,
|
seed,
|
||||||
|
|||||||
@@ -39,5 +39,7 @@ pub use any::AnyColumn;
|
|||||||
pub use column::{Column, Window};
|
pub use column::{Column, Window};
|
||||||
pub use ctx::Ctx;
|
pub use ctx::Ctx;
|
||||||
pub use error::KindMismatch;
|
pub use error::KindMismatch;
|
||||||
pub use node::{FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder};
|
pub use node::{
|
||||||
|
zip_params, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder,
|
||||||
|
};
|
||||||
pub use scalar::{Scalar, ScalarKind, Timestamp};
|
pub use scalar::{Scalar, ScalarKind, Timestamp};
|
||||||
|
|||||||
@@ -70,6 +70,14 @@ pub struct ParamSpec {
|
|||||||
pub kind: ScalarKind,
|
pub kind: ScalarKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pair each param-space name with the co-indexed positional value — the
|
||||||
|
/// inverse of positional binding (C23): names are a derived view, not identity.
|
||||||
|
/// `space` and `point` are co-indexed in `param_space()` slot order; a length
|
||||||
|
/// mismatch (contract violation) truncates to the shorter, never panics.
|
||||||
|
pub fn zip_params(space: &[ParamSpec], point: &[Scalar]) -> Vec<(String, Scalar)> {
|
||||||
|
space.iter().zip(point).map(|(ps, v)| (ps.name.clone(), *v)).collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// One param bound to a structural constant by [`PrimitiveBuilder::bind`] — a
|
/// One param bound to a structural constant by [`PrimitiveBuilder::bind`] — a
|
||||||
/// render/debug surface only (C23), dropped at lowering. `pos` is the slot's
|
/// render/debug surface only (C23), dropped at lowering. `pos` is the slot's
|
||||||
/// position in the node's ORIGINAL (pre-bind) param list, so the model serializer
|
/// position in the node's ORIGINAL (pre-bind) param list, so the model serializer
|
||||||
@@ -522,3 +530,37 @@ mod tests {
|
|||||||
let _ = b.bind("length", Scalar::F64(2.0)); // F64 value for an I64 slot
|
let _ = b.bind("length", Scalar::F64(2.0)); // F64 value for an I64 slot
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod zip_params_tests {
|
||||||
|
use super::{zip_params, ParamSpec};
|
||||||
|
use crate::{Scalar, ScalarKind};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zip_params_pairs_names_with_values_in_slot_order() {
|
||||||
|
let space = vec![
|
||||||
|
ParamSpec { name: "fast".into(), kind: ScalarKind::I64 },
|
||||||
|
ParamSpec { name: "scale".into(), kind: ScalarKind::F64 },
|
||||||
|
];
|
||||||
|
let point = vec![Scalar::I64(2), Scalar::F64(0.5)];
|
||||||
|
assert_eq!(
|
||||||
|
zip_params(&space, &point),
|
||||||
|
vec![
|
||||||
|
("fast".to_string(), Scalar::I64(2)),
|
||||||
|
("scale".to_string(), Scalar::F64(0.5)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zip_params_matches_hand_zip() {
|
||||||
|
let space = vec![
|
||||||
|
ParamSpec { name: "a".into(), kind: ScalarKind::I64 },
|
||||||
|
ParamSpec { name: "b".into(), kind: ScalarKind::I64 },
|
||||||
|
];
|
||||||
|
let point = vec![Scalar::I64(7), Scalar::I64(9)];
|
||||||
|
let hand: Vec<(String, Scalar)> =
|
||||||
|
space.iter().zip(&point).map(|(ps, v)| (ps.name.clone(), *v)).collect();
|
||||||
|
assert_eq!(zip_params(&space, &point), hand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
//! per-point run-to-metrics closure is the author's (harness-specific sink glue
|
//! per-point run-to-metrics closure is the author's (harness-specific sink glue
|
||||||
//! the engine cannot generically own — C8/C18).
|
//! the engine cannot generically own — C8/C18).
|
||||||
|
|
||||||
use aura_core::{ParamSpec, Scalar, ScalarKind};
|
use aura_core::{zip_params, ParamSpec, Scalar, ScalarKind};
|
||||||
use crate::RunReport;
|
use crate::RunReport;
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
|
|||||||
/// points (C12.1 grid axis).
|
/// points (C12.1 grid axis).
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct GridSpace {
|
pub struct GridSpace {
|
||||||
|
space: Vec<ParamSpec>,
|
||||||
axes: Vec<Vec<Scalar>>,
|
axes: Vec<Vec<Scalar>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ impl GridSpace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(Self { axes })
|
Ok(Self { space: space.to_vec(), axes })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The number of grid points (`∏ |axis_i|`), always `>= 1` (a valid grid has
|
/// The number of grid points (`∏ |axis_i|`), always `>= 1` (a valid grid has
|
||||||
@@ -77,6 +78,12 @@ impl GridSpace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The param-space (names + kinds) this grid was validated against, retained
|
||||||
|
/// for the family to carry — the derived-name source (C23: names, not identity).
|
||||||
|
pub fn param_specs(&self) -> &[ParamSpec] {
|
||||||
|
&self.space
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A structural fault constructing a `GridSpace` — the typed gate before any run.
|
/// A structural fault constructing a `GridSpace` — the typed gate before any run.
|
||||||
@@ -105,9 +112,18 @@ pub struct SweepPoint {
|
|||||||
/// enumeration (odometer) order, independent of thread completion order.
|
/// enumeration (odometer) order, independent of thread completion order.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct SweepFamily {
|
pub struct SweepFamily {
|
||||||
|
pub space: Vec<ParamSpec>,
|
||||||
pub points: Vec<SweepPoint>,
|
pub points: Vec<SweepPoint>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SweepFamily {
|
||||||
|
/// The i-th point's params paired with their names — a derived view over the
|
||||||
|
/// carried param-space (reuses [`zip_params`]); no new per-point state.
|
||||||
|
pub fn named_params(&self, i: usize) -> Vec<(String, Scalar)> {
|
||||||
|
zip_params(&self.space, &self.points[i].params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Run `run_one` over every grid point, disjointly in parallel (C1), and collect
|
/// Run `run_one` over every grid point, disjointly in parallel (C1), and collect
|
||||||
/// the family in enumeration order. `run_one` builds + bootstraps + runs +
|
/// the family in enumeration order. `run_one` builds + bootstraps + runs +
|
||||||
/// summarizes one point; it shares nothing mutable, so it is `Sync` and the runs
|
/// summarizes one point; it shares nothing mutable, so it is `Sync` and the runs
|
||||||
@@ -173,6 +189,7 @@ where
|
|||||||
let points = space.points();
|
let points = space.points();
|
||||||
let reports = run_indexed(points.len(), nthreads, |i| run_one(&points[i]));
|
let reports = run_indexed(points.len(), nthreads, |i| run_one(&points[i]));
|
||||||
SweepFamily {
|
SweepFamily {
|
||||||
|
space: space.param_specs().to_vec(),
|
||||||
points: points
|
points: points
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.zip(reports)
|
.zip(reports)
|
||||||
@@ -325,6 +342,28 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sweep_family_carries_param_space() {
|
||||||
|
let space = composite_sma_cross_harness().0.param_space();
|
||||||
|
let family = sweep(&sma_cross_grid(), run_point);
|
||||||
|
assert_eq!(family.space, space);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn family_named_params_round_trips() {
|
||||||
|
let space = composite_sma_cross_harness().0.param_space();
|
||||||
|
let family = sweep(&sma_cross_grid(), run_point);
|
||||||
|
// odometer-first point is [I64(2), I64(4), F64(0.5)]
|
||||||
|
let expected: Vec<(String, Scalar)> = space
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.zip(family.points[0].params.clone())
|
||||||
|
.map(|(ps, v)| (ps.name, v))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(family.named_params(0), expected);
|
||||||
|
assert_eq!(family.named_params(0)[0].1, Scalar::I64(2));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn family_is_deterministic_across_thread_counts() {
|
fn family_is_deterministic_across_thread_counts() {
|
||||||
let grid = sma_cross_grid();
|
let grid = sma_cross_grid();
|
||||||
|
|||||||
@@ -276,6 +276,7 @@ mod tests {
|
|||||||
report: report_with(pips, 0.5, 0),
|
report: report_with(pips, 0.5, 0),
|
||||||
};
|
};
|
||||||
let family = SweepFamily {
|
let family = SweepFamily {
|
||||||
|
space: vec![],
|
||||||
points: vec![
|
points: vec![
|
||||||
point(0.0, 1.0), // 0: also-ran
|
point(0.0, 1.0), // 0: also-ran
|
||||||
point(10.0, 3.0), // 1: tied max, earliest -> the winner
|
point(10.0, 3.0), // 1: tied max, earliest -> the winner
|
||||||
|
|||||||
Reference in New Issue
Block a user