131 lines
3.9 KiB
Go
131 lines
3.9 KiB
Go
package synthetic
|
|
|
|
import (
|
|
"math"
|
|
"testing"
|
|
)
|
|
|
|
// evalDef compiles a SignalDef and evaluates it against per-source values keyed
|
|
// by source node id.
|
|
func evalDef(t *testing.T, def SignalDef, srcVals map[string]float64) float64 {
|
|
t.Helper()
|
|
rg, err := compileGraph(def)
|
|
if err != nil {
|
|
t.Fatalf("compileGraph: %v", err)
|
|
}
|
|
out, err := rg.eval(srcVals)
|
|
if err != nil {
|
|
t.Fatalf("eval: %v", err)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// TestGraphMultiInputDAG verifies that an intermediate op can take two
|
|
// independently-wired sources — the capability the old linear pipeline lacked.
|
|
func TestGraphMultiInputDAG(t *testing.T) {
|
|
def := SignalDef{
|
|
Name: "diff",
|
|
Graph: &Graph{
|
|
Output: "out",
|
|
Nodes: []GraphNode{
|
|
{ID: "a", Kind: "source", DS: "x", Signal: "left"},
|
|
{ID: "b", Kind: "source", DS: "x", Signal: "right"},
|
|
{ID: "sub", Kind: "op", Op: "subtract", Inputs: []string{"a", "b"}},
|
|
{ID: "out", Kind: "output", Inputs: []string{"sub"}},
|
|
},
|
|
},
|
|
}
|
|
got := evalDef(t, def, map[string]float64{"a": 10, "b": 3})
|
|
if got != 7 {
|
|
t.Errorf("subtract DAG: want 7, got %v", got)
|
|
}
|
|
}
|
|
|
|
// TestGraphExprNamedInputs verifies expr nodes bind arbitrary named inputs in
|
|
// wired order.
|
|
func TestGraphExprNamedInputs(t *testing.T) {
|
|
def := SignalDef{
|
|
Name: "formula",
|
|
Graph: &Graph{
|
|
Output: "out",
|
|
Nodes: []GraphNode{
|
|
{ID: "a", Kind: "source", DS: "x", Signal: "p"},
|
|
{ID: "b", Kind: "source", DS: "x", Signal: "q"},
|
|
{ID: "e", Kind: "op", Op: "expr", Inputs: []string{"a", "b"},
|
|
Params: map[string]any{"expr": "price * qty", "vars": []any{"price", "qty"}}},
|
|
{ID: "out", Kind: "output", Inputs: []string{"e"}},
|
|
},
|
|
},
|
|
}
|
|
got := evalDef(t, def, map[string]float64{"a": 4, "b": 2.5})
|
|
if math.Abs(got-10) > 1e-9 {
|
|
t.Errorf("expr named inputs: want 10, got %v", got)
|
|
}
|
|
}
|
|
|
|
// TestGraphFanInToExpr exercises a non-trivial DAG: two ops feeding one expr.
|
|
func TestGraphFanInToExpr(t *testing.T) {
|
|
def := SignalDef{
|
|
Name: "combo",
|
|
Graph: &Graph{
|
|
Output: "out",
|
|
Nodes: []GraphNode{
|
|
{ID: "a", Kind: "source", DS: "x", Signal: "p"},
|
|
{ID: "b", Kind: "source", DS: "x", Signal: "q"},
|
|
{ID: "g", Kind: "op", Op: "gain", Inputs: []string{"a"}, Params: map[string]any{"gain": 2.0}},
|
|
{ID: "o", Kind: "op", Op: "offset", Inputs: []string{"b"}, Params: map[string]any{"offset": 1.0}},
|
|
{ID: "e", Kind: "op", Op: "expr", Inputs: []string{"g", "o"}, Params: map[string]any{"expr": "a + b"}},
|
|
{ID: "out", Kind: "output", Inputs: []string{"e"}},
|
|
},
|
|
},
|
|
}
|
|
// g = 5*2 = 10 ; o = 4+1 = 5 ; a+b = 15
|
|
got := evalDef(t, def, map[string]float64{"a": 5, "b": 4})
|
|
if math.Abs(got-15) > 1e-9 {
|
|
t.Errorf("fan-in DAG: want 15, got %v", got)
|
|
}
|
|
}
|
|
|
|
// TestGraphLegacyConversion verifies the linear Inputs+Pipeline form still
|
|
// evaluates correctly via the graph runtime.
|
|
func TestGraphLegacyConversion(t *testing.T) {
|
|
def := SignalDef{
|
|
Name: "legacy",
|
|
DS: "x",
|
|
Signal: "p",
|
|
Pipeline: []NodeDef{{Type: "gain", Params: map[string]any{"gain": 3.0}}},
|
|
}
|
|
rg, err := compileGraph(def)
|
|
if err != nil {
|
|
t.Fatalf("compileGraph: %v", err)
|
|
}
|
|
if len(rg.sources) != 1 {
|
|
t.Fatalf("want 1 source, got %d", len(rg.sources))
|
|
}
|
|
got, err := rg.eval(map[string]float64{rg.sources[0].id: 4})
|
|
if err != nil {
|
|
t.Fatalf("eval: %v", err)
|
|
}
|
|
if got != 12 {
|
|
t.Errorf("legacy gain: want 12, got %v", got)
|
|
}
|
|
}
|
|
|
|
// TestGraphCycleRejected ensures a cyclic graph is refused at compile time.
|
|
func TestGraphCycleRejected(t *testing.T) {
|
|
def := SignalDef{
|
|
Name: "cyclic",
|
|
Graph: &Graph{
|
|
Output: "out",
|
|
Nodes: []GraphNode{
|
|
{ID: "a", Kind: "op", Op: "gain", Inputs: []string{"b"}, Params: map[string]any{"gain": 1.0}},
|
|
{ID: "b", Kind: "op", Op: "gain", Inputs: []string{"a"}, Params: map[string]any{"gain": 1.0}},
|
|
{ID: "out", Kind: "output", Inputs: []string{"a"}},
|
|
},
|
|
},
|
|
}
|
|
if _, err := compileGraph(def); err == nil {
|
|
t.Error("expected cycle to be rejected")
|
|
}
|
|
}
|