// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: MIT package signal import ( "math" "testing" "sourcedock.dev/petrbalvin/tensor/internal/core" ) // TestAutocorrelateAR1 pins the ACF of an AR(1) process against its // geometric theoretical decay. func TestAutocorrelateAR1(t *testing.T) { const ( phi = 0.6 n = 20000 ) g := core.NewGenerator(23) x := make([]float64, n) noise := 0.0 for i := range n { noise = phi*noise + g.NormalUnit() x[i] = noise } xArr, err := core.FromFloats(x, n) if err != nil { t.Fatalf("FromFloats: %v", err) } acf, err := Autocorrelate(xArr, 5) if err != nil { t.Fatalf("Autocorrelate: %v", err) } if math.Abs(acf.FloatAt(0)-1) > 1e-12 { t.Fatalf("acf[0] = %.12f, want 1", acf.FloatAt(0)) } for k := 1; k <= 5; k++ { want := math.Pow(phi, float64(k)) if math.Abs(acf.FloatAt(k)-want) > 0.06 { t.Fatalf("acf[%d] = %.4f, want %.4f", k, acf.FloatAt(k), want) } } } // TestAutocorrelateBrute pins the ACF against the direct sum. func TestAutocorrelateBrute(t *testing.T) { x := []float64{3, -1, 4, -1.5, 2, -0.5, 1, 2} n := len(x) xArr, _ := core.FromFloats(x, n) acf, err := Autocorrelate(xArr, n-1) if err != nil { t.Fatalf("Autocorrelate: %v", err) } mean := 0.0 for _, v := range x { mean += v } mean /= float64(n) num := 0.0 for _, v := range x { num += (v - mean) * (v - mean) } for k := range n { s := 0.0 for t := 0; t+k < n; t++ { s += (x[t] - mean) * (x[t+k] - mean) } want := s / num if math.Abs(acf.FloatAt(k)-want) > 1e-12 { t.Fatalf("acf[%d] = %.12f, direct sum %.12f", k, acf.FloatAt(k), want) } } } // TestCrossCorrelateBrute pins the XCF against the direct sum on the // documented lag convention. func TestCrossCorrelateBrute(t *testing.T) { x := []float64{2, -1, 3, 0.5, -2} y := []float64{1, 4, -2, 0.5, 3} n := len(x) xArr, _ := core.FromFloats(x, n) yArr, _ := core.FromFloats(y, n) xcf, err := CrossCorrelate(xArr, yArr) if err != nil { t.Fatalf("CrossCorrelate: %v", err) } if xcf.Len() != 2*n-1 { t.Fatalf("length %d, want %d", xcf.Len(), 2*n-1) } for k := range 2*n - 1 { lag := k - (n - 1) s := 0.0 for t := 0; t+lag < n && t < n; t++ { if t+lag < 0 { continue } s += x[t+lag] * y[t] } if math.Abs(xcf.FloatAt(k)-s) > 1e-12 { t.Fatalf("xcf[lag %d] = %.12f, direct sum %.12f", lag, xcf.FloatAt(k), s) } } } // TestPartialAutocorrelateAR1 pins the PACF signature of an AR(1): // lag 1 estimates phi, the rest are noise around zero. func TestPartialAutocorrelateAR1(t *testing.T) { const ( phi = 0.7 n = 30000 ) g := core.NewGenerator(31) x := make([]float64, n) e := 0.0 for i := range n { e = phi*e + g.NormalUnit() x[i] = e } xArr, _ := core.FromFloats(x, n) pacf, err := PartialAutocorrelate(xArr, 6) if err != nil { t.Fatalf("PartialAutocorrelate: %v", err) } if math.Abs(pacf.FloatAt(0)-phi) > 0.05 { t.Fatalf("pacf[1] = %.4f, want %.2f", pacf.FloatAt(0), phi) } for k := 2; k <= 6; k++ { if math.Abs(pacf.FloatAt(k-1)) > 0.05 { t.Fatalf("pacf[%d] = %.4f, an AR(1) cuts off after lag 1", k, pacf.FloatAt(k-1)) } } } // TestCorrelateErrors pins the input gates. func TestCorrelateErrors(t *testing.T) { x, _ := core.FromFloats([]float64{1, 2, 3}, 3) if _, err := Autocorrelate(x, 3); err == nil { t.Error("maxLag ≥ n accepted") } m, _ := core.FromFloats([]float64{1, 2}, 1, 2) if _, err := Autocorrelate(m, 0); err == nil { t.Error("rank-2 signal accepted") } y, _ := core.FromFloats([]float64{1, 2}, 2) if _, err := CrossCorrelate(x, y); err == nil { t.Error("length mismatch accepted") } if _, err := PartialAutocorrelate(x, 5); err == nil { t.Error("oversized PACF maxLag accepted") } }