Files
tensor/signal/entry_gate_pins_test.go
petrbalvin af4ee19703
Release / gates (push) Successful in 4m38s
Test / test (push) Successful in 5m16s
Release / release (push) Successful in 35s
feat: initial release
Assisted-by: GLM 5.3 Flash
2026-09-03 10:00:00 +02:00

228 lines
7.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package signal
import (
"math"
"testing"
"sourcedock.dev/petrbalvin/tensor/internal/core"
)
// mustF builds a float array or fails the test.
func mustF(t *testing.T, vals []float64, shape ...int) *core.Array {
t.Helper()
a, err := core.FromFloats(vals, shape...)
if err != nil {
t.Fatalf("FromFloats: %v", err)
}
return a
}
// TestFFTRejectsComplexMatrices pins the rank gate on the
// complex path, which used to flatten a matrix silently where the
// real path refused.
func TestFFTRejectsComplexMatrices(t *testing.T) {
c, err := core.FromComplexes([]complex128{1, 2, 3, 4}, 2, 2)
if err != nil {
t.Fatalf("FromComplexes: %v", err)
}
if _, err := FFT(c); err == nil {
t.Fatal("FFT: expected a rank error for a complex matrix")
}
if _, err := IFFT(c); err == nil {
t.Fatal("IFFT: expected a rank error for a complex matrix")
}
}
// TestNUFFTRejectsNaNCoordinate pins the named refusal: NaN
// defeats both range comparisons and used to poison the whole grid.
func TestNUFFTRejectsNaNCoordinate(t *testing.T) {
x := mustF(t, []float64{math.NaN(), 0.1}, 2)
c := mustF(t, []float64{1, 1}, 2)
if _, err := NUFFTType1(x, c, 4); err == nil {
t.Fatal("NUFFTType1: expected an error for a NaN coordinate")
}
}
// TestInfiniteParameterGates pins that a +Inf parameter cannot
// pass a positive-only gate and turn the answer into NaN or silent
// zeros.
func TestInfiniteParameterGates(t *testing.T) {
times := mustF(t, []float64{0.1, 0.4, 0.7, 1.0, 1.3}, 5)
vals := mustF(t, []float64{1, -0.5, 0.8, -0.2, 0.6}, 5)
if _, _, err := LombScargle(times, vals, math.Inf(1), math.Inf(1), 4); err == nil {
t.Fatal("LombScargle: expected an error for an infinite frequency range")
}
x := mustF(t, make([]float64, 16), 16)
for i := range 16 {
x.SetFloatAt(i, math.Sin(float64(i)/3))
}
if _, err := CWT(x, Morlet, []float64{1}, math.Inf(1)); err == nil {
t.Fatal("CWT: expected an error for an infinite sample spacing")
}
if _, err := CWT(x, Morlet, []float64{math.Inf(1)}, 0.1); err == nil {
t.Fatal("CWT: expected an error for an infinite scale")
}
if _, _, err := WelchPSD(x, math.Inf(1), 8, 0, "hann"); err == nil {
t.Fatal("WelchPSD: expected an error for an infinite fs")
}
if _, err := Spectrogram(x, math.Inf(1), STFTOptions{Segment: 8}); err == nil {
t.Fatal("Spectrogram: expected an error for an infinite fs")
}
if _, _, err := ButterworthLowPass(2, math.Inf(1), 100); err == nil {
t.Fatal("ButterworthLowPass: expected an error for an infinite fs")
}
if _, _, err := ChebyshevLowPass(2, math.Inf(1), 100, 1); err == nil {
t.Fatal("ChebyshevLowPass: expected an error for an infinite fs")
}
}
// TestPoissonDirichletMinimumGrid pins the documented 3×3
// minimum, whose length-one interior used to hit the DST-I floor.
func TestPoissonDirichletMinimumGrid(t *testing.T) {
f := mustF(t, []float64{0, 0, 0, 0, 1, 0, 0, 0, 0}, 3, 3)
u, err := SolvePoissonDirichlet(f, 1, 1)
if err != nil {
t.Fatalf("SolvePoissonDirichlet on the 3×3 minimum: %v", err)
}
// The single interior unknown solves −(4u)/h² = 1 at h = 1/2:
// u = -1/16... the sign follows the convention −Δu = f, so the
// interior value is f/16 with the operator's sign.
if v := u.FloatAt(4); math.IsNaN(v) || math.IsInf(v, 0) {
t.Fatalf("interior value = %v", v)
}
// (3, k) and (k, 3) grids take the one-point DST on one axis only.
f2 := mustF(t, []float64{0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0}, 3, 4)
if _, err := SolvePoissonDirichlet(f2, 1, 1); err != nil {
t.Fatalf("SolvePoissonDirichlet on 3×4: %v", err)
}
}
// TestMaxPoolNaNEveryRank pins the NaN-propagation promise on
// the four max-pool entry points the earlier pin left uncovered.
func TestMaxPoolNaNEveryRank(t *testing.T) {
x1 := mustF(t, []float64{1, math.NaN(), 3, 4, 5, 6}, 1, 1, 6)
got1, err := MaxPool1D(x1, 2, 1, 0)
if err != nil {
t.Fatalf("MaxPool1D: %v", err)
}
if !math.IsNaN(got1.FloatAt(0)) {
t.Fatalf("MaxPool1D[0] = %v, want NaN", got1.FloatAt(0))
}
a1, err := AdaptiveMaxPool1D(x1, 2)
if err != nil {
t.Fatalf("AdaptiveMaxPool1D: %v", err)
}
if !math.IsNaN(a1.FloatAt(0)) {
t.Fatalf("AdaptiveMaxPool1D[0] = %v, want NaN", a1.FloatAt(0))
}
g1, err := GlobalMaxPool1D(x1)
if err != nil {
t.Fatalf("GlobalMaxPool1D: %v", err)
}
if !math.IsNaN(g1.FloatAt(0)) {
t.Fatalf("GlobalMaxPool1D[0] = %v, want NaN", g1.FloatAt(0))
}
x3 := make([]float64, 2*3*4*4*4)
for i := range x3 {
x3[i] = float64(i % 7)
}
x3[40] = math.NaN()
c3, err := core.FromFloats(x3, 2, 3, 4, 4, 4)
if err != nil {
t.Fatalf("FromFloats: %v", err)
}
g3, err := GlobalMaxPool3D(c3)
if err != nil {
t.Fatalf("GlobalMaxPool3D: %v", err)
}
if !math.IsNaN(g3.FloatAt(0)) {
t.Fatalf("GlobalMaxPool3D[0] = %v, want NaN", g3.FloatAt(0))
}
m3, err := MaxPool3D(c3, [3]int{2, 2, 2}, [3]int{1, 1, 1}, [3]int{0, 0, 0})
if err != nil {
t.Fatalf("MaxPool3D: %v", err)
}
// Flat 40 is (d, h, w) = (2, 2, 0); the window that covers it
// starts at (1, 1, 0), output index 12 of the 3×3×3 grid.
if !math.IsNaN(m3.FloatAt(12)) {
t.Fatalf("MaxPool3D[12] = %v, want NaN", m3.FloatAt(12))
}
a3, err := AdaptiveMaxPool3D(c3, 1, 1, 1)
if err != nil {
t.Fatalf("AdaptiveMaxPool3D: %v", err)
}
if !math.IsNaN(a3.FloatAt(0)) {
t.Fatalf("AdaptiveMaxPool3D[0] = %v, want NaN", a3.FloatAt(0))
}
}
// TestGlobalPoolsRejectEmptySpatial pins the named refusal the
// adaptive ranks already make.
func TestGlobalPoolsRejectEmptySpatial(t *testing.T) {
empty1, err := core.Zeros(core.Float, 2, 3, 0)
if err != nil {
t.Fatalf("Zeros: %v", err)
}
if _, err := GlobalMaxPool1D(empty1); err == nil {
t.Fatal("GlobalMaxPool1D: expected an error for an empty spatial dimension")
}
if _, err := GlobalAvgPool1D(empty1); err == nil {
t.Fatal("GlobalAvgPool1D: expected an error for an empty spatial dimension")
}
empty3, err := core.Zeros(core.Float, 2, 3, 0, 4, 4)
if err != nil {
t.Fatalf("Zeros: %v", err)
}
if _, err := GlobalMaxPool3D(empty3); err == nil {
t.Fatal("GlobalMaxPool3D: expected an error for an empty spatial dimension")
}
if _, err := GlobalAvgPool3D(empty3); err == nil {
t.Fatal("GlobalAvgPool3D: expected an error for an empty spatial dimension")
}
}
// TestResampleMatchesDirectSum pins the polyphase window
// restriction: the bounded loop must produce bit-identical output to
// the full scan it replaced.
func TestResampleMatchesDirectSum(t *testing.T) {
n := 61
src := make([]float64, n)
for i := range n {
src[i] = math.Sin(float64(i)*0.37) + 0.2*math.Cos(float64(i)*1.7)
}
x := mustF(t, src, n)
for _, c := range []struct{ up, down int }{{2, 3}, {3, 2}, {1, 2}, {2, 1}, {5, 7}} {
got, err := Resample(x, c.up, c.down, 13)
if err != nil {
t.Fatalf("Resample(%d/%d): %v", c.up, c.down, err)
}
// The reference: the original full scan over every input
// index, computing the same taps by hand.
taps := 13
fc := 0.5 * math.Min(1/float64(c.up), 1/float64(c.down))
h := kaiserSinc(taps, fc)
delay := (taps - 1) / 2
outLen := (n*c.up + c.down - 1) / c.down
if got.Len() != outLen {
t.Fatalf("Resample(%d/%d): length %d, want %d", c.up, c.down, got.Len(), outLen)
}
for m := range outLen {
centre := delay + m*c.down
total := 0.0
for k := range n {
j := centre - k*c.up
if j < 0 || j >= taps {
continue
}
total += h[j] * src[k]
}
if want := float64(c.up) * total; got.FloatAt(m) != want {
t.Fatalf("Resample(%d/%d)[%d] = %g, want %g", c.up, c.down, m, got.FloatAt(m), want)
}
}
}
}