refactor(aura-engine): Cell into the construction path — param-plane base/frontend split
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.
This commit is contained in:
@@ -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<Box<dyn aura_engine::Source>> =
|
||||
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<Box<dyn aura_engine::Source>> =
|
||||
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<Box<dyn aura_engine::Source>> =
|
||||
vec![Box::new(walkforward_window_source(from, to))];
|
||||
let window = window_of(&sources).expect("non-empty out-of-sample window");
|
||||
|
||||
@@ -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<dyn Fn(&[Scalar]) -> Box<dyn Node>>,
|
||||
build: Box<dyn Fn(&[Cell]) -> Box<dyn Node>>,
|
||||
}
|
||||
|
||||
impl PrimitiveBuilder {
|
||||
@@ -116,7 +122,7 @@ impl PrimitiveBuilder {
|
||||
pub fn new(
|
||||
name: &'static str,
|
||||
schema: NodeSchema,
|
||||
build: impl Fn(&[Scalar]) -> Box<dyn Node> + 'static,
|
||||
build: impl Fn(&[Cell]) -> Box<dyn Node> + '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<dyn Node> {
|
||||
/// 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<dyn Node> {
|
||||
(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<Scalar>);
|
||||
struct Probe(Vec<Cell>);
|
||||
impl Node for Probe {
|
||||
fn lookbacks(&self) -> Vec<usize> {
|
||||
vec![]
|
||||
@@ -440,19 +447,19 @@ mod tests {
|
||||
bound.params().iter().map(|p| p.name.as_str()).collect::<Vec<_>>(),
|
||||
["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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<FlatGraph, CompileError> {
|
||||
/// 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<FlatGraph, CompileError> {
|
||||
// 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<Box<dyn Node>> = 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<FlatGraph, CompileError> {
|
||||
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<Cell> = 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<FlatGraph, CompileError> {
|
||||
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<Harness, CompileError> {
|
||||
let flat = self.compile_with_cells(point)?;
|
||||
Harness::bootstrap(flat).map_err(CompileError::Bootstrap)
|
||||
}
|
||||
|
||||
/// No-param bootstrap.
|
||||
pub fn bootstrap(self) -> Result<Harness, CompileError> {
|
||||
self.bootstrap_with_params(vec![])
|
||||
@@ -348,7 +382,7 @@ impl SweepBinder {
|
||||
/// run the disjoint sweep.
|
||||
pub fn sweep<F>(self, run_one: F) -> Result<SweepFamily, BindError>
|
||||
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<BlueprintNode>,
|
||||
params: &[Scalar],
|
||||
point: &[Cell],
|
||||
cursor: &mut usize,
|
||||
flat_nodes: &mut Vec<Box<dyn Node>>,
|
||||
flat_signatures: &mut Vec<NodeSchema>,
|
||||
@@ -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<Box<dyn Node>>,
|
||||
flat_signatures: &mut Vec<NodeSchema>,
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<ParamSpec>,
|
||||
axes: Vec<Vec<Scalar>>,
|
||||
axes: Vec<Vec<Cell>>,
|
||||
}
|
||||
|
||||
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<Vec<Scalar>> {
|
||||
pub fn points(&self) -> Vec<Vec<Cell>> {
|
||||
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<Scalar>,
|
||||
pub params: Vec<Cell>,
|
||||
pub report: RunReport,
|
||||
}
|
||||
|
||||
@@ -131,7 +135,7 @@ impl SweepFamily {
|
||||
/// via `std::thread::scope`.
|
||||
pub fn sweep<F>(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<F>(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<ParamSpec> {
|
||||
(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::<Vec<_>>(), 0);
|
||||
let exposure = f64_field(&rx_ex.try_iter().collect::<Vec<_>>(), 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));
|
||||
|
||||
@@ -178,7 +178,7 @@ impl From<std::io::Error> 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]);
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::<Vec<_>>(),
|
||||
["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),
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user