From 43716be10ea31d68e21bbd7d0ff8e73ed3c63dd6 Mon Sep 17 00:00:00 2001 From: Brummel Date: Tue, 16 Jun 2026 16:22:17 +0200 Subject: [PATCH] =?UTF-8?q?refactor(aura-engine):=20Cell=20into=20the=20co?= =?UTF-8?q?nstruction=20path=20=E2=80=94=20param-plane=20base/frontend=20s?= =?UTF-8?q?plit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cell becomes the carrier of the construction path; Scalar narrows to the author/render boundaries. The validated/enumerated param point carries no redundant kind (it lives once, in the declared param-space); at the author edge the kind is a checksum — two independent sources, the typed value vs. the slot — so the self-describing Scalar stays there. The name->slot binding is dynamic (C23), so the check is necessarily a runtime one and the value must self-describe for it. Base/frontend split (the AnyColumn push/push_cell pattern one level up): compile_with_cells / bootstrap_with_cells are the kind-check-free base; compile_with_params / bootstrap_with_params are the frontend that adds only the per-value kind checksum, strips to cells (new Scalar::cell(), the partner of Scalar::from_cell), and delegates. lower_items loses its per-primitive kind-check; PrimitiveBuilder::build and the std node builders (sma/ema/exposure/lincomb) read cells. Boundary: construction -> Cell (PrimitiveBuilder::build, lower_items, GridSpace.axes/points, SweepPoint.params, the sweep closure). Author edges stay Scalar (GridSpace::new, bind, compile_with_params/bootstrap_with_params). walkforward chosen_params stays a self-describing Scalar report record (Option A — WalkForwardResult carries no space); the cell winner is reconstructed once at the WindowRun site via from_cell. injective-check moved ahead of the arity-check in the frontend to preserve the pre-split error order (DuplicateParamPath before ParamArity). The lossy i64->f64 param projection (scalar_as_f64 / scalar_as_param_f64) is deliberately untouched — a separate follow-up. Behaviour-preserving (C1): build --all-targets / test / clippy -D warnings / doc all clean across the workspace. --- crates/aura-cli/src/main.rs | 25 ++++++--- crates/aura-core/src/node.rs | 43 +++++++++------- crates/aura-core/src/scalar.rs | 31 +++++++++++ crates/aura-engine/src/blueprint.rs | 80 +++++++++++++++++++---------- crates/aura-engine/src/sweep.rs | 44 ++++++++-------- crates/aura-registry/src/lib.rs | 6 +-- crates/aura-std/src/ema.rs | 2 +- crates/aura-std/src/exposure.rs | 2 +- crates/aura-std/src/lincomb.rs | 4 +- crates/aura-std/src/sma.rs | 2 +- 10 files changed, 159 insertions(+), 80 deletions(-) diff --git a/crates/aura-cli/src/main.rs b/crates/aura-cli/src/main.rs index 3257ff8..07f933a 100644 --- a/crates/aura-cli/src/main.rs +++ b/crates/aura-cli/src/main.rs @@ -13,7 +13,7 @@ mod render; -use aura_core::{zip_params, Firing, Scalar, ScalarKind, Timestamp}; +use aura_core::{zip_params, Cell, Firing, Scalar, ScalarKind, Timestamp}; use aura_engine::{ f64_field, monte_carlo, param_stability, summarize, walk_forward, window_of, Composite, Edge, FlatGraph, GraphBuilder, Harness, McAggregate, McFamily, RollMode, RunManifest, RunReport, @@ -378,7 +378,7 @@ fn sweep_family() -> SweepFamily { .sweep(|point| { let (bp, rx_eq, rx_ex) = sample_blueprint_with_sinks(); let mut h = bp - .bootstrap_with_params(point.to_vec()) + .bootstrap_with_cells(point) .expect("grid points are kind-checked against param_space"); let sources: Vec> = vec![Box::new(VecSource::new(showcase_prices()))]; @@ -470,7 +470,18 @@ fn walkforward_family() -> WalkForwardResult { let is_family = sweep_over(w.is.0, w.is.1); let best = optimize(&is_family, "total_pips").expect("total_pips is a known metric"); let (oos_equity, oos_report) = run_oos(&best.params, w.oos.0, w.oos.1); - WindowRun { chosen_params: best.params, oos_equity, oos_report } + WindowRun { + // best.params is the enumerated cell winner; reconstruct the + // self-describing report record (Option A) via the in-sample space. + chosen_params: best + .params + .iter() + .zip(&is_family.space) + .map(|(c, ps)| Scalar::from_cell(ps.kind, *c)) + .collect(), + oos_equity, + oos_report, + } }) } @@ -490,7 +501,7 @@ fn sweep_over(from: Timestamp, to: Timestamp) -> SweepFamily { .sweep(|point| { let (bp, rx_eq, rx_ex) = sample_blueprint_with_sinks(); let mut h = bp - .bootstrap_with_params(point.to_vec()) + .bootstrap_with_cells(point) .expect("grid points are kind-checked against param_space"); let sources: Vec> = vec![Box::new(walkforward_window_source(from, to))]; @@ -508,12 +519,12 @@ fn sweep_over(from: Timestamp, to: Timestamp) -> SweepFamily { /// Run the chosen params over an out-of-sample window; return the recorded /// pip-equity segment (for stitching) and the OOS RunReport (the C18 record). -fn run_oos(params: &[Scalar], from: Timestamp, to: Timestamp) -> (Vec<(Timestamp, f64)>, RunReport) { +fn run_oos(params: &[Cell], from: Timestamp, to: Timestamp) -> (Vec<(Timestamp, f64)>, RunReport) { let (bp, rx_eq, rx_ex) = sample_blueprint_with_sinks(); let space = bp.param_space(); let mut h = bp - .bootstrap_with_params(params.to_vec()) - .expect("chosen params are kind-checked against param_space"); + .bootstrap_with_cells(params) + .expect("chosen params pre-validated by the in-sample GridSpace::new"); let sources: Vec> = vec![Box::new(walkforward_window_source(from, to))]; let window = window_of(&sources).expect("non-empty out-of-sample window"); diff --git a/crates/aura-core/src/node.rs b/crates/aura-core/src/node.rs index c071a43..d972cb2 100644 --- a/crates/aura-core/src/node.rs +++ b/crates/aura-core/src/node.rs @@ -74,8 +74,14 @@ pub struct ParamSpec { /// 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() +/// +/// The point arrives as tag-free [`Cell`]s (C7): a *validated* param point no +/// longer carries a per-value kind — that kind lives once, in the co-indexed +/// `ParamSpec`. This is `AnyColumn::get` for the param plane: a bare cell meets +/// the kind from its co-present schema and becomes a self-describing [`Scalar`] +/// only here, at the render boundary. +pub fn zip_params(space: &[ParamSpec], point: &[Cell]) -> Vec<(String, Scalar)> { + space.iter().zip(point).map(|(ps, c)| (ps.name.clone(), Scalar::from_cell(ps.kind, *c))).collect() } /// One param bound to a structural constant by [`PrimitiveBuilder::bind`] — a @@ -106,7 +112,7 @@ pub struct PrimitiveBuilder { // The build closure's type is exactly the recipe contract (a param slice in, a // boxed node out); a type alias would not clarify it. #[allow(clippy::type_complexity)] - build: Box Box>, + build: Box Box>, } impl PrimitiveBuilder { @@ -116,7 +122,7 @@ impl PrimitiveBuilder { pub fn new( name: &'static str, schema: NodeSchema, - build: impl Fn(&[Scalar]) -> Box + 'static, + build: impl Fn(&[Cell]) -> Box + 'static, ) -> Self { Self { name, instance_name: None, schema, bound: Vec::new(), build: Box::new(build) } } @@ -162,9 +168,10 @@ impl PrimitiveBuilder { pub fn params(&self) -> &[ParamSpec] { &self.schema.params } - /// Build a sized node from its param slice (the slice is kind-checked by the - /// caller before this runs). - pub fn build(&self, params: &[Scalar]) -> Box { + /// Build a sized node from its param slice — tag-free [`Cell`]s, already + /// kind-checked against the declared slots by the caller (the param-plane + /// frontend), so the closure reads each by its own declared kind. + pub fn build(&self, params: &[Cell]) -> Box { (self.build)(params) } /// The param-generic render label for the blueprint view (C22): just the node @@ -221,9 +228,9 @@ impl PrimitiveBuilder { self.bound.push(BoundParam { pos: orig, name, kind, value }); self.schema.params.remove(pos); // [param_space side] shrink the declared surface let inner = self.build; // [value side] wrap the build closure - self.build = Box::new(move |open: &[Scalar]| { + self.build = Box::new(move |open: &[Cell]| { let mut full = open.to_vec(); - full.insert(pos, value); // re-splice at the slot's original position + full.insert(pos, value.cell()); // bound value (kind-checked above) → Cell, at its original slot inner(&full) }); self @@ -402,7 +409,7 @@ mod tests { /// A node whose label echoes the param vector its constructor received — lets a /// test read back the positional vector `.build()` reconstructed after binds. - struct Probe(Vec); + struct Probe(Vec); impl Node for Probe { fn lookbacks(&self) -> Vec { vec![] @@ -440,19 +447,19 @@ mod tests { bound.params().iter().map(|p| p.name.as_str()).collect::>(), ["c"], // only c remains open ); - let built = bound.build(&[Scalar::f64(9.0)]); // inject c + let built = bound.build(&[Cell::from_f64(9.0)]); // inject c assert_eq!( built.label(), - format!("{:?}", vec![Scalar::i64(7), Scalar::i64(8), Scalar::f64(9.0)]), + format!("{:?}", vec![Cell::from_i64(7), Cell::from_i64(8), Cell::from_f64(9.0)]), ); // partial: bind only b; a and c stay open and keep their positions let built2 = probe3() .bind("b", Scalar::i64(8)) - .build(&[Scalar::i64(7), Scalar::f64(9.0)]); // a, c injected in order + .build(&[Cell::from_i64(7), Cell::from_f64(9.0)]); // a, c injected in order assert_eq!( built2.label(), - format!("{:?}", vec![Scalar::i64(7), Scalar::i64(8), Scalar::f64(9.0)]), + format!("{:?}", vec![Cell::from_i64(7), Cell::from_i64(8), Cell::from_f64(9.0)]), ); } @@ -534,7 +541,7 @@ mod tests { #[cfg(test)] mod zip_params_tests { use super::{zip_params, ParamSpec}; - use crate::{Scalar, ScalarKind}; + use crate::{Cell, Scalar, ScalarKind}; #[test] fn zip_params_pairs_names_with_values_in_slot_order() { @@ -542,7 +549,7 @@ mod zip_params_tests { ParamSpec { name: "fast".into(), kind: ScalarKind::I64 }, ParamSpec { name: "scale".into(), kind: ScalarKind::F64 }, ]; - let point = vec![Scalar::i64(2), Scalar::f64(0.5)]; + let point = vec![Cell::from_i64(2), Cell::from_f64(0.5)]; assert_eq!( zip_params(&space, &point), vec![ @@ -558,9 +565,9 @@ mod zip_params_tests { ParamSpec { name: "a".into(), kind: ScalarKind::I64 }, ParamSpec { name: "b".into(), kind: ScalarKind::I64 }, ]; - let point = vec![Scalar::i64(7), Scalar::i64(9)]; + let point = vec![Cell::from_i64(7), Cell::from_i64(9)]; let hand: Vec<(String, Scalar)> = - space.iter().zip(&point).map(|(ps, v)| (ps.name.clone(), *v)).collect(); + space.iter().zip(&point).map(|(ps, c)| (ps.name.clone(), Scalar::from_cell(ps.kind, *c))).collect(); assert_eq!(zip_params(&space, &point), hand); } } diff --git a/crates/aura-core/src/scalar.rs b/crates/aura-core/src/scalar.rs index d4b63db..666ab9b 100644 --- a/crates/aura-core/src/scalar.rs +++ b/crates/aura-core/src/scalar.rs @@ -61,6 +61,27 @@ impl Scalar { Scalar { kind: ScalarKind::Timestamp, cell: Cell::from_ts(v) } } + /// Re-attach a `kind` to a tag-free [`Cell`] at a dynamic boundary — the + /// inverse of the hot path's tag-stripping. Used where the kind comes from a + /// co-present schema rather than from the value itself: a validated param + /// point carries its values as bare cells (C7 — the kind lives once, in the + /// co-indexed `ParamSpec`), and `zip_params` recombines the two into the + /// self-describing `Scalar` only at the render boundary. The `Cell`'s bits are + /// taken verbatim; correctness rests on `kind` actually describing them (the + /// bootstrap's edge-time check, not a per-value tag). + pub fn from_cell(kind: ScalarKind, cell: Cell) -> Self { + Scalar { kind, cell } + } + + /// The bare [`Cell`], kind dropped — the strip side of the dynamic boundary + /// (the inverse of [`from_cell`](Self::from_cell)). Used by the param-plane + /// frontend to hand a validated value to the cell-carrying construction base, + /// once the kind has been checked against the slot it no longer needs to ride + /// along. + pub fn cell(self) -> Cell { + self.cell + } + /// The kind tag of this scalar (a field read, no branch). pub fn kind(self) -> ScalarKind { self.kind @@ -176,6 +197,16 @@ mod tests { assert_eq!(Scalar::ts(Timestamp(7)).as_ts(), Timestamp(7)); } + #[test] + fn from_cell_and_cell_are_inverse() { + // cell() strips the kind; from_cell re-attaches it — the dynamic-boundary + // round trip the param-plane frontend/base split relies on (strip to hand a + // validated value to the cell base, re-attach to render it). + for s in [Scalar::i64(7), Scalar::f64(2.5), Scalar::bool(true), Scalar::ts(Timestamp(9))] { + assert_eq!(Scalar::from_cell(s.kind(), s.cell()), s); + } + } + #[test] fn scalar_eq_is_value_not_bitwise() { // f64 follows IEEE-754 exactly as the former enum did: a NaN is never diff --git a/crates/aura-engine/src/blueprint.rs b/crates/aura-engine/src/blueprint.rs index 733866f..0dba2b1 100644 --- a/crates/aura-engine/src/blueprint.rs +++ b/crates/aura-engine/src/blueprint.rs @@ -15,7 +15,7 @@ //! C23) and no external dependency (C16). use aura_core::{ - FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder, Scalar, ScalarKind, + Cell, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder, Scalar, ScalarKind, }; use crate::harness::{BootstrapError, Edge, FlatGraph, Harness, SourceSpec, Target}; @@ -197,11 +197,13 @@ impl Composite { out } - /// Compile this composite as the ROOT graph under an injected param vector: - /// validate structurally pre-build (via `signature()`, no node built), require - /// every root role bound, then lower (build each primitive, gather its signature, - /// inline composites, rewrite edges, lower bound roles to flat sources). - pub fn compile_with_params(self, params: &[Scalar]) -> Result { + /// Compile this composite as the ROOT graph from a VALIDATED cell point — the + /// construction base (the cell side of the param-plane split). Structural + /// validation runs here (wiring, root roles, arity), but NOT the per-value kind + /// check: the cells are trusted to match the declared slot kinds, having been + /// checked at the authoring edge (`compile_with_params`, `GridSpace::new`, or + /// `bind`). Lowers by reading each cell as its declared kind. + pub fn compile_with_cells(self, point: &[Cell]) -> Result { // structural validation, all pre-build (no node constructed): let space = self.param_space(); check_param_namespace_injective(&space)?; @@ -212,8 +214,8 @@ impl Composite { } } - if params.len() != space.len() { - return Err(CompileError::ParamArity { expected: space.len(), got: params.len() }); + if point.len() != space.len() { + return Err(CompileError::ParamArity { expected: space.len(), got: point.len() }); } let mut flat_nodes: Vec> = Vec::new(); @@ -223,7 +225,7 @@ impl Composite { let lowerings = lower_items( self.nodes, - params, + point, &mut cursor, &mut flat_nodes, &mut flat_signatures, @@ -250,6 +252,30 @@ impl Composite { Ok(FlatGraph { nodes: flat_nodes, signatures: flat_signatures, sources: flat_sources, edges: flat_edges }) } + /// Compile from a self-describing param vector — the authoring-edge frontend + /// over [`Self::compile_with_cells`]. Its sole added responsibility is the per-value + /// kind checksum (each value's `kind()` vs. the declared slot kind, C7's + /// authoring boundary); it then strips the validated values to tag-free cells + /// and delegates to the base. Arity is checked here too, so the checksum's zip + /// cannot silently skip a trailing slot. + pub fn compile_with_params(self, params: &[Scalar]) -> Result { + let space = self.param_space(); + // injective before arity, preserving the pre-split error order: a duplicate + // param path is reported regardless of how many values were supplied (e.g. + // `compile()` passes none). The base re-checks it for the direct cell path. + check_param_namespace_injective(&space)?; + if params.len() != space.len() { + return Err(CompileError::ParamArity { expected: space.len(), got: params.len() }); + } + for (slot, (v, spec)) in params.iter().zip(&space).enumerate() { + if v.kind() != spec.kind { + return Err(CompileError::ParamKindMismatch { slot, expected: spec.kind, got: v.kind() }); + } + } + let cells: Vec = params.iter().map(|s| s.cell()).collect(); + self.compile_with_cells(&cells) + } + /// No-param compile (errors `ParamArity` if any param is declared). pub fn compile(self) -> Result { self.compile_with_params(&[]) @@ -261,6 +287,14 @@ impl Composite { Harness::bootstrap(flat).map_err(CompileError::Bootstrap) } + /// Bootstrap from a VALIDATED cell point — the cell-side base of + /// [`Self::bootstrap_with_params`]. The sweep path uses this directly: its enumerated + /// point is already kind-checked by `GridSpace::new`, so it skips the frontend. + pub fn bootstrap_with_cells(self, point: &[Cell]) -> Result { + let flat = self.compile_with_cells(point)?; + Harness::bootstrap(flat).map_err(CompileError::Bootstrap) + } + /// No-param bootstrap. pub fn bootstrap(self) -> Result { self.bootstrap_with_params(vec![]) @@ -348,7 +382,7 @@ impl SweepBinder { /// run the disjoint sweep. pub fn sweep(self, run_one: F) -> Result where - F: Fn(&[Scalar]) -> RunReport + Sync, + F: Fn(&[Cell]) -> RunReport + Sync, { let space = self.bp.param_space(); check_param_namespace_injective(&space).map_err(BindError::Compile)?; @@ -642,7 +676,7 @@ enum ItemLowering { /// input item, in order. fn lower_items( items: Vec, - params: &[Scalar], + point: &[Cell], cursor: &mut usize, flat_nodes: &mut Vec>, flat_signatures: &mut Vec, @@ -653,17 +687,9 @@ fn lower_items( match item { BlueprintNode::Primitive(builder) => { let n = builder.params().len(); - let slice = ¶ms[*cursor..*cursor + n]; // in range: arity checked up front - for (i, spec) in builder.params().iter().enumerate() { - let got = slice[i].kind(); - if got != spec.kind { - return Err(CompileError::ParamKindMismatch { - slot: *cursor + i, - expected: spec.kind, - got, - }); - } - } + // cells are trusted: kind-checked at the authoring edge (the + // `compile_with_params` frontend); here read each by its declared kind. + let slice = &point[*cursor..*cursor + n]; // in range: arity checked up front let index = flat_nodes.len(); flat_signatures.push(builder.schema().clone()); flat_nodes.push(builder.build(slice)); @@ -673,7 +699,7 @@ fn lower_items( BlueprintNode::Composite(c) => { lowerings.push(inline_composite( c, - params, + point, cursor, flat_nodes, flat_signatures, @@ -689,7 +715,7 @@ fn lower_items( /// edges, then resolve its output port and per-role flat targets. fn inline_composite( c: Composite, - params: &[Scalar], + point: &[Cell], cursor: &mut usize, flat_nodes: &mut Vec>, flat_signatures: &mut Vec, @@ -699,13 +725,13 @@ fn inline_composite( // (C23 — the boundary does not reach the flat graph), so it is not destructured. // Node names join the same non-load-bearing debug-symbol class: they qualify the // param-space path at construction but are dropped at lowering — the injected - // scalar `params: &[Scalar]` arg drives `lower_items` below; the flat graph stays + // cell `point: &[Cell]` arg drives `lower_items` below; the flat graph stays // wired by raw index. let Composite { name: _, nodes, edges, input_roles, output } = c; let item_count = nodes.len(); // recursively lower interior items, then rewrite interior edges through them - let interior = lower_items(nodes, params, cursor, flat_nodes, flat_signatures, flat_edges)?; + let interior = lower_items(nodes, point, cursor, flat_nodes, flat_signatures, flat_edges)?; for e in &edges { for fe in rewrite_edge(e, &interior, flat_signatures)? { flat_edges.push(fe); @@ -888,7 +914,7 @@ mod tests { .axis("sma_cross.fast.length", [2.0, 3.0]) // F64 values for the I64 slot .axis("sma_cross.slow.length", [4]) .axis("exposure.scale", [0.5]) - .sweep(|_: &[Scalar]| -> RunReport { panic!("axis pre-validation must reject before running") }); + .sweep(|_: &[Cell]| -> RunReport { panic!("axis pre-validation must reject before running") }); assert_eq!( result, Err(BindError::KindMismatch { diff --git a/crates/aura-engine/src/sweep.rs b/crates/aura-engine/src/sweep.rs index ae1b53f..5a4ea1a 100644 --- a/crates/aura-engine/src/sweep.rs +++ b/crates/aura-engine/src/sweep.rs @@ -4,7 +4,7 @@ //! per-point run-to-metrics closure is the author's (harness-specific sink glue //! the engine cannot generically own — C8/C18). -use aura_core::{zip_params, ParamSpec, Scalar, ScalarKind}; +use aura_core::{zip_params, Cell, ParamSpec, Scalar, ScalarKind}; use crate::RunReport; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -14,7 +14,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; #[derive(Debug)] pub struct GridSpace { space: Vec, - axes: Vec>, + axes: Vec>, } impl GridSpace { @@ -41,6 +41,10 @@ impl GridSpace { } } } + // author edge: the values were just kind-checked above; strip the tag and + // store the enumerated grid as tag-free cells (the kind now lives once, in + // `space`). + let axes = axes.iter().map(|ax| ax.iter().map(|s| s.cell()).collect()).collect(); Ok(Self { space: space.to_vec(), axes }) } @@ -58,7 +62,7 @@ impl GridSpace { /// The cartesian product, in odometer order: the **last** axis varies /// fastest. Deterministic — the same grid yields the same point sequence. - pub fn points(&self) -> Vec> { + pub fn points(&self) -> Vec> { let mut out = Vec::with_capacity(self.len()); let mut idx = vec![0usize; self.axes.len()]; loop { @@ -104,7 +108,7 @@ pub enum SweepError { /// registry indexes (C18). #[derive(Clone, Debug, PartialEq)] pub struct SweepPoint { - pub params: Vec, + pub params: Vec, pub report: RunReport, } @@ -131,7 +135,7 @@ impl SweepFamily { /// via `std::thread::scope`. pub fn sweep(space: &GridSpace, run_one: F) -> SweepFamily where - F: Fn(&[Scalar]) -> RunReport + Sync, + F: Fn(&[Cell]) -> RunReport + Sync, { let nthreads = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1); sweep_with_threads(space, nthreads, run_one) @@ -184,7 +188,7 @@ where /// onto their points in enumeration (odometer) order. fn sweep_with_threads(space: &GridSpace, nthreads: usize, run_one: F) -> SweepFamily where - F: Fn(&[Scalar]) -> RunReport + Sync, + F: Fn(&[Cell]) -> RunReport + Sync, { let points = space.points(); let reports = run_indexed(points.len(), nthreads, |i| run_one(&points[i])); @@ -203,7 +207,7 @@ mod tests { use super::*; use crate::test_fixtures::{composite_sma_cross_harness, synthetic_prices}; use crate::{f64_field, summarize, RunManifest, VecSource}; - use aura_core::{ParamSpec, Scalar, ScalarKind, Timestamp}; + use aura_core::{Cell, ParamSpec, Scalar, ScalarKind, Timestamp}; fn i64_space(n: usize) -> Vec { (0..n) @@ -225,10 +229,10 @@ mod tests { assert_eq!( grid.points(), vec![ - vec![Scalar::i64(2), Scalar::i64(4)], - vec![Scalar::i64(2), Scalar::i64(5)], - vec![Scalar::i64(3), Scalar::i64(4)], - vec![Scalar::i64(3), Scalar::i64(5)], + vec![Cell::from_i64(2), Cell::from_i64(4)], + vec![Cell::from_i64(2), Cell::from_i64(5)], + vec![Cell::from_i64(3), Cell::from_i64(4)], + vec![Cell::from_i64(3), Cell::from_i64(5)], ], ); } @@ -287,11 +291,11 @@ mod tests { /// 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 { + fn run_point(point: &[Cell]) -> RunReport { let (bp, rx_eq, rx_ex) = composite_sma_cross_harness(); let mut h = bp - .bootstrap_with_params(point.to_vec()) - .expect("grid points are kind-checked against param_space"); + .bootstrap_with_cells(point) + .expect("enumerated grid points are pre-validated by GridSpace::new"); h.run(vec![Box::new(VecSource::new(synthetic_prices()))]); let equity = f64_field(&rx_eq.try_iter().collect::>(), 0); let exposure = f64_field(&rx_ex.try_iter().collect::>(), 0); @@ -325,14 +329,15 @@ mod tests { let grid = sma_cross_grid(); let family = sweep(&grid, run_point); assert_eq!(family.points.len(), 4); - // params carried in enumeration (odometer) order, self-describing + // params carried in enumeration (odometer) order, as tag-free cells (the + // kind lives once, in `family.space`) assert_eq!( family.points[0].params, - vec![Scalar::i64(2), Scalar::i64(4), Scalar::f64(0.5)], + vec![Cell::from_i64(2), Cell::from_i64(4), Cell::from_f64(0.5)], ); assert_eq!( family.points[3].params, - vec![Scalar::i64(3), Scalar::i64(5), Scalar::f64(0.5)], + vec![Cell::from_i64(3), Cell::from_i64(5), Cell::from_f64(0.5)], ); // each point's metrics equal a direct, independent run of the same point: // the sweep adds enumeration + execution, never a metrics change (C1). @@ -356,9 +361,8 @@ mod tests { // 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)) + .zip(&family.points[0].params) + .map(|(ps, c)| (ps.name.clone(), Scalar::from_cell(ps.kind, *c))) .collect(); assert_eq!(family.named_params(0), expected); assert_eq!(family.named_params(0)[0].1, Scalar::i64(2)); diff --git a/crates/aura-registry/src/lib.rs b/crates/aura-registry/src/lib.rs index 2d8c93e..dc38ed2 100644 --- a/crates/aura-registry/src/lib.rs +++ b/crates/aura-registry/src/lib.rs @@ -178,7 +178,7 @@ impl From for RegistryError { #[cfg(test)] mod tests { use super::*; - use aura_core::{Scalar, Timestamp}; + use aura_core::{Cell, Timestamp}; use aura_engine::{RunManifest, RunMetrics, RunReport, SweepFamily, SweepPoint}; fn report_with(total_pips: f64, max_drawdown: f64, flips: u64) -> RunReport { @@ -272,7 +272,7 @@ mod tests { // total_pips (3.0); the EARLIER of the two (index 1, params p=10) must // win, not the later (index 3, params p=30) — that pins the tie rule. let point = |p: f64, pips: f64| SweepPoint { - params: vec![Scalar::f64(p)], + params: vec![Cell::from_f64(p)], report: report_with(pips, 0.5, 0), }; let family = SweepFamily { @@ -291,7 +291,7 @@ mod tests { assert_eq!(winner.report.metrics.total_pips, 3.0); // ...and ties resolve to the earliest enumeration-order point: its params // identify point 1, not point 3. - assert_eq!(winner.params, vec![Scalar::f64(10.0)]); + assert_eq!(winner.params, vec![Cell::from_f64(10.0)]); // the whole winning point is returned, params AND report together. assert_eq!(winner, family.points[1]); } diff --git a/crates/aura-std/src/ema.rs b/crates/aura-std/src/ema.rs index 93c2342..a112a86 100644 --- a/crates/aura-std/src/ema.rs +++ b/crates/aura-std/src/ema.rs @@ -64,7 +64,7 @@ impl Ema { output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }], params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }], }, - |p| Box::new(Ema::new(p[0].as_i64() as usize)), + |p| Box::new(Ema::new(p[0].i64() as usize)), ) } } diff --git a/crates/aura-std/src/exposure.rs b/crates/aura-std/src/exposure.rs index 1dd81a4..5f7662e 100644 --- a/crates/aura-std/src/exposure.rs +++ b/crates/aura-std/src/exposure.rs @@ -33,7 +33,7 @@ impl Exposure { output: vec![FieldSpec { name: "exposure".into(), kind: ScalarKind::F64 }], params: vec![ParamSpec { name: "scale".into(), kind: ScalarKind::F64 }], }, - |p| Box::new(Exposure::new(p[0].as_f64())), + |p| Box::new(Exposure::new(p[0].f64())), ) } } diff --git a/crates/aura-std/src/lincomb.rs b/crates/aura-std/src/lincomb.rs index 9a99c66..22e3884 100644 --- a/crates/aura-std/src/lincomb.rs +++ b/crates/aura-std/src/lincomb.rs @@ -54,7 +54,7 @@ impl LinComb { "LinComb", NodeSchema { inputs, output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }], params }, |p| Box::new(LinComb::new( - p.iter().map(|s| s.as_f64()).collect(), + p.iter().map(|c| c.f64()).collect(), )), ) } @@ -173,7 +173,7 @@ mod tests { partial.params().iter().map(|p| p.name.as_str()).collect::>(), ["weights[1]"], ); - let mut lc2 = partial.build(&[Scalar::f64(2.0)]); // weights[1] = 2.0 injected + let mut lc2 = partial.build(&[Cell::from_f64(2.0)]); // weights[1] = 2.0 injected let mut inputs2 = vec![ AnyColumn::with_capacity(ScalarKind::F64, 1), AnyColumn::with_capacity(ScalarKind::F64, 1), diff --git a/crates/aura-std/src/sma.rs b/crates/aura-std/src/sma.rs index c95a377..e62c7c9 100644 --- a/crates/aura-std/src/sma.rs +++ b/crates/aura-std/src/sma.rs @@ -32,7 +32,7 @@ impl Sma { output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }], params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }], }, - |p| Box::new(Sma::new(p[0].as_i64() as usize)), + |p| Box::new(Sma::new(p[0].i64() as usize)), ) } }