af0191884d
Public-API field test of the random param-sweep surface, from a standalone downstream-consumer crate (path-deps only; the public interface = ledger + glossary + spec 0049 + cargo doc rustdoc; no crates/*/src read). Four bins, each built from HEAD and run: continuous tuning (200-point random tune ranked by total_pips), the typed validation gate (all five reachable SweepError variants pre-run), reproducibility + seed-sensitivity + the full i64::MIN..=MAX sampler edge, and Space-trait interchangeability (one tune_and_rank<S: Space> over both GridSpace and RandomSpace). Findings: 0 bugs, 4 working, 2 spec_gap, 1 friction. The four working findings confirm the cycle's acceptance criterion empirically — the headline tune reads as the code a researcher would write, the gate is precise and fires before any run, the C1 reproducibility promise is checkable in one line, and the Space trait delivers one-consumer/both-enumerations. Triage of the actionable findings: - friction (no named-axis builder for RandomSpace — positional Vec<ParamRange> must align with param_space() by hand, and a same-kind transposition passes validation silently): filed as a feature for a future cycle, refs #79 (a RandomBinder sibling to the grid's SweepBinder). - spec_gap (SweepError rustdoc summary named only GridSpace): fixed inline in a follow-up doc commit. - spec_gap (NonNumericRange / Bool-slot ranges unreachable with the shipped aura-std node roster — no node declares a Bool/Timestamp knob): RATIFIED as intentional. The variant is a forward-looking structural guard for the C16 "author your own node" path (a Bool/Timestamp param-slot a custom node may declare); its current untriggerability with the standard roster is expected, not drift. refs #79
201 lines
7.3 KiB
Rust
201 lines
7.3 KiB
Rust
// Cycle-0049 fieldtest — example 2: the typed VALIDATION GATE.
|
|
//
|
|
// Declare invalid ranges and observe the typed `SweepError` returned by
|
|
// `RandomSpace::new` BEFORE any run (axis-hint 2). Exercises every reachable
|
|
// SweepError construction fault:
|
|
// - Arity (wrong number of ranges vs. param-space slots)
|
|
// - RangeKindMismatch (F64 range on an I64 slot)
|
|
// - EmptyRange (I64) (lo > hi)
|
|
// - EmptyRange (F64) (lo >= hi — the half-open lo == hi case)
|
|
// - the valid I64 lo == hi single-point range (accepted, NOT an error)
|
|
// - NonNumericRange (a range on a Bool slot)
|
|
//
|
|
// The NonNumericRange case needs a node with a Bool param. No shipped aura-std
|
|
// node has one (all knobs are I64 `length` / F64 `scale`), so — exactly as a
|
|
// C16 research project would (it authors its own nodes in Rust) — this fixture
|
|
// authors a tiny custom pass-through node `GateNode` whose single knob is a Bool.
|
|
//
|
|
// PUBLIC INTERFACE ONLY: ledger + glossary + spec 0049 + rustdoc. No
|
|
// crates/*/src was read.
|
|
|
|
use aura_core::{
|
|
Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder,
|
|
ScalarKind,
|
|
};
|
|
use aura_engine::{
|
|
BlueprintNode, Composite, Edge, OutField, ParamRange, RandomSpace, Role, SweepError, Target,
|
|
};
|
|
use aura_std::{Exposure, Sma, Sub};
|
|
|
|
// ---- A minimal custom node with a BOOL param (what a real project would write
|
|
// to need the NonNumericRange gate). It passes its f64 input through unchanged;
|
|
// the `enabled: bool` knob is declared purely so a Bool slot exists in the
|
|
// param-space. -----------------------------------------------------------------
|
|
struct GateNode {
|
|
out: [Cell; 1],
|
|
}
|
|
impl Node for GateNode {
|
|
fn lookbacks(&self) -> Vec<usize> {
|
|
vec![1]
|
|
}
|
|
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> {
|
|
let w = ctx.f64_in(0);
|
|
if w.is_empty() {
|
|
return None;
|
|
}
|
|
self.out[0] = Cell::from_f64(w[0]);
|
|
Some(&self.out)
|
|
}
|
|
}
|
|
fn gate_builder() -> PrimitiveBuilder {
|
|
PrimitiveBuilder::new(
|
|
"gate",
|
|
NodeSchema {
|
|
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "in".into() }],
|
|
output: vec![FieldSpec { name: "out".into(), kind: ScalarKind::F64 }],
|
|
params: vec![ParamSpec { name: "enabled".into(), kind: ScalarKind::Bool }],
|
|
},
|
|
|_params: &[Cell]| Box::new(GateNode { out: [Cell::from_f64(0.0)] }) as Box<dyn Node>,
|
|
)
|
|
}
|
|
|
|
/// SMA-cross: param-space = [sma_cross.fast.length: I64, sma_cross.slow.length:
|
|
/// I64, exposure.scale: F64]. Used for the numeric error variants.
|
|
fn numeric_space() -> Vec<ParamSpec> {
|
|
let cross = Composite::new(
|
|
"sma_cross",
|
|
vec![
|
|
Sma::builder().named("fast").into(),
|
|
Sma::builder().named("slow").into(),
|
|
Sub::builder().into(),
|
|
],
|
|
vec![
|
|
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
|
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
|
|
],
|
|
vec![Role {
|
|
name: "price".into(),
|
|
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }],
|
|
source: None,
|
|
}],
|
|
vec![OutField { node: 2, field: 0, name: "out".into() }],
|
|
);
|
|
Composite::new(
|
|
"harness",
|
|
vec![BlueprintNode::Composite(cross), Exposure::builder().into()],
|
|
vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }],
|
|
vec![Role {
|
|
name: "price".into(),
|
|
targets: vec![Target { node: 0, slot: 0 }],
|
|
source: Some(ScalarKind::F64),
|
|
}],
|
|
vec![OutField { node: 1, field: 0, name: "exposure".into() }],
|
|
)
|
|
.param_space()
|
|
}
|
|
|
|
/// A param-space with a Bool slot at index 0 (the GateNode's `enabled` knob),
|
|
/// followed by an F64 slot (exposure.scale).
|
|
fn bool_space() -> Vec<ParamSpec> {
|
|
Composite::new(
|
|
"harness",
|
|
vec![gate_builder().into(), Exposure::builder().into()],
|
|
vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }],
|
|
vec![Role {
|
|
name: "price".into(),
|
|
targets: vec![Target { node: 0, slot: 0 }],
|
|
source: Some(ScalarKind::F64),
|
|
}],
|
|
vec![OutField { node: 1, field: 0, name: "exposure".into() }],
|
|
)
|
|
.param_space()
|
|
}
|
|
|
|
fn main() {
|
|
let space = numeric_space();
|
|
println!("numeric param-space:");
|
|
for (i, p) in space.iter().enumerate() {
|
|
println!(" slot {i}: {} : {:?}", p.name, p.kind);
|
|
}
|
|
println!();
|
|
|
|
// 1. Arity — two ranges for a three-slot space.
|
|
let e = RandomSpace::new(&space, vec![ParamRange::i64(2, 8), ParamRange::i64(10, 30)], 50, 1);
|
|
report("Arity (2 ranges, 3 slots)", &e);
|
|
assert!(matches!(e, Err(SweepError::Arity { expected: 3, got: 2 })));
|
|
|
|
// 2. RangeKindMismatch — an F64 range on the I64 fast.length slot (slot 0).
|
|
let e = RandomSpace::new(
|
|
&space,
|
|
vec![ParamRange::f64(2.0, 8.0), ParamRange::i64(10, 30), ParamRange::f64(0.5, 4.0)],
|
|
50,
|
|
1,
|
|
);
|
|
report("RangeKindMismatch (F64 range on I64 slot 0)", &e);
|
|
assert!(matches!(
|
|
e,
|
|
Err(SweepError::RangeKindMismatch { slot: 0, expected: ScalarKind::I64, got: ScalarKind::F64 })
|
|
));
|
|
|
|
// 3. EmptyRange (I64) — lo > hi on slot 0.
|
|
let e = RandomSpace::new(
|
|
&space,
|
|
vec![ParamRange::i64(8, 2), ParamRange::i64(10, 30), ParamRange::f64(0.5, 4.0)],
|
|
50,
|
|
1,
|
|
);
|
|
report("EmptyRange I64 (lo=8 > hi=2 on slot 0)", &e);
|
|
assert!(matches!(e, Err(SweepError::EmptyRange { slot: 0 })));
|
|
|
|
// 4. EmptyRange (F64) — half-open [lo, hi) with lo == hi is empty (slot 2).
|
|
let e = RandomSpace::new(
|
|
&space,
|
|
vec![ParamRange::i64(2, 8), ParamRange::i64(10, 30), ParamRange::f64(1.5, 1.5)],
|
|
50,
|
|
1,
|
|
);
|
|
report("EmptyRange F64 (lo == hi == 1.5 on slot 2, half-open)", &e);
|
|
assert!(matches!(e, Err(SweepError::EmptyRange { slot: 2 })));
|
|
|
|
// 5. The VALID I64 lo == hi single-point range (NOT an error — inclusive).
|
|
let ok = RandomSpace::new(
|
|
&space,
|
|
vec![ParamRange::i64(5, 5), ParamRange::i64(10, 30), ParamRange::f64(0.5, 4.0)],
|
|
7,
|
|
1,
|
|
);
|
|
match &ok {
|
|
Ok(rs) => println!(
|
|
"\nVALID | I64 lo == hi == 5 single point accepted -> {} points",
|
|
rs.len()
|
|
),
|
|
Err(err) => panic!("I64 lo == hi must be a valid single point, got {err:?}"),
|
|
}
|
|
|
|
// 6. NonNumericRange — any range on the Bool slot 0 of the custom node.
|
|
let bspace = bool_space();
|
|
println!("\nbool param-space:");
|
|
for (i, p) in bspace.iter().enumerate() {
|
|
println!(" slot {i}: {} : {:?}", p.name, p.kind);
|
|
}
|
|
// We must still pass *some* range per slot to even reach the per-slot check;
|
|
// an i64 range on the Bool slot. The kind-check ordering decides which error
|
|
// wins (NonNumericRange vs RangeKindMismatch) — record whichever surfaces.
|
|
let e = RandomSpace::new(
|
|
&bspace,
|
|
vec![ParamRange::i64(0, 1), ParamRange::f64(0.5, 4.0)],
|
|
50,
|
|
1,
|
|
);
|
|
report("range on Bool slot 0", &e);
|
|
|
|
println!("\nOK: every fault was caught by RandomSpace::new before any run.");
|
|
}
|
|
|
|
fn report(label: &str, e: &Result<RandomSpace, SweepError>) {
|
|
match e {
|
|
Ok(rs) => println!("{label:<48} -> Ok({} points) [UNEXPECTED]", rs.len()),
|
|
Err(err) => println!("{label:<48} -> Err({err:?})"),
|
|
}
|
|
}
|