use super::*;
#[test]
fn test_flowbuilder_new() {
let graph = FlowBuilder::new()
.tag("input")
.through(Linear::on_device(3, 2, crate::tensor::test_device()).unwrap())
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0, 3.0], &[1, 3]), false);
let y = graph.forward(&x).unwrap();
assert_eq!(y.shape(), vec![1, 2]);
}
#[test]
fn test_deferred_error_carries_chain_position() {
let dev = crate::tensor::test_device();
let result = FlowBuilder::from(Linear::on_device(4, 8, dev).unwrap())
.through(Linear::on_device(8, 8, dev).unwrap())
.merge(MergeOp::Add) .build();
let msg = result.err().expect("expected build to fail").to_string();
assert!(
msg.contains("merge requires multiple streams"),
"expected the guard message; got: {msg}"
);
assert!(
msg.contains("after node '"),
"GD15: deferred error must carry chain position; got: {msg}"
);
}
#[test]
fn test_unknown_port_name_errors_at_build() {
use crate::graph::node::{DEFAULT_INPUT, DEFAULT_OUTPUT, Edge, ExposedPort, Node};
use indexmap::IndexMap;
use std::collections::HashSet;
let mk_node = |id: &str| Node {
id: id.to_string(),
input_ports: vec![DEFAULT_INPUT.to_string()],
output_ports: vec![DEFAULT_OUTPUT.to_string()],
run: Box::new(|inputs| Ok(inputs.to_vec())),
module: None,
ref_forward: None,
trace_buf: None,
named_trace_buf: None,
loop_ports: None,
};
let mut nodes = IndexMap::new();
nodes.insert("a".to_string(), mk_node("a"));
nodes.insert("b".to_string(), mk_node("b"));
let result = Graph::build(
nodes,
vec![Edge {
from_node: "a".into(),
from_port: DEFAULT_OUTPUT.into(),
to_node: "b".into(),
to_port: "bogus".into(),
}],
vec![ExposedPort {
name: "input".into(),
node_id: "a".into(),
port: DEFAULT_INPUT.into(),
}],
vec![ExposedPort {
name: "output".into(),
node_id: "b".into(),
port: DEFAULT_OUTPUT.into(),
}],
HashMap::new(),
Vec::new(),
HashMap::new(),
None,
HashSet::new(),
false,
);
let msg = match result {
Ok(_) => panic!("build must reject the unknown port"),
Err(e) => e.to_string(),
};
assert!(
msg.contains("bogus") && msg.contains("edge target") && msg.contains("\"b\""),
"error must name the port, the resolution kind, and the node: {msg}"
);
}
#[test]
fn test_forward_ref() {
let graph = FlowBuilder::from(Identity)
.through(NilSafeAdd)
.using(&["memory"])
.through(Identity)
.tag("memory")
.build()
.unwrap();
assert!(graph.has_state());
let x = Variable::new(from_f32(&[1.0, 2.0], &[1, 2]), false);
let y1 = graph.forward(&x).unwrap();
let d1 = y1.data().to_f32_vec().unwrap();
assert!((d1[0] - 1.0).abs() < 1e-5, "pass1[0]: got {}", d1[0]);
assert!((d1[1] - 2.0).abs() < 1e-5, "pass1[1]: got {}", d1[1]);
let y2 = graph.forward(&x).unwrap();
let d2 = y2.data().to_f32_vec().unwrap();
assert!((d2[0] - 2.0).abs() < 1e-5, "pass2[0]: got {}", d2[0]);
assert!((d2[1] - 4.0).abs() < 1e-5, "pass2[1]: got {}", d2[1]);
let y3 = graph.forward(&x).unwrap();
let d3 = y3.data().to_f32_vec().unwrap();
assert!((d3[0] - 3.0).abs() < 1e-5, "pass3[0]: got {}", d3[0]);
assert!((d3[1] - 6.0).abs() < 1e-5, "pass3[1]: got {}", d3[1]);
}
#[test]
fn test_forward_ref_reset_state() {
let graph = FlowBuilder::from(Identity)
.through(NilSafeAdd)
.using(&["memory"])
.through(Identity)
.tag("memory")
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0], &[1, 2]), false);
graph.forward(&x).unwrap();
graph.forward(&x).unwrap();
let y_before = graph.forward(&x).unwrap();
let d_before = y_before.data().to_f32_vec().unwrap();
assert!((d_before[0] - 3.0).abs() < 1e-5);
graph.reset_state();
let y_after = graph.forward(&x).unwrap();
let d_after = y_after.data().to_f32_vec().unwrap();
assert!((d_after[0] - 1.0).abs() < 1e-5, "after reset: got {}", d_after[0]);
}
#[test]
fn test_forward_ref_detach_state() {
let graph = FlowBuilder::from(Identity)
.through(NilSafeAdd)
.using(&["memory"])
.through(Identity)
.tag("memory")
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0], &[1, 2]), true);
let y1 = graph.forward(&x).unwrap();
let _ = y1.sum().unwrap();
graph.detach_state();
let y2 = graph.forward(&x).unwrap();
let d2 = y2.data().to_f32_vec().unwrap();
assert!((d2[0] - 2.0).abs() < 1e-5, "detach preserves values: got {}", d2[0]);
}
#[test]
fn test_forward_ref_backward() {
let graph = FlowBuilder::from(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap())
.through(NilSafeAdd)
.using(&["memory"])
.through(Identity)
.tag("memory")
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0], &[1, 2]), true);
let y = graph.forward(&x).unwrap();
let loss = y.sum().unwrap();
loss.backward().unwrap();
assert!(x.grad().is_some(), "input should have gradient");
for p in graph.parameters() {
assert!(p.variable.grad().is_some(), "{} should have gradient", p.name);
}
}
#[test]
fn test_forward_ref_unresolved_error() {
let result = FlowBuilder::from(Identity)
.through(NilSafeAdd)
.using(&["nonexistent"])
.build();
assert!(result.is_err());
}
#[test]
fn test_forward_ref_mixed_refs() {
let graph = FlowBuilder::from(Identity)
.tag("ctx")
.through(AddRefModule)
.using(&["ctx"])
.through(NilSafeAdd)
.using(&["memory"])
.through(Identity)
.tag("memory")
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0], &[1, 2]), false);
let y1 = graph.forward(&x).unwrap();
let d1 = y1.data().to_f32_vec().unwrap();
assert!((d1[0] - 2.0).abs() < 1e-5, "mixed pass1[0]: got {}", d1[0]);
let y2 = graph.forward(&x).unwrap();
let d2 = y2.data().to_f32_vec().unwrap();
assert!((d2[0] - 4.0).abs() < 1e-5, "mixed pass2[0]: got {}", d2[0]);
}
#[test]
fn test_switch_selects_branch() {
let graph = FlowBuilder::from(Identity)
.switch(FixedSelector::new(1), vec![Box::new(Doubler), Box::new(Tripler)])
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0], &[1, 2]), false);
let y = graph.forward(&x).unwrap();
let data = y.data().to_f32_vec().unwrap();
assert!((data[0] - 3.0).abs() < 1e-5, "triple [1]=3, got {}", data[0]);
assert!((data[1] - 6.0).abs() < 1e-5, "triple [2]=6, got {}", data[1]);
}
#[test]
fn test_switch_branch0() {
let graph = FlowBuilder::from(Identity)
.switch(FixedSelector::new(0), vec![Box::new(Doubler), Box::new(Tripler)])
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0], &[1, 2]), false);
let y = graph.forward(&x).unwrap();
let data = y.data().to_f32_vec().unwrap();
assert!((data[0] - 2.0).abs() < 1e-5, "double [1]=2, got {}", data[0]);
assert!((data[1] - 4.0).abs() < 1e-5, "double [2]=4, got {}", data[1]);
}
#[test]
fn test_switch_backward() {
let graph = FlowBuilder::from(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap())
.switch(FixedSelector::new(0), vec![
Box::new(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap()),
Box::new(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap()),
])
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0], &[1, 2]), true);
let y = graph.forward(&x).unwrap();
let loss = y.sum().unwrap();
loss.backward().unwrap();
assert!(x.grad().is_some());
}
#[test]
fn test_switch_parameters() {
let graph = FlowBuilder::from(Identity)
.switch(
Linear::on_device(2, 1, crate::tensor::test_device()).unwrap(),
vec![
Box::new(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap()),
Box::new(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap()),
],
)
.build()
.unwrap();
let params = graph.parameters();
assert_eq!(params.len(), 6);
}
struct EqualRouter(usize);
impl Module for EqualRouter {
fn forward(&self, input: &Variable) -> Result<Variable> {
let batch = input.shape()[0];
let w = 1.0 / self.0 as f32;
let data = vec![w; batch as usize * self.0];
Ok(Variable::new(
Tensor::from_f32(&data, &[batch, self.0 as i64], crate::tensor::test_device())?,
false,
))
}
fn parameters(&self) -> Vec<Parameter> { vec![] }
}
#[test]
fn test_gate_equal_weights() {
let graph = FlowBuilder::from(Identity)
.gate(EqualRouter(2), vec![Box::new(Doubler), Box::new(Tripler)])
.build()
.unwrap();
let x = Variable::new(from_f32(&[2.0, 4.0], &[1, 2]), false);
let y = graph.forward(&x).unwrap();
let data = y.data().to_f32_vec().unwrap();
assert!((data[0] - 5.0).abs() < 1e-5, "gate[0]=5, got {}", data[0]);
assert!((data[1] - 10.0).abs() < 1e-5, "gate[1]=10, got {}", data[1]);
}
#[test]
fn test_gate_backward() {
let graph = FlowBuilder::from(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap())
.gate(
Linear::on_device(2, 2, crate::tensor::test_device()).unwrap(),
vec![
Box::new(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap()),
Box::new(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap()),
],
)
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0], &[1, 2]), true);
let y = graph.forward(&x).unwrap();
let loss = y.sum().unwrap();
loss.backward().unwrap();
assert!(x.grad().is_some());
for p in graph.parameters() {
assert!(p.variable.grad().is_some(), "{} should have gradient", p.name);
}
}
#[test]
fn test_gate_parameters() {
let graph = FlowBuilder::from(Identity)
.gate(
Linear::on_device(2, 2, crate::tensor::test_device()).unwrap(),
vec![
Box::new(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap()),
Box::new(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap()),
],
)
.build()
.unwrap();
let params = graph.parameters();
assert_eq!(params.len(), 6);
}
#[test]
fn test_softmax_router_gate() {
let graph = FlowBuilder::from(Identity)
.gate(
SoftmaxRouter::on_device(2, 2, crate::tensor::test_device()).unwrap(),
vec![Box::new(Doubler), Box::new(Tripler)],
)
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0], &[1, 2]), false);
let y = graph.forward(&x).unwrap();
assert_eq!(y.shape(), vec![1, 2]);
let params = graph.parameters();
assert_eq!(params.len(), 2);
}
#[test]
fn test_softmax_router_backward() {
let graph = FlowBuilder::from(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap())
.gate(
SoftmaxRouter::on_device(2, 2, crate::tensor::test_device()).unwrap(),
vec![
Box::new(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap()),
Box::new(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap()),
],
)
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0], &[1, 2]), true);
let y = graph.forward(&x).unwrap();
let loss = y.sum().unwrap();
loss.backward().unwrap();
assert!(x.grad().is_some());
for p in graph.parameters() {
assert!(p.variable.grad().is_some(), "{} missing gradient", p.name);
}
}
#[test]
fn test_sigmoid_router_gate() {
let graph = FlowBuilder::from(Identity)
.gate(
SigmoidRouter::on_device(2, 2, crate::tensor::test_device()).unwrap(),
vec![Box::new(Doubler), Box::new(Tripler)],
)
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0], &[1, 2]), false);
let y = graph.forward(&x).unwrap();
assert_eq!(y.shape(), vec![1, 2]);
}
#[test]
fn test_fixed_selector_switch() {
let graph = FlowBuilder::from(Identity)
.switch(FixedSelector::new(1), vec![Box::new(Doubler), Box::new(Tripler)])
.build()
.unwrap();
let x = Variable::new(from_f32(&[2.0, 3.0], &[1, 2]), false);
let y = graph.forward(&x).unwrap();
let data = y.data().to_f32_vec().unwrap();
assert!((data[0] - 6.0).abs() < 1e-5, "triple 2=6, got {}", data[0]);
assert!((data[1] - 9.0).abs() < 1e-5, "triple 3=9, got {}", data[1]);
}
#[test]
fn test_argmax_selector_switch() {
let graph = FlowBuilder::from(Identity)
.switch(
ArgmaxSelector::on_device(2, 2, crate::tensor::test_device()).unwrap(),
vec![Box::new(Doubler), Box::new(Tripler)],
)
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0], &[1, 2]), false);
let y = graph.forward(&x).unwrap();
assert_eq!(y.shape(), vec![1, 2]);
assert_eq!(graph.parameters().len(), 2);
}
#[test]
fn test_threshold_halt_while() {
let graph = FlowBuilder::from(Identity)
.loop_body(Doubler)
.while_cond(ThresholdHalt::new(10.0), 20)
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0], &[1, 2]), false);
let y = graph.forward(&x).unwrap();
let data = y.data().to_f32_vec().unwrap();
assert!((data[0] - 8.0).abs() < 1e-5, "expected 8, got {}", data[0]);
assert!((data[1] - 16.0).abs() < 1e-5, "expected 16, got {}", data[1]);
}
#[test]
fn test_threshold_halt_until() {
let graph = FlowBuilder::from(Identity)
.loop_body(Doubler)
.until_cond(ThresholdHalt::new(10.0), 20)
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0], &[1, 2]), false);
let y = graph.forward(&x).unwrap();
let data = y.data().to_f32_vec().unwrap();
assert!((data[0] - 8.0).abs() < 1e-5, "expected 8, got {}", data[0]);
assert!((data[1] - 16.0).abs() < 1e-5, "expected 16, got {}", data[1]);
}
#[test]
fn test_threshold_halt_immediate() {
let graph = FlowBuilder::from(Identity)
.loop_body(Doubler)
.while_cond(ThresholdHalt::new(0.5), 20)
.build()
.unwrap();
let x = Variable::new(from_f32(&[1.0, 2.0], &[1, 2]), false);
let y = graph.forward(&x).unwrap();
let data = y.data().to_f32_vec().unwrap();
assert!((data[0] - 1.0).abs() < 1e-5, "expected 1, got {}", data[0]);
assert!((data[1] - 2.0).abs() < 1e-5, "expected 2, got {}", data[1]);
}
#[test]
fn test_learned_halt_parameters() {
let graph = FlowBuilder::from(Identity)
.loop_body(Linear::on_device(2, 2, crate::tensor::test_device()).unwrap())
.until_cond(LearnedHalt::on_device(2, crate::tensor::test_device()).unwrap(), 5)
.build()
.unwrap();
let params = graph.parameters();
assert_eq!(params.len(), 4);
}