feat: initial release
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package signal
|
||||
|
||||
import "sourcedock.dev/petrbalvin/tensor/internal/core"
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// naiveDFT computes the transform quadratically, the reference the fast
|
||||
// paths are verified against.
|
||||
func naiveDFT(vals []complex128, sign float64) []complex128 {
|
||||
n := len(vals)
|
||||
out := make([]complex128, n)
|
||||
for k := range n {
|
||||
var s complex128
|
||||
for j, x := range vals {
|
||||
angle := sign * 2 * math.Pi * float64(j*k%n) / float64(n)
|
||||
s += x * complex(math.Cos(angle), math.Sin(angle))
|
||||
}
|
||||
out[k] = s
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func assertCplxApprox(t *testing.T, name string, got []complex128, want []complex128) {
|
||||
t.Helper()
|
||||
for i := range want {
|
||||
if math.Abs(real(got[i])-real(want[i])) > 1e-9 || math.Abs(imag(got[i])-imag(want[i])) > 1e-9 {
|
||||
t.Fatalf("%s[%d]: got %v, want %v", name, i, got[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fftValues(t *testing.T, a *core.Array) []complex128 {
|
||||
t.Helper()
|
||||
out, err := FFT(a)
|
||||
if err != nil {
|
||||
t.Fatalf("FFT: %v", err)
|
||||
}
|
||||
vals := make([]complex128, out.Len())
|
||||
for i := range vals {
|
||||
vals[i], _ = core.ComplexAt(out, i)
|
||||
}
|
||||
return vals
|
||||
}
|
||||
|
||||
func TestFFTKnownValues(t *testing.T) {
|
||||
// FFT of [1, 2, 3, 4] is the textbook [10, -2+2i, -2, -2-2i].
|
||||
a := mustFromInts(t, []int64{1, 2, 3, 4}, 4)
|
||||
got := fftValues(t, a)
|
||||
assertCplxApprox(t, "FFT", got, []complex128{10, complex(-2, 2), -2, complex(-2, -2)})
|
||||
|
||||
// A constant input concentrates in the DC bin.
|
||||
c := mustFromFloats(t, []float64{2.5, 2.5, 2.5, 2.5}, 4)
|
||||
got = fftValues(t, c)
|
||||
assertCplxApprox(t, "FFT DC", got[:1], []complex128{10})
|
||||
for i := 1; i < 4; i++ {
|
||||
if math.Abs(real(got[i])) > 1e-9 || math.Abs(imag(got[i])) > 1e-9 {
|
||||
t.Fatalf("FFT constant input leaked into bin %d: %v", i, got[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFFTAgainstNaive(t *testing.T) {
|
||||
// Powers of two take the radix-2 path; the odd and mixed lengths take
|
||||
// Bluestein. Both must agree with the quadratic reference.
|
||||
lengths := []int{1, 2, 4, 8, 16, 3, 5, 6, 7, 12, 13, 20}
|
||||
for _, n := range lengths {
|
||||
vals := make([]complex128, n)
|
||||
g := core.NewGenerator(int64(n))
|
||||
for i := range vals {
|
||||
f, _ := core.Floats(g, 2)
|
||||
re, _ := core.FloatAt(f, 0)
|
||||
im, _ := core.FloatAt(f, 1)
|
||||
vals[i] = complex(re*10-5, im*10-5)
|
||||
}
|
||||
src, err := core.FromComplexes(vals, n)
|
||||
if err != nil {
|
||||
t.Fatalf("FromComplexes(%d): %v", n, err)
|
||||
}
|
||||
assertCplxApprox(t, "FFT radix/bluestein", fftValues(t, src), naiveDFT(vals, -1))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIFFTRoundTrip(t *testing.T) {
|
||||
all := []float64{0.5, -1.25, 3, -0.5, 2, 1, -2, 4, 0.25, -3, 1.5, 2.75}
|
||||
for _, n := range []int{8, 5, 12} {
|
||||
src := mustFromFloats(t, all[:n], n)
|
||||
fwd, err := FFT(src)
|
||||
if err != nil {
|
||||
t.Fatalf("FFT(%d): %v", n, err)
|
||||
}
|
||||
back, err := IFFT(fwd)
|
||||
if err != nil {
|
||||
t.Fatalf("IFFT(%d): %v", n, err)
|
||||
}
|
||||
for i := range n {
|
||||
want, _ := core.FloatAt(src, i)
|
||||
got, _ := core.ComplexAt(back, i)
|
||||
if math.Abs(real(got)-want) > 1e-9 || math.Abs(imag(got)) > 1e-9 {
|
||||
t.Fatalf("IFFT(%d)[%d]: got %v, want %v", n, i, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFFTErrors(t *testing.T) {
|
||||
empty := mustFromInts(t, nil, 0)
|
||||
if _, err := FFT(empty); err == nil || !strings.Contains(err.Error(), "empty array") {
|
||||
t.Fatalf("FFT empty: %v", err)
|
||||
}
|
||||
if _, err := IFFT(empty); err == nil || !strings.Contains(err.Error(), "empty array") {
|
||||
t.Fatalf("IFFT empty: %v", err)
|
||||
}
|
||||
m := mustFromInts(t, []int64{1, 2, 3, 4}, 2, 2)
|
||||
if _, err := FFT(m); err == nil || !strings.Contains(err.Error(), "needs a 1-D array") {
|
||||
t.Fatalf("FFT 2-D: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user