feat(aura-core,aura-engine,aura-cli): node-instance naming retires ParamAlias
Every blueprint node now carries a name. A primitive builder gains an optional
instance name (Sma::builder().named("fast")); when omitted it defaults to the
node's type label, ASCII-lowercased verbatim ("SMA"->"sma", "SimBroker"->
"simbroker"). param_space() is now uniformly <node>.<param> at every level
including the root (sma_cross.fast.length, exposure.scale). Fan-in
distinguishability (C9) and signature_of re-key onto the node name: two same-type
siblings both defaulting to "sma" collide and IndistinguishableFanIn fires, so
one act -- naming the legs "fast"/"slow" -- fixes both the naming collision and
the fan-in check. The index-addressed ParamAlias overlay, Composite.params, the
Composite::new 5th argument, aliases_on, and check_alias_indices are removed
(Role.name and OutField.name untouched). Ledger C9 and C23 amended.
This is why the change is correct, not merely convenient: blueprints are
value-empty (C11), so the thing that would otherwise distinguish two SMAs --
their length -- is not present at construction; identity must come from a name,
not a deferred value. Closes the #56 friction surfaced by the param-space &
sweep milestone fieldtest (a freshly-authored 2-SMA cross was unbindable and
would not bootstrap without two hand-counted ParamAlias overlays).
Two plan defects were corrected during implementation and verified:
- the three new fan-in/path tests authored sma_cross at root level, which would
qualify to "fast.length" (root prefix is empty) and is not the nested case the
fan-in check inspects; nesting sma_cross under a root (a shared
sma_cross_under_root helper) restores the asserted "sma_cross.fast.length" and
IndistinguishableFanIn{node:2};
- three cycle-0030 named-binder tests bound the real harness by the old names
(sma_cross.fast / scale) and were migrated to the new path strings, surfaced by
the workspace test gate.
Verified by the orchestrator: cargo build/test --workspace green (198 tests,
0 red), clippy --all-targets -D warnings clean. model_to_json emits the type
label + factory param names (node-name-independent), so sample-model.json and the
graph_model golden are byte-identical (untouched). NodeSchema gained a Default
derive (the builder default-name test constructs an empty schema). fieldtests/
are frozen non-workspace records, not migrated.
closes #56
This commit is contained in:
+34
-40
@@ -11,7 +11,7 @@ mod render;
|
|||||||
use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
|
use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
|
||||||
use aura_engine::{
|
use aura_engine::{
|
||||||
f64_field, summarize, BlueprintNode, Composite, Edge, FlatGraph, Harness,
|
f64_field, summarize, BlueprintNode, Composite, Edge, FlatGraph, Harness,
|
||||||
OutField, ParamAlias, Role, RunManifest, RunReport, SourceSpec, SweepFamily, Target,
|
OutField, Role, RunManifest, RunReport, SourceSpec, SweepFamily, Target,
|
||||||
};
|
};
|
||||||
use aura_registry::{rank_by, Registry};
|
use aura_registry::{rank_by, Registry};
|
||||||
use aura_std::{Ema, Exposure, Recorder, SimBroker, Sma, Sub};
|
use aura_std::{Ema, Exposure, Recorder, SimBroker, Sma, Sub};
|
||||||
@@ -136,7 +136,11 @@ fn run_sample() -> RunReport {
|
|||||||
fn sma_cross(name: &str) -> Composite {
|
fn sma_cross(name: &str) -> Composite {
|
||||||
Composite::new(
|
Composite::new(
|
||||||
name,
|
name,
|
||||||
vec![Sma::builder().into(), Sma::builder().into(), Sub::builder().into()],
|
vec![
|
||||||
|
Sma::builder().named("fast").into(), // fast SMA leg
|
||||||
|
Sma::builder().named("slow").into(), // slow SMA leg
|
||||||
|
Sub::builder().into(),
|
||||||
|
],
|
||||||
vec![
|
vec![
|
||||||
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
||||||
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
|
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
|
||||||
@@ -146,10 +150,6 @@ fn sma_cross(name: &str) -> Composite {
|
|||||||
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }],
|
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }],
|
||||||
source: None,
|
source: None,
|
||||||
}],
|
}],
|
||||||
vec![
|
|
||||||
ParamAlias { name: "fast".into(), node: 0, slot: 0 }, // fast SMA length
|
|
||||||
ParamAlias { name: "slow".into(), node: 1, slot: 0 }, // slow SMA length
|
|
||||||
],
|
|
||||||
vec![OutField { node: 2, field: 0, name: "cross".into() }],
|
vec![OutField { node: 2, field: 0, name: "cross".into() }],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -190,7 +190,6 @@ fn sample_blueprint_with_sinks() -> (
|
|||||||
],
|
],
|
||||||
source: Some(ScalarKind::F64),
|
source: Some(ScalarKind::F64),
|
||||||
}],
|
}],
|
||||||
vec![], // params: the interior sma_cross carries the aliases
|
|
||||||
vec![], // output: the root ends in sinks, no re-export
|
vec![], // output: the root ends in sinks, no re-export
|
||||||
);
|
);
|
||||||
(bp, rx_eq, rx_ex)
|
(bp, rx_eq, rx_ex)
|
||||||
@@ -226,9 +225,9 @@ fn scalar_as_param_f64(s: &Scalar) -> f64 {
|
|||||||
fn sweep_family() -> SweepFamily {
|
fn sweep_family() -> SweepFamily {
|
||||||
let bp = sample_blueprint_with_sinks().0;
|
let bp = sample_blueprint_with_sinks().0;
|
||||||
let space = bp.param_space();
|
let space = bp.param_space();
|
||||||
bp.axis("sma_cross.fast", [2, 3])
|
bp.axis("sma_cross.fast.length", [2, 3])
|
||||||
.axis("sma_cross.slow", [4, 5])
|
.axis("sma_cross.slow.length", [4, 5])
|
||||||
.axis("scale", [0.5])
|
.axis("exposure.scale", [0.5])
|
||||||
.sweep(|point| {
|
.sweep(|point| {
|
||||||
let (bp, rx_eq, rx_ex) = sample_blueprint_with_sinks();
|
let (bp, rx_eq, rx_ex) = sample_blueprint_with_sinks();
|
||||||
let mut h = bp
|
let mut h = bp
|
||||||
@@ -338,11 +337,11 @@ fn macd(name: &str) -> Composite {
|
|||||||
Composite::new(
|
Composite::new(
|
||||||
name,
|
name,
|
||||||
vec![
|
vec![
|
||||||
Ema::builder().into(), // 0 fast EMA
|
Ema::builder().named("fast").into(), // 0 fast EMA
|
||||||
Ema::builder().into(), // 1 slow EMA
|
Ema::builder().named("slow").into(), // 1 slow EMA
|
||||||
Sub::builder().into(), // 2 MACD line = fast − slow
|
Sub::builder().into(), // 2 MACD line = fast − slow
|
||||||
Ema::builder().into(), // 3 signal EMA of the MACD line
|
Ema::builder().named("signal").into(), // 3 signal EMA of the MACD line
|
||||||
Sub::builder().into(), // 4 histogram = MACD line − signal
|
Sub::builder().into(), // 4 histogram = MACD line − signal
|
||||||
],
|
],
|
||||||
vec![
|
vec![
|
||||||
Edge { from: 0, to: 2, slot: 0, from_field: 0 }, // fast → line[0]
|
Edge { from: 0, to: 2, slot: 0, from_field: 0 }, // fast → line[0]
|
||||||
@@ -359,11 +358,6 @@ fn macd(name: &str) -> Composite {
|
|||||||
],
|
],
|
||||||
source: None,
|
source: None,
|
||||||
}],
|
}],
|
||||||
vec![
|
|
||||||
ParamAlias { name: "fast".into(), node: 0, slot: 0 }, // fast EMA length
|
|
||||||
ParamAlias { name: "slow".into(), node: 1, slot: 0 }, // slow EMA length
|
|
||||||
ParamAlias { name: "signal".into(), node: 3, slot: 0 }, // signal EMA length
|
|
||||||
],
|
|
||||||
vec![
|
vec![
|
||||||
OutField { node: 2, field: 0, name: "macd".into() }, // the MACD line
|
OutField { node: 2, field: 0, name: "macd".into() }, // the MACD line
|
||||||
OutField { node: 3, field: 0, name: "signal".into() }, // the signal line
|
OutField { node: 3, field: 0, name: "signal".into() }, // the signal line
|
||||||
@@ -402,7 +396,6 @@ fn macd_strategy_blueprint(
|
|||||||
],
|
],
|
||||||
source: Some(ScalarKind::F64),
|
source: Some(ScalarKind::F64),
|
||||||
}],
|
}],
|
||||||
vec![], // params: the interior macd carries the aliases
|
|
||||||
vec![], // output: the root ends in sinks, no re-export
|
vec![], // output: the root ends in sinks, no re-export
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -513,9 +506,9 @@ mod tests {
|
|||||||
// so a caller can bootstrap one point, run it, and drain both sinks.
|
// so a caller can bootstrap one point, run it, and drain both sinks.
|
||||||
let (bp, rx_eq, rx_ex) = sample_blueprint_with_sinks();
|
let (bp, rx_eq, rx_ex) = sample_blueprint_with_sinks();
|
||||||
let mut h = bp
|
let mut h = bp
|
||||||
.with("sma_cross.fast", 2)
|
.with("sma_cross.fast.length", 2)
|
||||||
.with("sma_cross.slow", 4)
|
.with("sma_cross.slow.length", 4)
|
||||||
.with("scale", 0.5)
|
.with("exposure.scale", 0.5)
|
||||||
.bootstrap()
|
.bootstrap()
|
||||||
.expect("sample blueprint compiles under a valid point");
|
.expect("sample blueprint compiles under a valid point");
|
||||||
h.run(vec![synthetic_prices()]);
|
h.run(vec![synthetic_prices()]);
|
||||||
@@ -534,10 +527,10 @@ mod tests {
|
|||||||
for line in &lines {
|
for line in &lines {
|
||||||
assert!(line.starts_with(r#"{"manifest":{"commit":""#), "not a RunReport: {line}");
|
assert!(line.starts_with(r#"{"manifest":{"commit":""#), "not a RunReport: {line}");
|
||||||
}
|
}
|
||||||
assert!(lines[0].contains(r#""params":{"sma_cross.fast":2,"sma_cross.slow":4,"scale":0.5}"#), "line0: {}", lines[0]);
|
assert!(lines[0].contains(r#""params":{"sma_cross.fast.length":2,"sma_cross.slow.length":4,"exposure.scale":0.5}"#), "line0: {}", lines[0]);
|
||||||
assert!(lines[1].contains(r#""params":{"sma_cross.fast":2,"sma_cross.slow":5,"scale":0.5}"#), "line1: {}", lines[1]);
|
assert!(lines[1].contains(r#""params":{"sma_cross.fast.length":2,"sma_cross.slow.length":5,"exposure.scale":0.5}"#), "line1: {}", lines[1]);
|
||||||
assert!(lines[2].contains(r#""params":{"sma_cross.fast":3,"sma_cross.slow":4,"scale":0.5}"#), "line2: {}", lines[2]);
|
assert!(lines[2].contains(r#""params":{"sma_cross.fast.length":3,"sma_cross.slow.length":4,"exposure.scale":0.5}"#), "line2: {}", lines[2]);
|
||||||
assert!(lines[3].contains(r#""params":{"sma_cross.fast":3,"sma_cross.slow":5,"scale":0.5}"#), "line3: {}", lines[3]);
|
assert!(lines[3].contains(r#""params":{"sma_cross.fast.length":3,"sma_cross.slow.length":5,"exposure.scale":0.5}"#), "line3: {}", lines[3]);
|
||||||
for line in &lines {
|
for line in &lines {
|
||||||
assert!(line.contains(r#""total_pips":"#), "missing total_pips: {line}");
|
assert!(line.contains(r#""total_pips":"#), "missing total_pips: {line}");
|
||||||
assert!(line.contains(r#""max_drawdown":"#), "missing max_drawdown: {line}");
|
assert!(line.contains(r#""max_drawdown":"#), "missing max_drawdown: {line}");
|
||||||
@@ -577,24 +570,25 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// E2E acceptance (#41 / spec 0019, the worked example): the real MACD strategy
|
/// E2E acceptance (#41 / spec 0019, the worked example): the real MACD strategy
|
||||||
/// blueprint's swept param surface relabels the three otherwise-indistinguishable
|
/// blueprint's swept param surface qualifies the three otherwise-indistinguishable
|
||||||
/// EMA `length` slots to `macd.fast` / `macd.slow` / `macd.signal` — the named
|
/// EMA `length` slots by node name to `macd.fast.length` / `macd.slow.length` /
|
||||||
/// composite boundary visible end-to-end through `param_space()`, with the slot
|
/// `macd.signal.length` — the named composite boundary visible end-to-end through
|
||||||
/// count and order unchanged (C23 — pure naming overlay, not curation: every
|
/// `param_space()`, with the slot count and order unchanged (C23 — node names are
|
||||||
/// interior slot stays sweepable, the `scale` knob is unaffected).
|
/// non-load-bearing: every interior slot stays sweepable, the `exposure.scale`
|
||||||
|
/// knob is unaffected).
|
||||||
#[test]
|
#[test]
|
||||||
fn macd_param_space_surfaces_the_three_named_aliases() {
|
fn macd_param_space_surfaces_the_three_named_legs() {
|
||||||
let names: Vec<String> =
|
let names: Vec<String> =
|
||||||
macd_blueprint().param_space().into_iter().map(|p| p.name).collect();
|
macd_blueprint().param_space().into_iter().map(|p| p.name).collect();
|
||||||
// three aliased composite slots, in declared (fast, slow, signal) order,
|
// three named composite-interior slots, in declared (fast, slow, signal)
|
||||||
// then the strategy-level Exposure `scale` (outside the composite, unaliased).
|
// order, then the strategy-level Exposure `scale` (a root-level leaf).
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
names,
|
names,
|
||||||
vec![
|
vec![
|
||||||
"macd.fast".to_string(),
|
"macd.fast.length".to_string(),
|
||||||
"macd.slow".to_string(),
|
"macd.slow.length".to_string(),
|
||||||
"macd.signal".to_string(),
|
"macd.signal.length".to_string(),
|
||||||
"scale".to_string(),
|
"exposure.scale".to_string(),
|
||||||
],
|
],
|
||||||
"MACD param surface must expose the three named EMA lengths + scale",
|
"MACD param surface must expose the three named EMA lengths + scale",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ fn sweep_prints_four_json_lines_and_exits_zero() {
|
|||||||
// pin the first odometer point's manifest params, not the commit value.
|
// pin the first odometer point's manifest params, not the commit value.
|
||||||
assert!(first.starts_with("{\"manifest\":{\"commit\":\""), "got: {stdout}");
|
assert!(first.starts_with("{\"manifest\":{\"commit\":\""), "got: {stdout}");
|
||||||
assert!(
|
assert!(
|
||||||
first.contains("\"params\":{\"sma_cross.fast\":2,\"sma_cross.slow\":4,\"scale\":0.5}"),
|
first.contains("\"params\":{\"sma_cross.fast.length\":2,\"sma_cross.slow.length\":4,\"exposure.scale\":0.5}"),
|
||||||
"got: {stdout}"
|
"got: {stdout}"
|
||||||
);
|
);
|
||||||
let _ = std::fs::remove_dir_all(&cwd);
|
let _ = std::fs::remove_dir_all(&cwd);
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ pub struct ParamSpec {
|
|||||||
/// pre-build (#43).
|
/// pre-build (#43).
|
||||||
pub struct PrimitiveBuilder {
|
pub struct PrimitiveBuilder {
|
||||||
name: &'static str,
|
name: &'static str,
|
||||||
|
instance_name: Option<String>,
|
||||||
schema: NodeSchema,
|
schema: NodeSchema,
|
||||||
// The build closure's type is exactly the recipe contract (a param slice in, a
|
// 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.
|
// boxed node out); a type alias would not clarify it.
|
||||||
@@ -92,7 +93,20 @@ impl PrimitiveBuilder {
|
|||||||
schema: NodeSchema,
|
schema: NodeSchema,
|
||||||
build: impl Fn(&[Scalar]) -> Box<dyn Node> + 'static,
|
build: impl Fn(&[Scalar]) -> Box<dyn Node> + 'static,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self { name, schema, build: Box::new(build) }
|
Self { name, instance_name: None, schema, build: Box::new(build) }
|
||||||
|
}
|
||||||
|
/// Set this node instance's name. Must be non-empty.
|
||||||
|
pub fn named(mut self, name: &str) -> Self {
|
||||||
|
debug_assert!(!name.is_empty(), "node name must be non-empty");
|
||||||
|
self.instance_name = Some(name.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// The resolved node name: the explicit instance name if set, else the
|
||||||
|
/// type label ASCII-lowercased verbatim (e.g. "SimBroker" -> "simbroker").
|
||||||
|
pub fn node_name(&self) -> String {
|
||||||
|
self.instance_name
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| self.name.to_ascii_lowercase())
|
||||||
}
|
}
|
||||||
/// The full declared signature (read pre-build by `Composite::param_space`,
|
/// The full declared signature (read pre-build by `Composite::param_space`,
|
||||||
/// `BlueprintNode::signature`, and the renderer).
|
/// `BlueprintNode::signature`, and the renderer).
|
||||||
@@ -120,7 +134,7 @@ impl PrimitiveBuilder {
|
|||||||
/// case), and an **empty** `output` (`vec![]`) declares a **pure consumer**
|
/// case), and an **empty** `output` (`vec![]`) declares a **pure consumer**
|
||||||
/// (sink role, C8) — a node with no output port. Built once at wiring, never on
|
/// (sink role, C8) — a node with no output port. Built once at wiring, never on
|
||||||
/// the hot path — the `Vec`s are fine here.
|
/// the hot path — the `Vec`s are fine here.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||||
pub struct NodeSchema {
|
pub struct NodeSchema {
|
||||||
pub inputs: Vec<PortSpec>,
|
pub inputs: Vec<PortSpec>,
|
||||||
pub output: Vec<FieldSpec>,
|
pub output: Vec<FieldSpec>,
|
||||||
@@ -215,6 +229,26 @@ mod tests {
|
|||||||
assert_eq!(none.label(), "Sub");
|
assert_eq!(none.label(), "Sub");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn node_name_defaults_to_lowercased_type_label() {
|
||||||
|
// unnamed → the type label, ASCII-lowercased verbatim (no snake_case)
|
||||||
|
let unnamed = PrimitiveBuilder::new(
|
||||||
|
"SimBroker",
|
||||||
|
NodeSchema::default(),
|
||||||
|
|_| panic!("not built in this test"),
|
||||||
|
);
|
||||||
|
assert_eq!(unnamed.node_name(), "simbroker");
|
||||||
|
|
||||||
|
// explicit name overrides
|
||||||
|
let named = PrimitiveBuilder::new(
|
||||||
|
"SMA",
|
||||||
|
NodeSchema::default(),
|
||||||
|
|_| panic!("not built in this test"),
|
||||||
|
)
|
||||||
|
.named("fast");
|
||||||
|
assert_eq!(named.node_name(), "fast");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn primitive_builder_build_runs_the_closure() {
|
fn primitive_builder_build_runs_the_closure() {
|
||||||
let f = PrimitiveBuilder::new(
|
let f = PrimitiveBuilder::new(
|
||||||
|
|||||||
+186
-369
@@ -87,7 +87,7 @@ fn derive_signature(c: &Composite) -> NodeSchema {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let mut params = Vec::new();
|
let mut params = Vec::new();
|
||||||
collect_params(c.nodes(), "", c.params(), &mut params);
|
collect_params(c.nodes(), "", &mut params);
|
||||||
NodeSchema { inputs, output, params }
|
NodeSchema { inputs, output, params }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,17 +118,6 @@ pub struct Role {
|
|||||||
pub source: Option<ScalarKind>,
|
pub source: Option<ScalarKind>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A composite-level alias relabelling one interior leaf param slot's surface
|
|
||||||
/// name in `param_space()`. `node` is the interior item index, `slot` the param
|
|
||||||
/// slot within that leaf. Pure legibility: the alias relabels in place and never
|
|
||||||
/// reorders, adds, or removes a slot (C23 — identity stays the slot).
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub struct ParamAlias {
|
|
||||||
pub name: String,
|
|
||||||
pub node: usize,
|
|
||||||
pub slot: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A reusable sub-graph fragment compiled away by inlining (C9/C23). It is **not**
|
/// A reusable sub-graph fragment compiled away by inlining (C9/C23). It is **not**
|
||||||
/// a [`Node`]: it is never `eval`'d. It holds interior items (local indices),
|
/// a [`Node`]: it is never `eval`'d. It holds interior items (local indices),
|
||||||
/// interior edges (local indices), input roles (role `r` fans into the interior
|
/// interior edges (local indices), input roles (role `r` fans into the interior
|
||||||
@@ -139,7 +128,6 @@ pub struct Composite {
|
|||||||
nodes: Vec<BlueprintNode>,
|
nodes: Vec<BlueprintNode>,
|
||||||
edges: Vec<Edge>,
|
edges: Vec<Edge>,
|
||||||
input_roles: Vec<Role>,
|
input_roles: Vec<Role>,
|
||||||
params: Vec<ParamAlias>,
|
|
||||||
output: Vec<OutField>,
|
output: Vec<OutField>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,10 +141,9 @@ impl Composite {
|
|||||||
nodes: Vec<BlueprintNode>,
|
nodes: Vec<BlueprintNode>,
|
||||||
edges: Vec<Edge>,
|
edges: Vec<Edge>,
|
||||||
input_roles: Vec<Role>,
|
input_roles: Vec<Role>,
|
||||||
params: Vec<ParamAlias>,
|
|
||||||
output: Vec<OutField>,
|
output: Vec<OutField>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self { name: name.into(), nodes, edges, input_roles, params, output }
|
Self { name: name.into(), nodes, edges, input_roles, output }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The authored render name (cluster title, #13). Non-load-bearing.
|
/// The authored render name (cluster title, #13). Non-load-bearing.
|
||||||
@@ -177,11 +164,6 @@ impl Composite {
|
|||||||
pub fn input_roles(&self) -> &[Role] {
|
pub fn input_roles(&self) -> &[Role] {
|
||||||
&self.input_roles
|
&self.input_roles
|
||||||
}
|
}
|
||||||
/// The param aliases: each relabels one interior leaf param slot's surface
|
|
||||||
/// name in `param_space()` (pure naming overlay; identity stays the slot, C23).
|
|
||||||
pub fn params(&self) -> &[ParamAlias] {
|
|
||||||
&self.params
|
|
||||||
}
|
|
||||||
/// The exposed output record: each entry re-exports one interior
|
/// The exposed output record: each entry re-exports one interior
|
||||||
/// `(node, output-field)` under a boundary name (C8 — one port, K columns).
|
/// `(node, output-field)` under a boundary name (C8 — one port, K columns).
|
||||||
pub fn output(&self) -> &[OutField] {
|
pub fn output(&self) -> &[OutField] {
|
||||||
@@ -189,12 +171,14 @@ impl Composite {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The aggregated, flat, path-qualified param-space (C12): every node's declared
|
/// The aggregated, flat, path-qualified param-space (C12): every node's declared
|
||||||
/// params, concatenated in lowering order. The ROOT uses an empty path prefix
|
/// params, concatenated in lowering order. Each param is `<node>.<param>` (the
|
||||||
/// (its own name does not prefix — preserving the pre-refactor param names);
|
/// node name = its instance name, default = lowercased type label), with the
|
||||||
/// interior composite names prefix via the recursion in `collect_params`.
|
/// composite path prefixed at every level including the root — so a top-level
|
||||||
|
/// leaf carries its own node segment (e.g. `sma.length`). Interior composite
|
||||||
|
/// names prefix via the recursion in `collect_params`.
|
||||||
pub fn param_space(&self) -> Vec<ParamSpec> {
|
pub fn param_space(&self) -> Vec<ParamSpec> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
collect_params(&self.nodes, "", &self.params, &mut out);
|
collect_params(&self.nodes, "", &mut out);
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,8 +253,9 @@ impl Composite {
|
|||||||
|
|
||||||
/// Begin binding this blueprint's knobs **by name** for a single run (the
|
/// Begin binding this blueprint's knobs **by name** for a single run (the
|
||||||
/// fluent alternative to a positional `bootstrap_with_params` vector). The
|
/// fluent alternative to a positional `bootstrap_with_params` vector). The
|
||||||
/// bound name is the exact `param_space()` name — path-qualified for a knob
|
/// bound name is the exact `param_space()` name — `<node>.<param>` at every
|
||||||
/// inside a composite (`sma_cross.fast`), bare for a root-level knob (`scale`).
|
/// level, e.g. `sma_cross.fast.length` for a composite-interior knob and
|
||||||
|
/// `exposure.scale` for a root-level knob.
|
||||||
pub fn with(self, name: &str, v: impl Into<Scalar>) -> Binder {
|
pub fn with(self, name: &str, v: impl Into<Scalar>) -> Binder {
|
||||||
Binder { bp: self, bound: vec![(name.to_string(), v.into())] }
|
Binder { bp: self, bound: vec![(name.to_string(), v.into())] }
|
||||||
}
|
}
|
||||||
@@ -536,81 +521,37 @@ fn validate_wiring(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The recursive authoring signature of interior node `node`: the type initial,
|
/// The recursive authoring signature of interior node `node`: the node name (the
|
||||||
/// then one initial per declared param alias (declared order), then each wired
|
/// instance name, default = the lowercased type label), then each wired input's
|
||||||
/// input's signature in slot order — recursing into interior-leaf sources,
|
/// signature in slot order — recursing into interior-leaf sources, stopping at a
|
||||||
/// stopping at a named source (role name / nested-composite name). Single source
|
/// named source (role name / nested-composite name). Single source of truth for
|
||||||
/// of truth for the fan-in distinguishability check (collision = equal
|
/// the fan-in distinguishability check (collision = equal signatures) and the CLI
|
||||||
/// signatures) and the CLI render (shortest sibling-unique prefix). Terminates:
|
/// render (shortest sibling-unique prefix). Terminates: the dataflow is a DAG (C5)
|
||||||
/// the dataflow is a DAG (C5) and the descent stops at named ports.
|
/// and the descent stops at named ports.
|
||||||
pub fn signature_of(
|
pub fn signature_of(nodes: &[BlueprintNode], edges: &[Edge], roles: &[Role], node: usize) -> String {
|
||||||
nodes: &[BlueprintNode],
|
|
||||||
edges: &[Edge],
|
|
||||||
roles: &[Role],
|
|
||||||
aliases: &[ParamAlias],
|
|
||||||
node: usize,
|
|
||||||
) -> String {
|
|
||||||
let mut s = String::new();
|
|
||||||
match &nodes[node] {
|
match &nodes[node] {
|
||||||
|
// a nested composite is a named source: its name, no descent.
|
||||||
|
BlueprintNode::Composite(inner) => inner.name().to_string(),
|
||||||
BlueprintNode::Primitive(f) => {
|
BlueprintNode::Primitive(f) => {
|
||||||
if let Some(ch) = f.label().chars().next() {
|
let mut s = f.node_name();
|
||||||
s.push(ch);
|
|
||||||
}
|
|
||||||
for a in aliases_on(aliases, node) {
|
|
||||||
if let Some(ch) = a.name.chars().next() {
|
|
||||||
s.push(ch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// wired input slots in slot order; per slot, the source's signature
|
// wired input slots in slot order; per slot, the source's signature
|
||||||
// (interior edge -> recurse; role -> the role initial, no descent)
|
// (interior edge -> recurse; role -> the role name, no descent)
|
||||||
let mut slotted: Vec<(usize, String)> = Vec::new();
|
let mut slotted: Vec<(usize, String)> = Vec::new();
|
||||||
for e in edges.iter().filter(|e| e.to == node) {
|
for e in edges.iter().filter(|e| e.to == node) {
|
||||||
slotted.push((e.slot, signature_of(nodes, edges, roles, aliases, e.from)));
|
slotted.push((e.slot, signature_of(nodes, edges, roles, e.from)));
|
||||||
}
|
}
|
||||||
for r in roles {
|
for r in roles {
|
||||||
for t in r.targets.iter().filter(|t| t.node == node) {
|
for t in r.targets.iter().filter(|t| t.node == node) {
|
||||||
// a role is a named source: it contributes its initial, no
|
slotted.push((t.slot, r.name.clone()));
|
||||||
// descent (matching the nested-composite branch below).
|
|
||||||
let init = r.name.chars().next().map(String::from).unwrap_or_default();
|
|
||||||
slotted.push((t.slot, init));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
slotted.sort_by_key(|(slot, _)| *slot);
|
slotted.sort_by_key(|(slot, _)| *slot);
|
||||||
for (_, sig) in slotted {
|
for (_, sig) in slotted {
|
||||||
s.push_str(&sig);
|
s.push_str(&sig);
|
||||||
}
|
}
|
||||||
}
|
s
|
||||||
BlueprintNode::Composite(inner) => {
|
|
||||||
if let Some(ch) = inner.name().chars().next() {
|
|
||||||
s.push(ch);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The param aliases declared on interior node `node`, in declared order. The single
|
|
||||||
/// anchor for the `a.node == node` predicate shared by the signature base, the
|
|
||||||
/// unaliased-param test, and the CLI's render base-length (issue #45) — change the
|
|
||||||
/// predicate here, not in three places.
|
|
||||||
pub fn aliases_on(aliases: &[ParamAlias], node: usize) -> impl Iterator<Item = &ParamAlias> {
|
|
||||||
aliases.iter().filter(move |a| a.node == node)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validate that every param alias names a real interior leaf param slot: a dangling
|
|
||||||
/// `(node, slot)` is a `BadInteriorIndex` (C23 — names are cosmetic, but a dangling
|
|
||||||
/// handle is an author error). Sole owner of the alias-index check: the structural
|
|
||||||
/// pre-pass recurses every composite that lowering reaches, so `inline_composite`
|
|
||||||
/// trusts this has already run rather than re-checking (issue #45).
|
|
||||||
fn check_alias_indices(nodes: &[BlueprintNode], aliases: &[ParamAlias]) -> Result<(), CompileError> {
|
|
||||||
for a in aliases {
|
|
||||||
let ok = a.node < nodes.len()
|
|
||||||
&& matches!(&nodes[a.node], BlueprintNode::Primitive(f) if a.slot < f.params().len());
|
|
||||||
if !ok {
|
|
||||||
return Err(CompileError::BadInteriorIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Structural validation (param-value-independent): walk every composite in the
|
/// Structural validation (param-value-independent): walk every composite in the
|
||||||
@@ -620,7 +561,7 @@ fn check_alias_indices(nodes: &[BlueprintNode], aliases: &[ParamAlias]) -> Resul
|
|||||||
fn check_fan_in_distinguishability(items: &[BlueprintNode]) -> Result<(), CompileError> {
|
fn check_fan_in_distinguishability(items: &[BlueprintNode]) -> Result<(), CompileError> {
|
||||||
for item in items {
|
for item in items {
|
||||||
if let BlueprintNode::Composite(c) = item {
|
if let BlueprintNode::Composite(c) = item {
|
||||||
check_composite_fan_in(c.nodes(), c.edges(), c.input_roles(), c.params())?;
|
check_composite_fan_in(c.nodes(), c.edges(), c.input_roles())?;
|
||||||
check_fan_in_distinguishability(c.nodes())?;
|
check_fan_in_distinguishability(c.nodes())?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -629,27 +570,22 @@ fn check_fan_in_distinguishability(items: &[BlueprintNode]) -> Result<(), Compil
|
|||||||
|
|
||||||
/// The per-composite fan-in distinguishability check on the destructured pieces:
|
/// The per-composite fan-in distinguishability check on the destructured pieces:
|
||||||
/// for each interior node with >1 wired input slot, a collision (equal source
|
/// for each interior node with >1 wired input slot, a collision (equal source
|
||||||
/// signatures) is a fault only when at least one colliding source has an unaliased
|
/// signatures, now keyed on the node name) is a fault only when at least one
|
||||||
/// param slot — the unnamed configuration axis. A role contributes its name and
|
/// colliding source carries a param — the unnamed configuration axis resolved by
|
||||||
/// has no param of its own.
|
/// giving the colliding nodes distinct names. A role contributes its name and has
|
||||||
|
/// no param of its own.
|
||||||
fn check_composite_fan_in(
|
fn check_composite_fan_in(
|
||||||
nodes: &[BlueprintNode],
|
nodes: &[BlueprintNode],
|
||||||
edges: &[Edge],
|
edges: &[Edge],
|
||||||
input_roles: &[Role],
|
input_roles: &[Role],
|
||||||
param_aliases: &[ParamAlias],
|
|
||||||
) -> Result<(), CompileError> {
|
) -> Result<(), CompileError> {
|
||||||
// alias-validity first: a bogus alias index is a `BadInteriorIndex`, ordered
|
|
||||||
// ahead of the fan-in fault so a bad alias surfaces as the index error, not as an
|
|
||||||
// incidental collision.
|
|
||||||
check_alias_indices(nodes, param_aliases)?;
|
|
||||||
|
|
||||||
for node in 0..nodes.len() {
|
for node in 0..nodes.len() {
|
||||||
let mut sources: Vec<(usize, String, bool)> = Vec::new(); // (slot, sig, has_unaliased_param)
|
let mut sources: Vec<(usize, String, bool)> = Vec::new(); // (slot, sig, has_param)
|
||||||
for e in edges.iter().filter(|e| e.to == node) {
|
for e in edges.iter().filter(|e| e.to == node) {
|
||||||
sources.push((
|
sources.push((
|
||||||
e.slot,
|
e.slot,
|
||||||
signature_of(nodes, edges, input_roles, param_aliases, e.from),
|
signature_of(nodes, edges, input_roles, e.from),
|
||||||
leaf_has_unaliased_param(nodes, param_aliases, e.from),
|
leaf_has_param(nodes, e.from),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
for r in input_roles {
|
for r in input_roles {
|
||||||
@@ -671,50 +607,30 @@ fn check_composite_fan_in(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether interior leaf `node` has at least one param slot with no alias in
|
/// Whether interior leaf `node` has at least one param slot — the configuration
|
||||||
/// `aliases` — the unnamed configuration axis the fan-in rule keys on. A non-leaf
|
/// axis the fan-in rule keys on once the node name no longer distinguishes the
|
||||||
/// (nested composite) reports `false`.
|
/// colliding sources. A non-leaf (nested composite) reports `false`.
|
||||||
fn leaf_has_unaliased_param(nodes: &[BlueprintNode], aliases: &[ParamAlias], node: usize) -> bool {
|
fn leaf_has_param(nodes: &[BlueprintNode], node: usize) -> bool {
|
||||||
match &nodes[node] {
|
matches!(&nodes[node], BlueprintNode::Primitive(b) if !b.params().is_empty())
|
||||||
BlueprintNode::Primitive(f) => {
|
|
||||||
let n_params = f.params().len();
|
|
||||||
let aliased = aliases_on(aliases, node).count();
|
|
||||||
n_params > aliased
|
|
||||||
}
|
|
||||||
BlueprintNode::Composite(_) => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recursive read-only walk for `Blueprint::param_space`: a leaf contributes its
|
/// Recursive read-only walk for `Blueprint::param_space`: a leaf contributes its
|
||||||
/// declared params under the running path prefix; a composite pushes its `name()`
|
/// declared params under `<prefix>.<node-name>.<param>` (the node name is the
|
||||||
/// onto the path and recurses, passing its own param aliases down. A leaf param
|
/// instance name, default = the lowercased type label); a composite pushes its
|
||||||
/// slot matched by an `(node, slot)` alias is relabelled in place (C23 — pure
|
/// `name()` onto the path and recurses. Order mirrors `lower_items` (items in
|
||||||
/// naming overlay; the slot stays, order is untouched). Order mirrors `lower_items`
|
/// declared order, composites depth-first) so a param's slot matches the later
|
||||||
/// (items in declared order, composites depth-first) so a param's slot matches the
|
/// flat-node order.
|
||||||
/// later flat-node order.
|
fn collect_params(items: &[BlueprintNode], prefix: &str, out: &mut Vec<ParamSpec>) {
|
||||||
fn collect_params(
|
for item in items {
|
||||||
items: &[BlueprintNode],
|
|
||||||
prefix: &str,
|
|
||||||
aliases: &[ParamAlias],
|
|
||||||
out: &mut Vec<ParamSpec>,
|
|
||||||
) {
|
|
||||||
for (i, item) in items.iter().enumerate() {
|
|
||||||
match item {
|
match item {
|
||||||
BlueprintNode::Primitive(builder) => {
|
BlueprintNode::Primitive(b) => {
|
||||||
for (s, p) in builder.params().iter().enumerate() {
|
let node = if prefix.is_empty() {
|
||||||
// an alias for this exact (node, slot) relabels in place;
|
b.node_name()
|
||||||
// otherwise the factory param name, as today.
|
} else {
|
||||||
let local = aliases
|
format!("{prefix}.{}", b.node_name())
|
||||||
.iter()
|
};
|
||||||
.find(|a| a.node == i && a.slot == s)
|
for p in b.params() {
|
||||||
.map(|a| a.name.as_str())
|
out.push(ParamSpec { name: format!("{node}.{}", p.name), kind: p.kind });
|
||||||
.unwrap_or(p.name.as_str());
|
|
||||||
let name = if prefix.is_empty() {
|
|
||||||
local.to_string()
|
|
||||||
} else {
|
|
||||||
format!("{prefix}.{local}")
|
|
||||||
};
|
|
||||||
out.push(ParamSpec { name, kind: p.kind });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BlueprintNode::Composite(c) => {
|
BlueprintNode::Composite(c) => {
|
||||||
@@ -723,7 +639,7 @@ fn collect_params(
|
|||||||
} else {
|
} else {
|
||||||
format!("{prefix}.{}", c.name())
|
format!("{prefix}.{}", c.name())
|
||||||
};
|
};
|
||||||
collect_params(c.nodes(), &child, c.params(), out);
|
collect_params(c.nodes(), &child, out);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -800,16 +716,13 @@ fn inline_composite(
|
|||||||
) -> Result<ItemLowering, CompileError> {
|
) -> Result<ItemLowering, CompileError> {
|
||||||
// `name` is the non-load-bearing render symbol (#13); it dissolves at inline
|
// `name` is the non-load-bearing render symbol (#13); it dissolves at inline
|
||||||
// (C23 — the boundary does not reach the compilat), so it is not destructured.
|
// (C23 — the boundary does not reach the compilat), so it is not destructured.
|
||||||
// `params` (the composite's ParamAlias overlay) is a pure naming layer validated
|
// Node names join the same non-load-bearing debug-symbol class: they qualify the
|
||||||
// by the pre-pass and unused in lowering — the injected scalar `params: &[Scalar]`
|
// param-space path at construction but are dropped at lowering — the injected
|
||||||
// arg drives `lower_items` below — so it is dropped here.
|
// scalar `params: &[Scalar]` arg drives `lower_items` below; the compilat stays
|
||||||
let Composite { name: _, nodes, edges, input_roles, params: _, output } = c;
|
// wired by raw index.
|
||||||
|
let Composite { name: _, nodes, edges, input_roles, output } = c;
|
||||||
let item_count = nodes.len();
|
let item_count = nodes.len();
|
||||||
|
|
||||||
// alias-validity (a dangling `(node, slot)` is a `BadInteriorIndex`) is owned by
|
|
||||||
// the structural pre-pass `check_alias_indices`, which `compile_with_params` runs
|
|
||||||
// over every composite before any lowering — so it has already fired here (#45).
|
|
||||||
|
|
||||||
// recursively lower interior items, then rewrite interior edges through them
|
// 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, params, cursor, flat_nodes, flat_signatures, flat_edges)?;
|
||||||
for e in &edges {
|
for e in &edges {
|
||||||
@@ -989,14 +902,14 @@ mod tests {
|
|||||||
// must not be invoked.
|
// must not be invoked.
|
||||||
let (bp, _eq, _ex) = composite_sma_cross_harness();
|
let (bp, _eq, _ex) = composite_sma_cross_harness();
|
||||||
let result = bp
|
let result = bp
|
||||||
.axis("sma_cross.fast", [2.0, 3.0]) // F64 values for the I64 slot
|
.axis("sma_cross.fast.length", [2.0, 3.0]) // F64 values for the I64 slot
|
||||||
.axis("sma_cross.slow", [4])
|
.axis("sma_cross.slow.length", [4])
|
||||||
.axis("scale", [0.5])
|
.axis("exposure.scale", [0.5])
|
||||||
.sweep(|_: &[Scalar]| -> RunReport { panic!("axis pre-validation must reject before running") });
|
.sweep(|_: &[Scalar]| -> RunReport { panic!("axis pre-validation must reject before running") });
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result,
|
result,
|
||||||
Err(BindError::KindMismatch {
|
Err(BindError::KindMismatch {
|
||||||
knob: "sma_cross.fast".to_string(),
|
knob: "sma_cross.fast.length".to_string(),
|
||||||
expected: ScalarKind::I64,
|
expected: ScalarKind::I64,
|
||||||
got: ScalarKind::F64,
|
got: ScalarKind::F64,
|
||||||
}),
|
}),
|
||||||
@@ -1011,9 +924,9 @@ mod tests {
|
|||||||
let named = resolve_axes(
|
let named = resolve_axes(
|
||||||
&space,
|
&space,
|
||||||
&[
|
&[
|
||||||
("sma_cross.fast".to_string(), vec![Scalar::I64(2), Scalar::I64(3)]),
|
("sma_cross.fast.length".to_string(), vec![Scalar::I64(2), Scalar::I64(3)]),
|
||||||
("sma_cross.slow".to_string(), vec![Scalar::I64(4), Scalar::I64(5)]),
|
("sma_cross.slow.length".to_string(), vec![Scalar::I64(4), Scalar::I64(5)]),
|
||||||
("scale".to_string(), vec![Scalar::F64(0.5)]),
|
("exposure.scale".to_string(), vec![Scalar::F64(0.5)]),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.expect("named axes resolve");
|
.expect("named axes resolve");
|
||||||
@@ -1133,9 +1046,9 @@ mod tests {
|
|||||||
// the positional vector, over the sample composite harness.
|
// the positional vector, over the sample composite harness.
|
||||||
let (bp, comp_eq, comp_ex) = composite_sma_cross_harness();
|
let (bp, comp_eq, comp_ex) = composite_sma_cross_harness();
|
||||||
let mut named = bp
|
let mut named = bp
|
||||||
.with("sma_cross.fast", 2)
|
.with("sma_cross.fast.length", 2)
|
||||||
.with("sma_cross.slow", 4)
|
.with("sma_cross.slow.length", 4)
|
||||||
.with("scale", 0.5)
|
.with("exposure.scale", 0.5)
|
||||||
.bootstrap()
|
.bootstrap()
|
||||||
.expect("named binding resolves and bootstraps");
|
.expect("named binding resolves and bootstraps");
|
||||||
named.run(vec![synthetic_prices()]);
|
named.run(vec![synthetic_prices()]);
|
||||||
@@ -1280,27 +1193,99 @@ mod tests {
|
|||||||
vec![Role {
|
vec![Role {
|
||||||
name: "price".into(),
|
name: "price".into(),
|
||||||
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], source: None, }],
|
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], source: None, }],
|
||||||
vec![],
|
|
||||||
vec![OutField { node: 2, field: 0, name: "out".into() }],
|
vec![OutField { node: 2, field: 0, name: "out".into() }],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
/// The SMA-cross signal as a composite under the boundary name `sma_cross`,
|
||||||
fn signature_of_is_type_initial_plus_aliases_plus_recursive_inputs() {
|
/// with its two SMA legs given `name` (or left default when `named` is false).
|
||||||
// EMA(fast) fed by role price -> "E" + "f"(alias) + "p"(role, no descent)
|
/// Nested under a root so the composite name qualifies the param-space path and
|
||||||
let c = macd_like_signature_fixture(); // built below
|
/// the fan-in check (which inspects nested composites) reaches the Sub fan-in.
|
||||||
let sig = |n| signature_of(c.nodes(), c.edges(), c.input_roles(), c.params(), n);
|
fn sma_cross_under_root(named: bool) -> Composite {
|
||||||
// node 0 = Ema aliased "fast", fed by role "price"
|
use aura_std::{Sma, Sub};
|
||||||
assert_eq!(sig(0), "Efp");
|
let (a, b) = if named {
|
||||||
// node 2 = Sub(node0, node1) where node1 = Ema aliased "slow" -> "S"+inputs
|
(Sma::builder().named("fast"), Sma::builder().named("slow"))
|
||||||
assert_eq!(sig(2), "SEfpEsp");
|
} else {
|
||||||
|
(Sma::builder(), Sma::builder())
|
||||||
|
};
|
||||||
|
let cross = Composite::new(
|
||||||
|
"sma_cross",
|
||||||
|
vec![a.into(), b.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: "cross".into() }],
|
||||||
|
);
|
||||||
|
Composite::new(
|
||||||
|
"root",
|
||||||
|
vec![BlueprintNode::Composite(cross)],
|
||||||
|
vec![],
|
||||||
|
vec![Role {
|
||||||
|
name: "src".into(),
|
||||||
|
targets: vec![Target { node: 0, slot: 0 }],
|
||||||
|
source: Some(ScalarKind::F64),
|
||||||
|
}],
|
||||||
|
vec![],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A composite: two aliased EMAs (fast, slow) on role `price`, into a Sub.
|
#[test]
|
||||||
|
fn named_siblings_path_qualify_with_node_segment() {
|
||||||
|
// two SMAs named fast/slow under composite "sma_cross" -> the node segment
|
||||||
|
// qualifies each leaf's param path under the composite name.
|
||||||
|
let bp = sma_cross_under_root(true);
|
||||||
|
let names: Vec<String> = bp.param_space().into_iter().map(|p| p.name).collect();
|
||||||
|
assert_eq!(names, ["sma_cross.fast.length", "sma_cross.slow.length"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unnamed_single_primitive_uses_lowercased_type_label_segment() {
|
||||||
|
use aura_std::Sma;
|
||||||
|
let bp = Composite::new("root", vec![Sma::builder().into()], vec![], vec![], vec![]);
|
||||||
|
assert_eq!(bp.param_space()[0].name, "sma.length");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unnamed_same_type_param_bearing_fan_in_is_rejected() {
|
||||||
|
// both SMAs default to "sma" -> collide -> fan-in indistinguishable
|
||||||
|
let bp = sma_cross_under_root(false);
|
||||||
|
assert_eq!(bp.compile().err(), Some(CompileError::IndistinguishableFanIn { node: 2 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn named_param_bearing_fan_in_bootstraps() {
|
||||||
|
// distinct node names -> fan-in distinguishable -> compiles (the two SMA
|
||||||
|
// length params are supplied so the only thing under test is the fan-in)
|
||||||
|
let bp = sma_cross_under_root(true);
|
||||||
|
assert!(bp.compile_with_params(&[Scalar::I64(2), Scalar::I64(4)]).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn signature_of_is_node_name_plus_recursive_inputs() {
|
||||||
|
// EMA named "fast" fed by role price -> "fast" + "price" (role, no descent)
|
||||||
|
let c = macd_like_signature_fixture(); // built below
|
||||||
|
let sig = |n| signature_of(c.nodes(), c.edges(), c.input_roles(), n);
|
||||||
|
// node 0 = EMA named "fast", fed by role "price"
|
||||||
|
assert_eq!(sig(0), "fastprice");
|
||||||
|
// node 2 = Sub (default name "sub") over node0/node1; node1 = EMA "slow"
|
||||||
|
assert_eq!(sig(2), "subfastpriceslowprice");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A composite: two named EMAs (fast, slow) on role `price`, into a Sub.
|
||||||
fn macd_like_signature_fixture() -> Composite {
|
fn macd_like_signature_fixture() -> Composite {
|
||||||
Composite::new(
|
Composite::new(
|
||||||
"sig",
|
"sig",
|
||||||
vec![Ema::builder().into(), Ema::builder().into(), Sub::builder().into()],
|
vec![
|
||||||
|
Ema::builder().named("fast").into(),
|
||||||
|
Ema::builder().named("slow").into(),
|
||||||
|
Sub::builder().into(),
|
||||||
|
],
|
||||||
vec![
|
vec![
|
||||||
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
||||||
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
|
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
|
||||||
@@ -1308,10 +1293,6 @@ mod tests {
|
|||||||
vec![Role {
|
vec![Role {
|
||||||
name: "price".into(),
|
name: "price".into(),
|
||||||
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], source: None, }],
|
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], source: None, }],
|
||||||
vec![
|
|
||||||
ParamAlias { name: "fast".into(), node: 0, slot: 0 },
|
|
||||||
ParamAlias { name: "slow".into(), node: 1, slot: 0 },
|
|
||||||
],
|
|
||||||
vec![OutField { node: 2, field: 0, name: "x".into() }],
|
vec![OutField { node: 2, field: 0, name: "x".into() }],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1326,7 +1307,6 @@ mod tests {
|
|||||||
vec![
|
vec![
|
||||||
Role { name: "src".into(), targets: vec![Target { node: 0, slot: 0 }], source: Some(ScalarKind::F64) },
|
Role { name: "src".into(), targets: vec![Target { node: 0, slot: 0 }], source: Some(ScalarKind::F64) },
|
||||||
],
|
],
|
||||||
vec![], // params
|
|
||||||
vec![], // output
|
vec![], // output
|
||||||
);
|
);
|
||||||
let flat = bp.compile().expect("valid composite");
|
let flat = bp.compile().expect("valid composite");
|
||||||
@@ -1364,7 +1344,6 @@ mod tests {
|
|||||||
Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: None },
|
Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: None },
|
||||||
Role { name: "price2".into(), targets: vec![Target { node: 1, slot: 0 }], source: None },
|
Role { name: "price2".into(), targets: vec![Target { node: 1, slot: 0 }], source: None },
|
||||||
],
|
],
|
||||||
vec![],
|
|
||||||
vec![
|
vec![
|
||||||
OutField { node: 0, field: 0, name: "a".into() },
|
OutField { node: 0, field: 0, name: "a".into() },
|
||||||
OutField { node: 1, field: 0, name: "b".into() },
|
OutField { node: 1, field: 0, name: "b".into() },
|
||||||
@@ -1382,7 +1361,6 @@ mod tests {
|
|||||||
vec![
|
vec![
|
||||||
Role { name: "src".into(), targets: vec![Target { node: 0, slot: 0 }, Target { node: 0, slot: 1 }], source: Some(ScalarKind::F64) },
|
Role { name: "src".into(), targets: vec![Target { node: 0, slot: 0 }, Target { node: 0, slot: 1 }], source: Some(ScalarKind::F64) },
|
||||||
],
|
],
|
||||||
vec![], // params
|
|
||||||
vec![], // output
|
vec![], // output
|
||||||
);
|
);
|
||||||
let flat = bp.compile().expect("valid multi-output composite");
|
let flat = bp.compile().expect("valid multi-output composite");
|
||||||
@@ -1416,7 +1394,6 @@ mod tests {
|
|||||||
vec![BlueprintNode::Composite(inner)],
|
vec![BlueprintNode::Composite(inner)],
|
||||||
vec![],
|
vec![],
|
||||||
vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: None }],
|
vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: None }],
|
||||||
vec![],
|
|
||||||
vec![OutField { node: 0, field: 0, name: "out".into() }],
|
vec![OutField { node: 0, field: 0, name: "out".into() }],
|
||||||
);
|
);
|
||||||
let bp = Composite::new(
|
let bp = Composite::new(
|
||||||
@@ -1426,7 +1403,6 @@ mod tests {
|
|||||||
vec![
|
vec![
|
||||||
Role { name: "src".into(), targets: vec![Target { node: 0, slot: 0 }], source: Some(ScalarKind::F64) },
|
Role { name: "src".into(), targets: vec![Target { node: 0, slot: 0 }], source: Some(ScalarKind::F64) },
|
||||||
],
|
],
|
||||||
vec![], // params
|
|
||||||
vec![], // output
|
vec![], // output
|
||||||
);
|
);
|
||||||
let flat = bp.compile().expect("valid nested composite");
|
let flat = bp.compile().expect("valid nested composite");
|
||||||
@@ -1460,7 +1436,6 @@ mod tests {
|
|||||||
Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: None },
|
Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: None },
|
||||||
Role { name: "price2".into(), targets: vec![Target { node: 1, slot: 0 }], source: None },
|
Role { name: "price2".into(), targets: vec![Target { node: 1, slot: 0 }], source: None },
|
||||||
],
|
],
|
||||||
vec![],
|
|
||||||
vec![
|
vec![
|
||||||
OutField { node: 0, field: 0, name: "a".into() },
|
OutField { node: 0, field: 0, name: "a".into() },
|
||||||
OutField { node: 1, field: 0, name: "b".into() },
|
OutField { node: 1, field: 0, name: "b".into() },
|
||||||
@@ -1474,7 +1449,6 @@ mod tests {
|
|||||||
Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: None }, // outer role 0 -> inner role 0
|
Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: None }, // outer role 0 -> inner role 0
|
||||||
Role { name: "price2".into(), targets: vec![Target { node: 0, slot: 1 }], source: None }, // outer role 1 -> inner role 1
|
Role { name: "price2".into(), targets: vec![Target { node: 0, slot: 1 }], source: None }, // outer role 1 -> inner role 1
|
||||||
],
|
],
|
||||||
vec![],
|
|
||||||
vec![
|
vec![
|
||||||
OutField { node: 0, field: 0, name: "x".into() }, // inner field 0
|
OutField { node: 0, field: 0, name: "x".into() }, // inner field 0
|
||||||
OutField { node: 0, field: 1, name: "y".into() }, // inner field 1 (nested arm)
|
OutField { node: 0, field: 1, name: "y".into() }, // inner field 1 (nested arm)
|
||||||
@@ -1490,7 +1464,6 @@ mod tests {
|
|||||||
vec![
|
vec![
|
||||||
Role { name: "src".into(), targets: vec![Target { node: 0, slot: 0 }, Target { node: 0, slot: 1 }], source: Some(ScalarKind::F64) },
|
Role { name: "src".into(), targets: vec![Target { node: 0, slot: 0 }, Target { node: 0, slot: 1 }], source: Some(ScalarKind::F64) },
|
||||||
],
|
],
|
||||||
vec![], // params
|
|
||||||
vec![], // output
|
vec![], // output
|
||||||
);
|
);
|
||||||
let flat = bp.compile().expect("valid nested multi-output");
|
let flat = bp.compile().expect("valid nested multi-output");
|
||||||
@@ -1513,7 +1486,6 @@ mod tests {
|
|||||||
vec![pass1()],
|
vec![pass1()],
|
||||||
vec![Edge { from: 0, to: 9, slot: 0, from_field: 0 }],
|
vec![Edge { from: 0, to: 9, slot: 0, from_field: 0 }],
|
||||||
vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: None }],
|
vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: None }],
|
||||||
vec![],
|
|
||||||
vec![OutField { node: 0, field: 0, name: "out".into() }],
|
vec![OutField { node: 0, field: 0, name: "out".into() }],
|
||||||
);
|
);
|
||||||
let bp = Composite::new(
|
let bp = Composite::new(
|
||||||
@@ -1521,7 +1493,6 @@ mod tests {
|
|||||||
vec![BlueprintNode::Composite(c)],
|
vec![BlueprintNode::Composite(c)],
|
||||||
vec![],
|
vec![],
|
||||||
vec![],
|
vec![],
|
||||||
vec![], // params
|
|
||||||
vec![], // output
|
vec![], // output
|
||||||
);
|
);
|
||||||
// the Ok arm holds Box<dyn Node> (not Debug), so assert via the Err arm.
|
// the Ok arm holds Box<dyn Node> (not Debug), so assert via the Err arm.
|
||||||
@@ -1530,8 +1501,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn indistinguishable_fan_in_rejected() {
|
fn indistinguishable_fan_in_rejected() {
|
||||||
// two alias-less Sma (each an unaliased `length`) on role price into a Sub:
|
// two default-named Sma (both "sma", each a `length` param) on role price
|
||||||
// signatures collide ("Sp"=="Sp") and a param is unaliased -> fault.
|
// into a Sub: node-name signatures collide and a param is present -> fault.
|
||||||
let c = Composite::new(
|
let c = Composite::new(
|
||||||
"ambig",
|
"ambig",
|
||||||
vec![Sma::builder().into(), Sma::builder().into(), Sub::builder().into()],
|
vec![Sma::builder().into(), Sma::builder().into(), Sub::builder().into()],
|
||||||
@@ -1542,7 +1513,6 @@ mod tests {
|
|||||||
vec![Role {
|
vec![Role {
|
||||||
name: "price".into(),
|
name: "price".into(),
|
||||||
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], source: None, }],
|
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], source: None, }],
|
||||||
vec![],
|
|
||||||
vec![OutField { node: 2, field: 0, name: "x".into() }],
|
vec![OutField { node: 2, field: 0, name: "x".into() }],
|
||||||
);
|
);
|
||||||
let bp = Composite::new(
|
let bp = Composite::new(
|
||||||
@@ -1552,7 +1522,6 @@ mod tests {
|
|||||||
vec![
|
vec![
|
||||||
Role { name: "src".into(), targets: vec![Target { node: 0, slot: 0 }], source: Some(ScalarKind::F64) },
|
Role { name: "src".into(), targets: vec![Target { node: 0, slot: 0 }], source: Some(ScalarKind::F64) },
|
||||||
],
|
],
|
||||||
vec![], // params
|
|
||||||
vec![], // output
|
vec![], // output
|
||||||
);
|
);
|
||||||
assert_eq!(bp.compile().err(), Some(CompileError::IndistinguishableFanIn { node: 2 }));
|
assert_eq!(bp.compile().err(), Some(CompileError::IndistinguishableFanIn { node: 2 }));
|
||||||
@@ -1569,7 +1538,6 @@ mod tests {
|
|||||||
vec![
|
vec![
|
||||||
Role { name: "src".into(), targets: vec![Target { node: 0, slot: 0 }], source: Some(ScalarKind::F64) },
|
Role { name: "src".into(), targets: vec![Target { node: 0, slot: 0 }], source: Some(ScalarKind::F64) },
|
||||||
],
|
],
|
||||||
vec![], // params
|
|
||||||
vec![], // output
|
vec![], // output
|
||||||
);
|
);
|
||||||
assert!(bp.compile().is_ok(), "param-less interchangeable fan-in must compile");
|
assert!(bp.compile().is_ok(), "param-less interchangeable fan-in must compile");
|
||||||
@@ -1585,7 +1553,6 @@ mod tests {
|
|||||||
vec![Role {
|
vec![Role {
|
||||||
name: "price".into(),
|
name: "price".into(),
|
||||||
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], source: None, }],
|
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], source: None, }],
|
||||||
vec![],
|
|
||||||
vec![OutField { node: 0, field: 0, name: "out".into() }],
|
vec![OutField { node: 0, field: 0, name: "out".into() }],
|
||||||
);
|
);
|
||||||
let bp = Composite::new(
|
let bp = Composite::new(
|
||||||
@@ -1593,7 +1560,6 @@ mod tests {
|
|||||||
vec![BlueprintNode::Composite(c)],
|
vec![BlueprintNode::Composite(c)],
|
||||||
vec![],
|
vec![],
|
||||||
vec![],
|
vec![],
|
||||||
vec![], // params
|
|
||||||
vec![], // output
|
vec![], // output
|
||||||
);
|
);
|
||||||
// the Ok arm holds Box<dyn Node> (not Debug), so assert via the Err arm.
|
// the Ok arm holds Box<dyn Node> (not Debug), so assert via the Err arm.
|
||||||
@@ -1608,7 +1574,6 @@ mod tests {
|
|||||||
vec![pass1()],
|
vec![pass1()],
|
||||||
vec![],
|
vec![],
|
||||||
vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: None }],
|
vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: None }],
|
||||||
vec![],
|
|
||||||
vec![OutField { node: 0, field: 5, name: "out".into() }],
|
vec![OutField { node: 0, field: 5, name: "out".into() }],
|
||||||
);
|
);
|
||||||
let bp = Composite::new(
|
let bp = Composite::new(
|
||||||
@@ -1616,7 +1581,6 @@ mod tests {
|
|||||||
vec![BlueprintNode::Composite(c)],
|
vec![BlueprintNode::Composite(c)],
|
||||||
vec![],
|
vec![],
|
||||||
vec![],
|
vec![],
|
||||||
vec![], // params
|
|
||||||
vec![], // output
|
vec![], // output
|
||||||
);
|
);
|
||||||
// the Ok arm holds Box<dyn Node> (not Debug), so assert via the Err arm.
|
// the Ok arm holds Box<dyn Node> (not Debug), so assert via the Err arm.
|
||||||
@@ -1632,7 +1596,6 @@ mod tests {
|
|||||||
vec![pass1()],
|
vec![pass1()],
|
||||||
vec![],
|
vec![],
|
||||||
vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: None }],
|
vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: None }],
|
||||||
vec![],
|
|
||||||
vec![OutField { node: 0, field: 0, name: "a".into() }],
|
vec![OutField { node: 0, field: 0, name: "a".into() }],
|
||||||
);
|
);
|
||||||
let bp = Composite::new(
|
let bp = Composite::new(
|
||||||
@@ -1642,7 +1605,6 @@ mod tests {
|
|||||||
vec![
|
vec![
|
||||||
Role { name: "src".into(), targets: vec![Target { node: 0, slot: 0 }], source: Some(ScalarKind::F64) },
|
Role { name: "src".into(), targets: vec![Target { node: 0, slot: 0 }], source: Some(ScalarKind::F64) },
|
||||||
],
|
],
|
||||||
vec![], // params
|
|
||||||
vec![], // output
|
vec![], // output
|
||||||
);
|
);
|
||||||
assert_eq!(bp.compile().err(), Some(CompileError::BadInteriorIndex));
|
assert_eq!(bp.compile().err(), Some(CompileError::BadInteriorIndex));
|
||||||
@@ -1657,7 +1619,6 @@ mod tests {
|
|||||||
vec![pass1(), sink_i64()],
|
vec![pass1(), sink_i64()],
|
||||||
vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }],
|
vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }],
|
||||||
vec![],
|
vec![],
|
||||||
vec![], // params
|
|
||||||
vec![], // output
|
vec![], // output
|
||||||
);
|
);
|
||||||
match bp.bootstrap().unwrap_err() {
|
match bp.bootstrap().unwrap_err() {
|
||||||
@@ -1747,7 +1708,11 @@ mod tests {
|
|||||||
fn sma_cross() -> Composite {
|
fn sma_cross() -> Composite {
|
||||||
Composite::new(
|
Composite::new(
|
||||||
"sma_cross",
|
"sma_cross",
|
||||||
vec![Sma::builder().into(), Sma::builder().into(), Sub::builder().into()],
|
vec![
|
||||||
|
Sma::builder().named("fast").into(),
|
||||||
|
Sma::builder().named("slow").into(),
|
||||||
|
Sub::builder().into(),
|
||||||
|
],
|
||||||
vec![
|
vec![
|
||||||
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
||||||
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
|
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
|
||||||
@@ -1755,10 +1720,6 @@ mod tests {
|
|||||||
vec![Role {
|
vec![Role {
|
||||||
name: "price".into(),
|
name: "price".into(),
|
||||||
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], source: None, }],
|
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], source: None, }],
|
||||||
vec![
|
|
||||||
ParamAlias { name: "fast".into(), node: 0, slot: 0 },
|
|
||||||
ParamAlias { name: "slow".into(), node: 1, slot: 0 },
|
|
||||||
],
|
|
||||||
vec![OutField { node: 2, field: 0, name: "out".into() }],
|
vec![OutField { node: 2, field: 0, name: "out".into() }],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1793,7 +1754,6 @@ mod tests {
|
|||||||
Target { node: 2, slot: 1 }, // price -> SimBroker price slot
|
Target { node: 2, slot: 1 }, // price -> SimBroker price slot
|
||||||
], source: Some(ScalarKind::F64) },
|
], source: Some(ScalarKind::F64) },
|
||||||
],
|
],
|
||||||
vec![], // params
|
|
||||||
vec![], // output
|
vec![], // output
|
||||||
);
|
);
|
||||||
(bp, rx_eq, rx_ex)
|
(bp, rx_eq, rx_ex)
|
||||||
@@ -1851,7 +1811,6 @@ mod tests {
|
|||||||
Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: None },
|
Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: None },
|
||||||
Role { name: "price2".into(), targets: vec![Target { node: 1, slot: 0 }], source: None },
|
Role { name: "price2".into(), targets: vec![Target { node: 1, slot: 0 }], source: None },
|
||||||
],
|
],
|
||||||
vec![],
|
|
||||||
vec![
|
vec![
|
||||||
OutField { node: 0, field: 0, name: "a".into() },
|
OutField { node: 0, field: 0, name: "a".into() },
|
||||||
OutField { node: 1, field: 0, name: "b".into() },
|
OutField { node: 1, field: 0, name: "b".into() },
|
||||||
@@ -1871,7 +1830,6 @@ mod tests {
|
|||||||
vec![
|
vec![
|
||||||
Role { name: "src".into(), targets: vec![Target { node: 0, slot: 0 }, Target { node: 0, slot: 1 }], source: Some(ScalarKind::F64) },
|
Role { name: "src".into(), targets: vec![Target { node: 0, slot: 0 }, Target { node: 0, slot: 1 }], source: Some(ScalarKind::F64) },
|
||||||
],
|
],
|
||||||
vec![], // params
|
|
||||||
vec![], // output
|
vec![], // output
|
||||||
);
|
);
|
||||||
let mut h = bp
|
let mut h = bp
|
||||||
@@ -1980,7 +1938,7 @@ mod tests {
|
|||||||
// scale (F64); Sub/SimBroker/Recorder declare none
|
// scale (F64); Sub/SimBroker/Recorder declare none
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
space.iter().map(|p| p.name.as_str()).collect::<Vec<_>>(),
|
space.iter().map(|p| p.name.as_str()).collect::<Vec<_>>(),
|
||||||
["sma_cross.fast", "sma_cross.slow", "scale"],
|
["sma_cross.fast.length", "sma_cross.slow.length", "exposure.scale"],
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
space.iter().map(|p| p.kind).collect::<Vec<_>>(),
|
space.iter().map(|p| p.kind).collect::<Vec<_>>(),
|
||||||
@@ -1988,137 +1946,17 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn param_alias_relabels_param_space_name_in_place() {
|
|
||||||
// two Sma leaves (each one `length` param) under a composite that aliases
|
|
||||||
// slot 0 of node 0 -> "shortLen" and slot 0 of node 1 -> "longLen".
|
|
||||||
let c = Composite::new(
|
|
||||||
"cross",
|
|
||||||
vec![Sma::builder().into(), Sma::builder().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![
|
|
||||||
ParamAlias { name: "shortLen".into(), node: 0, slot: 0 },
|
|
||||||
ParamAlias { name: "longLen".into(), node: 1, slot: 0 },
|
|
||||||
],
|
|
||||||
vec![OutField { node: 2, field: 0, name: "out".into() }],
|
|
||||||
);
|
|
||||||
let bp = Composite::new(
|
|
||||||
"root",
|
|
||||||
vec![BlueprintNode::Composite(c)],
|
|
||||||
vec![],
|
|
||||||
vec![],
|
|
||||||
vec![], // params
|
|
||||||
vec![], // output
|
|
||||||
);
|
|
||||||
let names: Vec<String> = bp.param_space().into_iter().map(|p| p.name).collect();
|
|
||||||
// aliased in place: names are the aliases, NOT two duplicate "cross.length".
|
|
||||||
assert_eq!(names, vec!["cross.shortLen".to_string(), "cross.longLen".to_string()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn out_of_range_param_alias_rejected() {
|
|
||||||
// alias names node 9 (no such interior item) -> caught at compile.
|
|
||||||
let c = Composite::new(
|
|
||||||
"cross",
|
|
||||||
vec![Sma::builder().into(), Sma::builder().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![ParamAlias { name: "bogus".into(), node: 9, slot: 0 }],
|
|
||||||
vec![OutField { node: 2, field: 0, name: "out".into() }],
|
|
||||||
);
|
|
||||||
let bp = Composite::new(
|
|
||||||
"root",
|
|
||||||
vec![BlueprintNode::Composite(c)],
|
|
||||||
vec![],
|
|
||||||
vec![
|
|
||||||
Role { name: "src".into(), targets: vec![], source: Some(ScalarKind::F64) },
|
|
||||||
],
|
|
||||||
vec![], // params
|
|
||||||
vec![], // output
|
|
||||||
);
|
|
||||||
// two Sma leaves => two i64 length slots; supply a matching vector so the
|
|
||||||
// ONLY error is the bad alias, not arity. (The Ok arm holds Box<dyn Node>,
|
|
||||||
// not Debug, so assert via the Err arm — as the other reject tests do.)
|
|
||||||
assert_eq!(
|
|
||||||
bp.compile_with_params(&[Scalar::I64(2), Scalar::I64(4)]).err(),
|
|
||||||
Some(CompileError::BadInteriorIndex),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn unaliased_params_keep_factory_names() {
|
|
||||||
// no aliases => param_space identical to the pre-#41 path-qualified names.
|
|
||||||
let c = Composite::new(
|
|
||||||
"cross",
|
|
||||||
vec![Sma::builder().into(), Sma::builder().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![],
|
|
||||||
vec![OutField { node: 2, field: 0, name: "out".into() }],
|
|
||||||
);
|
|
||||||
let bp = Composite::new(
|
|
||||||
"root",
|
|
||||||
vec![BlueprintNode::Composite(c)],
|
|
||||||
vec![],
|
|
||||||
vec![],
|
|
||||||
vec![], // params
|
|
||||||
vec![], // output
|
|
||||||
);
|
|
||||||
let names: Vec<String> = bp.param_space().into_iter().map(|p| p.name).collect();
|
|
||||||
assert_eq!(names, vec!["cross.length".to_string(), "cross.length".to_string()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn partial_aliasing_relabels_only_the_named_slot() {
|
|
||||||
// alias node 0 only; node 1 keeps its factory name; order intact.
|
|
||||||
let c = Composite::new(
|
|
||||||
"cross",
|
|
||||||
vec![Sma::builder().into(), Sma::builder().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![ParamAlias { name: "shortLen".into(), node: 0, slot: 0 }],
|
|
||||||
vec![OutField { node: 2, field: 0, name: "out".into() }],
|
|
||||||
);
|
|
||||||
let bp = Composite::new(
|
|
||||||
"root",
|
|
||||||
vec![BlueprintNode::Composite(c)],
|
|
||||||
vec![],
|
|
||||||
vec![],
|
|
||||||
vec![], // params
|
|
||||||
vec![], // output
|
|
||||||
);
|
|
||||||
let names: Vec<String> = bp.param_space().into_iter().map(|p| p.name).collect();
|
|
||||||
assert_eq!(names, vec!["cross.shortLen".to_string(), "cross.length".to_string()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn param_space_is_flat_path_qualified_and_slot_disambiguated() {
|
fn param_space_is_flat_path_qualified_and_slot_disambiguated() {
|
||||||
use aura_std::{LinComb, Sma, Sub};
|
use aura_std::{LinComb, Sma, Sub};
|
||||||
// inner composite "fast_slow": two SMAs (same type → same param name) + a Sub
|
// inner composite "fast_slow": two named SMAs (fast/slow) + a Sub
|
||||||
let fast_slow = Composite::new(
|
let fast_slow = Composite::new(
|
||||||
"fast_slow",
|
"fast_slow",
|
||||||
vec![Sma::builder().into(), Sma::builder().into(), Sub::builder().into()],
|
vec![
|
||||||
|
Sma::builder().named("fast").into(),
|
||||||
|
Sma::builder().named("slow").into(),
|
||||||
|
Sub::builder().into(),
|
||||||
|
],
|
||||||
vec![
|
vec![
|
||||||
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
||||||
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
|
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
|
||||||
@@ -2126,7 +1964,6 @@ mod tests {
|
|||||||
vec![Role {
|
vec![Role {
|
||||||
name: "price".into(),
|
name: "price".into(),
|
||||||
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], source: None, }],
|
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], source: None, }],
|
||||||
vec![],
|
|
||||||
vec![OutField { node: 2, field: 0, name: "out".into() }],
|
vec![OutField { node: 2, field: 0, name: "out".into() }],
|
||||||
);
|
);
|
||||||
// outer composite "strategy": the inner composite + a LinComb([1,-1])
|
// outer composite "strategy": the inner composite + a LinComb([1,-1])
|
||||||
@@ -2135,7 +1972,6 @@ mod tests {
|
|||||||
vec![BlueprintNode::Composite(fast_slow), LinComb::builder(2).into()],
|
vec![BlueprintNode::Composite(fast_slow), LinComb::builder(2).into()],
|
||||||
vec![],
|
vec![],
|
||||||
vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: None }],
|
vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: None }],
|
||||||
vec![],
|
|
||||||
vec![OutField { node: 0, field: 0, name: "out".into() }],
|
vec![OutField { node: 0, field: 0, name: "out".into() }],
|
||||||
);
|
);
|
||||||
let bp = Composite::new(
|
let bp = Composite::new(
|
||||||
@@ -2143,7 +1979,6 @@ mod tests {
|
|||||||
vec![BlueprintNode::Composite(strategy)],
|
vec![BlueprintNode::Composite(strategy)],
|
||||||
vec![],
|
vec![],
|
||||||
vec![],
|
vec![],
|
||||||
vec![], // params
|
|
||||||
vec![], // output
|
vec![], // output
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -2152,10 +1987,10 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
names,
|
names,
|
||||||
[
|
[
|
||||||
"strategy.fast_slow.length", // slot 0 — Sma(2)
|
"strategy.fast_slow.fast.length", // slot 0 — Sma "fast"
|
||||||
"strategy.fast_slow.length", // slot 1 — Sma(4): same name, distinct slot
|
"strategy.fast_slow.slow.length", // slot 1 — Sma "slow"
|
||||||
"strategy.weights[0]", // slot 2 — LinComb weight 0
|
"strategy.lincomb.weights[0]", // slot 2 — LinComb weight 0
|
||||||
"strategy.weights[1]", // slot 3 — LinComb weight 1
|
"strategy.lincomb.weights[1]", // slot 3 — LinComb weight 1
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
assert_eq!(space[0].kind, ScalarKind::I64);
|
assert_eq!(space[0].kind, ScalarKind::I64);
|
||||||
@@ -2180,7 +2015,11 @@ mod tests {
|
|||||||
// isolated path-qualification test above)
|
// isolated path-qualification test above)
|
||||||
let fast_slow = Composite::new(
|
let fast_slow = Composite::new(
|
||||||
"fast_slow",
|
"fast_slow",
|
||||||
vec![Sma::builder().into(), Sma::builder().into(), Sub::builder().into()],
|
vec![
|
||||||
|
Sma::builder().named("fast").into(),
|
||||||
|
Sma::builder().named("slow").into(),
|
||||||
|
Sub::builder().into(),
|
||||||
|
],
|
||||||
vec![
|
vec![
|
||||||
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
||||||
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
|
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
|
||||||
@@ -2188,10 +2027,6 @@ mod tests {
|
|||||||
vec![Role {
|
vec![Role {
|
||||||
name: "price".into(),
|
name: "price".into(),
|
||||||
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], source: None, }],
|
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], source: None, }],
|
||||||
vec![
|
|
||||||
ParamAlias { name: "fast".into(), node: 0, slot: 0 },
|
|
||||||
ParamAlias { name: "slow".into(), node: 1, slot: 0 },
|
|
||||||
],
|
|
||||||
vec![OutField { node: 2, field: 0, name: "out".into() }],
|
vec![OutField { node: 2, field: 0, name: "out".into() }],
|
||||||
);
|
);
|
||||||
// outer composite "strategy": the inner composite + a LinComb([1,-1])
|
// outer composite "strategy": the inner composite + a LinComb([1,-1])
|
||||||
@@ -2200,7 +2035,6 @@ mod tests {
|
|||||||
vec![BlueprintNode::Composite(fast_slow), LinComb::builder(2).into()],
|
vec![BlueprintNode::Composite(fast_slow), LinComb::builder(2).into()],
|
||||||
vec![],
|
vec![],
|
||||||
vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: None }],
|
vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: None }],
|
||||||
vec![],
|
|
||||||
vec![OutField { node: 0, field: 0, name: "out".into() }],
|
vec![OutField { node: 0, field: 0, name: "out".into() }],
|
||||||
);
|
);
|
||||||
let bp = Composite::new(
|
let bp = Composite::new(
|
||||||
@@ -2208,7 +2042,6 @@ mod tests {
|
|||||||
vec![BlueprintNode::Composite(strategy)],
|
vec![BlueprintNode::Composite(strategy)],
|
||||||
vec![],
|
vec![],
|
||||||
vec![],
|
vec![],
|
||||||
vec![], // params
|
|
||||||
vec![], // output
|
vec![], // output
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -2240,19 +2073,13 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn top_level_leaf_params_are_unqualified() {
|
fn top_level_leaf_params_carry_the_node_segment() {
|
||||||
use aura_std::Sma;
|
use aura_std::Sma;
|
||||||
let bp = Composite::new(
|
let bp = Composite::new("root", vec![Sma::builder().into()], vec![], vec![], vec![]);
|
||||||
"root",
|
|
||||||
vec![Sma::builder().into()],
|
|
||||||
vec![],
|
|
||||||
vec![],
|
|
||||||
vec![], // params
|
|
||||||
vec![], // output
|
|
||||||
);
|
|
||||||
let space = bp.param_space();
|
let space = bp.param_space();
|
||||||
assert_eq!(space.len(), 1);
|
assert_eq!(space.len(), 1);
|
||||||
assert_eq!(space[0].name, "length"); // no path prefix at the top level
|
// the root node now carries its own node-name segment (default "sma")
|
||||||
|
assert_eq!(space[0].name, "sma.length");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2263,7 +2090,6 @@ mod tests {
|
|||||||
vec![Sma::builder().into(), LinComb::builder(2).into()],
|
vec![Sma::builder().into(), LinComb::builder(2).into()],
|
||||||
vec![],
|
vec![],
|
||||||
vec![],
|
vec![],
|
||||||
vec![], // params
|
|
||||||
vec![], // output
|
vec![], // output
|
||||||
);
|
);
|
||||||
assert_eq!(bp.param_space(), bp.param_space()); // pure structural function (C1)
|
assert_eq!(bp.param_space(), bp.param_space()); // pure structural function (C1)
|
||||||
@@ -2278,7 +2104,6 @@ mod tests {
|
|||||||
vec![Sub::builder().into(), Add::builder().into()],
|
vec![Sub::builder().into(), Add::builder().into()],
|
||||||
vec![],
|
vec![],
|
||||||
vec![],
|
vec![],
|
||||||
vec![], // params
|
|
||||||
vec![], // output
|
vec![], // output
|
||||||
);
|
);
|
||||||
assert!(only_paramless.param_space().is_empty());
|
assert!(only_paramless.param_space().is_empty());
|
||||||
@@ -2287,7 +2112,6 @@ mod tests {
|
|||||||
vec![],
|
vec![],
|
||||||
vec![],
|
vec![],
|
||||||
vec![],
|
vec![],
|
||||||
vec![], // params
|
|
||||||
vec![], // output
|
vec![], // output
|
||||||
);
|
);
|
||||||
assert!(empty.param_space().is_empty());
|
assert!(empty.param_space().is_empty());
|
||||||
@@ -2301,11 +2125,11 @@ mod tests {
|
|||||||
Composite::new(
|
Composite::new(
|
||||||
"macd",
|
"macd",
|
||||||
vec![
|
vec![
|
||||||
Ema::builder().into(), // 0 fast EMA
|
Ema::builder().named("fast").into(), // 0 fast EMA
|
||||||
Ema::builder().into(), // 1 slow EMA
|
Ema::builder().named("slow").into(), // 1 slow EMA
|
||||||
Sub::builder().into(), // 2 macd = fast - slow
|
Sub::builder().into(), // 2 macd = fast - slow
|
||||||
Ema::builder().into(), // 3 signal EMA of macd
|
Ema::builder().named("signal").into(), // 3 signal EMA of macd
|
||||||
Sub::builder().into(), // 4 histogram = macd - signal
|
Sub::builder().into(), // 4 histogram = macd - signal
|
||||||
],
|
],
|
||||||
vec![
|
vec![
|
||||||
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
||||||
@@ -2315,11 +2139,6 @@ mod tests {
|
|||||||
Edge { from: 3, to: 4, slot: 1, from_field: 0 },
|
Edge { from: 3, to: 4, slot: 1, from_field: 0 },
|
||||||
],
|
],
|
||||||
vec![root_role("price", vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }])],
|
vec![root_role("price", vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }])],
|
||||||
vec![
|
|
||||||
ParamAlias { name: "fast".into(), node: 0, slot: 0 },
|
|
||||||
ParamAlias { name: "slow".into(), node: 1, slot: 0 },
|
|
||||||
ParamAlias { name: "signal".into(), node: 3, slot: 0 },
|
|
||||||
],
|
|
||||||
vec![
|
vec![
|
||||||
OutField { node: 2, field: 0, name: "macd".into() },
|
OutField { node: 2, field: 0, name: "macd".into() },
|
||||||
OutField { node: 3, field: 0, name: "signal".into() },
|
OutField { node: 3, field: 0, name: "signal".into() },
|
||||||
@@ -2366,7 +2185,6 @@ mod tests {
|
|||||||
vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }], // f64 -> i64 slot
|
vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }], // f64 -> i64 slot
|
||||||
vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: Some(ScalarKind::F64) }],
|
vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: Some(ScalarKind::F64) }],
|
||||||
vec![],
|
vec![],
|
||||||
vec![],
|
|
||||||
);
|
);
|
||||||
let err = root.compile_with_params(&[Scalar::I64(3)]);
|
let err = root.compile_with_params(&[Scalar::I64(3)]);
|
||||||
// kind fault caught pre-build (no panic) — same variant bootstrap would give
|
// kind fault caught pre-build (no panic) — same variant bootstrap would give
|
||||||
@@ -2384,7 +2202,6 @@ mod tests {
|
|||||||
vec![],
|
vec![],
|
||||||
vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: None }],
|
vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }], source: None }],
|
||||||
vec![],
|
vec![],
|
||||||
vec![],
|
|
||||||
);
|
);
|
||||||
// the Ok arm holds a FlatGraph (not Debug), so assert via the Err arm.
|
// the Ok arm holds a FlatGraph (not Debug), so assert via the Err arm.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ pub fn model_to_json(root: &Composite) -> String {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{Edge, OutField, ParamAlias, Role, Target};
|
use crate::{Edge, OutField, Role, Target};
|
||||||
use aura_core::{
|
use aura_core::{
|
||||||
FieldSpec, Node, NodeSchema, PortSpec, PrimitiveBuilder, Scalar,
|
FieldSpec, Node, NodeSchema, PortSpec, PrimitiveBuilder, Scalar,
|
||||||
};
|
};
|
||||||
@@ -273,7 +273,11 @@ mod tests {
|
|||||||
fn sma_cross() -> Composite {
|
fn sma_cross() -> Composite {
|
||||||
Composite::new(
|
Composite::new(
|
||||||
"sma_cross",
|
"sma_cross",
|
||||||
vec![Sma::builder().into(), Sma::builder().into(), Sub::builder().into()],
|
vec![
|
||||||
|
Sma::builder().named("fast").into(),
|
||||||
|
Sma::builder().named("slow").into(),
|
||||||
|
Sub::builder().into(),
|
||||||
|
],
|
||||||
vec![
|
vec![
|
||||||
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
||||||
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
|
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
|
||||||
@@ -283,10 +287,6 @@ mod tests {
|
|||||||
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }],
|
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }],
|
||||||
source: None,
|
source: None,
|
||||||
}],
|
}],
|
||||||
vec![
|
|
||||||
ParamAlias { name: "fast".into(), node: 0, slot: 0 },
|
|
||||||
ParamAlias { name: "slow".into(), node: 1, slot: 0 },
|
|
||||||
],
|
|
||||||
vec![OutField { node: 2, field: 0, name: "out".into() }],
|
vec![OutField { node: 2, field: 0, name: "out".into() }],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -315,7 +315,6 @@ mod tests {
|
|||||||
targets: vec![Target { node: 0, slot: 0 }], // price -> sma_cross role 0
|
targets: vec![Target { node: 0, slot: 0 }], // price -> sma_cross role 0
|
||||||
source: Some(ScalarKind::F64),
|
source: Some(ScalarKind::F64),
|
||||||
}],
|
}],
|
||||||
vec![], // params: the interior sma_cross carries the aliases
|
|
||||||
vec![], // output: the root ends in a sink, no re-export
|
vec![], // output: the root ends in a sink, no re-export
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -331,7 +330,6 @@ mod tests {
|
|||||||
Role { name: "a".into(), targets: vec![Target { node: 0, slot: 0 }], source: None },
|
Role { name: "a".into(), targets: vec![Target { node: 0, slot: 0 }], source: None },
|
||||||
Role { name: "b".into(), targets: vec![Target { node: 1, slot: 0 }], source: None },
|
Role { name: "b".into(), targets: vec![Target { node: 1, slot: 0 }], source: None },
|
||||||
],
|
],
|
||||||
vec![],
|
|
||||||
vec![OutField { node: 0, field: 0, name: "v".into() }],
|
vec![OutField { node: 0, field: 0, name: "v".into() }],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -358,7 +356,6 @@ mod tests {
|
|||||||
source: Some(ScalarKind::F64),
|
source: Some(ScalarKind::F64),
|
||||||
}],
|
}],
|
||||||
vec![],
|
vec![],
|
||||||
vec![],
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ mod report;
|
|||||||
mod sweep;
|
mod sweep;
|
||||||
|
|
||||||
pub use blueprint::{
|
pub use blueprint::{
|
||||||
aliases_on, signature_of, BindError, Binder, BlueprintNode, CompileError, Composite, OutField,
|
signature_of, BindError, Binder, BlueprintNode, CompileError, Composite, OutField, Role,
|
||||||
ParamAlias, Role, SweepBinder,
|
SweepBinder,
|
||||||
};
|
};
|
||||||
pub use graph_model::model_to_json;
|
pub use graph_model::model_to_json;
|
||||||
pub use harness::{BootstrapError, Edge, FlatGraph, Harness, SourceSpec, Target};
|
pub use harness::{BootstrapError, Edge, FlatGraph, Harness, SourceSpec, Target};
|
||||||
|
|||||||
@@ -170,8 +170,7 @@ where
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
f64_field, summarize, BlueprintNode, Composite, Edge, OutField, ParamAlias, Role,
|
f64_field, summarize, BlueprintNode, Composite, Edge, OutField, Role, RunManifest, Target,
|
||||||
RunManifest, Target,
|
|
||||||
};
|
};
|
||||||
use aura_core::{Firing, ParamSpec, Scalar, ScalarKind, Timestamp};
|
use aura_core::{Firing, ParamSpec, Scalar, ScalarKind, Timestamp};
|
||||||
use aura_std::{Exposure, Recorder, SimBroker, Sma, Sub};
|
use aura_std::{Exposure, Recorder, SimBroker, Sma, Sub};
|
||||||
@@ -275,7 +274,11 @@ mod tests {
|
|||||||
fn sma_cross() -> Composite {
|
fn sma_cross() -> Composite {
|
||||||
Composite::new(
|
Composite::new(
|
||||||
"sma_cross",
|
"sma_cross",
|
||||||
vec![Sma::builder().into(), Sma::builder().into(), Sub::builder().into()],
|
vec![
|
||||||
|
Sma::builder().named("fast").into(),
|
||||||
|
Sma::builder().named("slow").into(),
|
||||||
|
Sub::builder().into(),
|
||||||
|
],
|
||||||
vec![
|
vec![
|
||||||
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
||||||
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
|
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
|
||||||
@@ -285,10 +288,6 @@ mod tests {
|
|||||||
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }],
|
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }],
|
||||||
source: None,
|
source: None,
|
||||||
}],
|
}],
|
||||||
vec![
|
|
||||||
ParamAlias { name: "fast".into(), node: 0, slot: 0 },
|
|
||||||
ParamAlias { name: "slow".into(), node: 1, slot: 0 },
|
|
||||||
],
|
|
||||||
vec![OutField { node: 2, field: 0, name: "out".into() }],
|
vec![OutField { node: 2, field: 0, name: "out".into() }],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -323,7 +322,6 @@ mod tests {
|
|||||||
targets: vec![Target { node: 0, slot: 0 }, Target { node: 2, slot: 1 }],
|
targets: vec![Target { node: 0, slot: 0 }, Target { node: 2, slot: 1 }],
|
||||||
source: Some(ScalarKind::F64),
|
source: Some(ScalarKind::F64),
|
||||||
}],
|
}],
|
||||||
vec![], // params: the interior sma_cross + Exposure carry the knobs
|
|
||||||
vec![], // output: the root ends in sinks
|
vec![], // output: the root ends in sinks
|
||||||
);
|
);
|
||||||
(bp, rx_eq, rx_ex)
|
(bp, rx_eq, rx_ex)
|
||||||
|
|||||||
+27
-24
@@ -349,30 +349,33 @@ its producing node as a `name := …` binding; originally `[out:<name>]` markers
|
|||||||
but are dropped at lowering — `ItemLowering::Composite.output` is `Vec<(usize,
|
but are dropped at lowering — `ItemLowering::Composite.output` is `Vec<(usize,
|
||||||
usize)>`, raw index pairs only, so the compilat is name-free (verified: the
|
usize)>`, raw index pairs only, so the compilat is name-free (verified: the
|
||||||
compiled-view render stayed bit-identical across this change).
|
compiled-view render stayed bit-identical across this change).
|
||||||
**Realization (cycle 0019 — name the composite boundary, #41).** The same
|
**Realization (cycle 0019 — name the composite boundary, #41; param-overlay retired,
|
||||||
named-projection shape now covers the other two boundary edge-kinds, so **all
|
cycle 0031).** The named-projection shape covers the surviving boundary edge-kinds:
|
||||||
three** are uniform: `input_roles` is a `Vec<Role { name, targets }>` (was a bare
|
`input_roles` is a `Vec<Role { name, targets }>` (was a bare `Vec<Vec<Target>>`),
|
||||||
`Vec<Vec<Target>>`) and a composite carries `params: Vec<ParamAlias { name, node,
|
alongside `output: Vec<OutField>`. Each is an ordered, positionally-indexed
|
||||||
slot }>`, alongside `output: Vec<OutField>`. Each is an ordered, positionally-indexed
|
**named projection** of interior handles. **Param projection is no longer a
|
||||||
**named projection** of interior handles. Param aliasing is a **pure naming overlay**,
|
composite overlay** (the index-addressed `ParamAlias` was retired in cycle 0031):
|
||||||
not curation: `param_space()` relabels an aliased slot's surface name **in place** —
|
a node's surface param name flows from its own **instance name** — every node
|
||||||
slot order, arity, and kinds are unchanged, so the sweep surface is identical and the
|
carries a name (default = its lowercased type label, override via `.named()`), and
|
||||||
`param_space_mirrors_compiled_flat_node_param_order` anchor stays green. Like the
|
`param_space()` is uniformly `<node>.<param>` at every level including the root. A
|
||||||
output names, role and param **names** are **non-load-bearing** (C23): they live at
|
same-type fan-in is distinguished by naming the colliding legs, the same single act
|
||||||
the blueprint boundary and in render (`[in:<name>]` / `[param:<name>]` markers) but
|
that qualifies their param paths. Like the output and role names, **node names are
|
||||||
are dropped at lowering — the compilat is wired by raw index and a dangling alias is
|
non-load-bearing** (C23): they live at the blueprint boundary and in render but are
|
||||||
rejected at compile (`BadInteriorIndex`), not silently lowered. The full composite
|
dropped at lowering — the compilat is wired by raw index. The full composite
|
||||||
boundary signature (named inputs, params, multi-outputs) is now legible without
|
boundary signature (named inputs, multi-outputs) and the per-node param path are
|
||||||
changing the compilat.
|
legible without changing the compilat.
|
||||||
**Refinement (fan-in distinguishability, 2026-06-08).** A fan-in node (>1 input)
|
**Refinement (fan-in distinguishability, 2026-06-08; node-name keying, cycle 0031).**
|
||||||
is well-formed only if its colliding sources — sources with equal recursive
|
A fan-in node (>1 input) is well-formed only if its colliding sources — sources
|
||||||
signatures (type initial + alias initials + recursive input signatures) — do not
|
with equal recursive **node-name** signatures (each node contributes its name —
|
||||||
hide an unnamed configuration axis: a collision is a `CompileError`
|
the instance name, default = the lowercased type label — then its inputs'
|
||||||
(`IndistinguishableFanIn`) when at least one colliding source carries an
|
signatures in slot order, stopping at a named source) — do not hide an unnamed
|
||||||
unaliased param slot. Genuinely-interchangeable sources (equal signatures, no
|
configuration axis: a collision is a `CompileError` (`IndistinguishableFanIn`)
|
||||||
param) stay legal. Construction-phase only; the compilat stays name-free (C23).
|
when at least one colliding source carries a **param** (a param-bearing same-name
|
||||||
The graph view renders each fan-in input as the shortest sibling-unique prefix
|
collision), resolved by giving the colliding nodes distinct names. Paramless
|
||||||
of its source signature.
|
interchangeable same-name sources (equal signatures, no param) stay legal.
|
||||||
|
Construction-phase only; the compilat stays name-free (C23). The graph view
|
||||||
|
renders each fan-in input as the shortest sibling-unique prefix of its source
|
||||||
|
signature.
|
||||||
|
|
||||||
### C10 — Strategy output is an intent/exposure stream; position management is a decoupled derived layer; brokers are downstream nodes
|
### C10 — Strategy output is an intent/exposure stream; position management is a decoupled derived layer; brokers are downstream nodes
|
||||||
**Guarantee.** A strategy's primary, backtestable output is **not** an equity
|
**Guarantee.** A strategy's primary, backtestable output is **not** an equity
|
||||||
|
|||||||
Reference in New Issue
Block a user