package dsp import ( "math" "testing" ) func TestSampleRoundTrip(t *testing.T) { s := Scalar(3.5) if s.IsArray { t.Error("Scalar should not be an array") } if s.Type() != ValScalar { t.Errorf("Scalar type: want ValScalar, got %v", s.Type()) } if s.AsAny() != 3.5 { t.Errorf("Scalar AsAny: want 3.5, got %v", s.AsAny()) } if got := s.AsArray(); len(got) != 1 || got[0] != 3.5 { t.Errorf("Scalar AsArray: want [3.5], got %v", got) } a := Array([]float64{1, 2, 3}) if !a.IsArray { t.Error("Array should be an array") } if a.Type() != ValArray { t.Errorf("Array type: want ValArray, got %v", a.Type()) } got, ok := a.AsAny().([]float64) if !ok || len(got) != 3 { t.Errorf("Array AsAny: want []float64 len 3, got %v", a.AsAny()) } } func TestApplyElementwiseAllScalar(t *testing.T) { n := &AddNode{} out, err := ApplyElementwise(n, []Sample{Scalar(2), Scalar(3)}, map[string]any{}) if err != nil { t.Fatal(err) } if out.IsArray || out.F != 5 { t.Errorf("all-scalar add: want scalar 5, got %v", out) } } func TestApplyElementwiseBroadcast(t *testing.T) { // array ⊕ scalar: scalar is a constant broadcast across the array. n := &AddNode{} out, err := ApplyElementwise(n, []Sample{Array([]float64{1, 2, 3}), Scalar(10)}, map[string]any{}) if err != nil { t.Fatal(err) } if !out.IsArray { t.Fatalf("array+scalar: want array, got %v", out) } want := []float64{11, 12, 13} for i, v := range want { if out.Arr[i] != v { t.Errorf("array+scalar[%d]: want %v, got %v", i, v, out.Arr[i]) } } } func TestApplyElementwiseArrayArray(t *testing.T) { n := &MultiplyNode{} out, err := ApplyElementwise(n, []Sample{Array([]float64{1, 2, 3}), Array([]float64{4, 5, 6})}, map[string]any{}) if err != nil { t.Fatal(err) } want := []float64{4, 10, 18} for i, v := range want { if out.Arr[i] != v { t.Errorf("array*array[%d]: want %v, got %v", i, v, out.Arr[i]) } } } func TestApplyElementwiseLengthMismatch(t *testing.T) { n := &AddNode{} _, err := ApplyElementwise(n, []Sample{Array([]float64{1, 2}), Array([]float64{1, 2, 3})}, map[string]any{}) if err == nil { t.Error("expected length-mismatch error") } } func TestReductionNodes(t *testing.T) { arr := []Sample{Array([]float64{2, 4, 6, 8})} cases := []struct { name string node ArrayNode want float64 }{ {"sum", &SumNode{}, 20}, {"mean", &MeanNode{}, 5}, {"min", &MinNode{}, 2}, {"max", &MaxNode{}, 8}, {"length", &LengthNode{}, 4}, {"index", &IndexNode{I: 2}, 6}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { out, err := tc.node.ProcessSample(arr, map[string]any{}) if err != nil { t.Fatal(err) } if out.IsArray || out.F != tc.want { t.Errorf("%s: want scalar %v, got %v", tc.name, tc.want, out) } }) } } func TestIndexNodeOutOfRange(t *testing.T) { n := &IndexNode{I: 9} _, err := n.ProcessSample([]Sample{Array([]float64{1, 2, 3})}, map[string]any{}) if err == nil { t.Error("expected out-of-range error") } } func TestSliceNode(t *testing.T) { n := &SliceNode{Start: 1, End: 3} out, err := n.ProcessSample([]Sample{Array([]float64{10, 20, 30, 40})}, map[string]any{}) if err != nil { t.Fatal(err) } want := []float64{20, 30} if len(out.Arr) != len(want) { t.Fatalf("slice: want len %d, got %d", len(want), len(out.Arr)) } for i, v := range want { if out.Arr[i] != v { t.Errorf("slice[%d]: want %v, got %v", i, v, out.Arr[i]) } } } func TestFFTMagnitude(t *testing.T) { // A constant signal has all energy in bin 0 (the DC term equals the sum). x := []float64{1, 1, 1, 1} mag := fftMagnitude(x) if len(mag) != 4 { t.Fatalf("fft len: want 4, got %d", len(mag)) } if math.Abs(mag[0]-4) > 1e-9 { t.Errorf("fft DC bin: want 4, got %v", mag[0]) } for k := 1; k < len(mag); k++ { if math.Abs(mag[k]) > 1e-9 { t.Errorf("fft bin %d: want ~0, got %v", k, mag[k]) } } } func TestFFTSingleTone(t *testing.T) { // One full cycle of a cosine over 8 samples → energy in bins 1 and N-1. n := 8 x := make([]float64, n) for i := range x { x[i] = math.Cos(2 * math.Pi * float64(i) / float64(n)) } mag := fftMagnitude(x) if math.Abs(mag[1]-float64(n)/2) > 1e-6 { t.Errorf("fft tone bin 1: want %v, got %v", float64(n)/2, mag[1]) } if math.Abs(mag[n-1]-float64(n)/2) > 1e-6 { t.Errorf("fft tone bin %d: want %v, got %v", n-1, float64(n)/2, mag[n-1]) } } func TestOpOutputType(t *testing.T) { cases := []struct { op string in []ValType want ValType wantErr bool }{ // reductions → scalar regardless of input {"sum", []ValType{ValArray}, ValScalar, false}, {"mean", []ValType{ValScalar}, ValScalar, false}, {"index", []ValType{ValUnknown}, ValScalar, false}, // array producers require array, yield array {"fft", []ValType{ValArray}, ValArray, false}, {"slice", []ValType{ValUnknown}, ValArray, false}, {"fft", []ValType{ValScalar}, ValUnknown, true}, // scalar-only reject arrays {"moving_average", []ValType{ValScalar}, ValScalar, false}, {"lua", []ValType{ValArray}, ValUnknown, true}, {"rms", []ValType{ValUnknown}, ValScalar, false}, // elementwise: array if any array, scalar if all scalar, else unknown {"add", []ValType{ValScalar, ValScalar}, ValScalar, false}, {"add", []ValType{ValArray, ValScalar}, ValArray, false}, {"gain", []ValType{ValUnknown}, ValUnknown, false}, {"expr", []ValType{ValArray, ValScalar}, ValArray, false}, } for _, tc := range cases { got, err := OpOutputType(tc.op, tc.in) if tc.wantErr { if err == nil { t.Errorf("OpOutputType(%q,%v): expected error", tc.op, tc.in) } continue } if err != nil { t.Errorf("OpOutputType(%q,%v): unexpected error %v", tc.op, tc.in, err) continue } if got != tc.want { t.Errorf("OpOutputType(%q,%v): want %v, got %v", tc.op, tc.in, tc.want, got) } } }