feat: initial release
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
// hilbertTone builds an integer number of periods of a cosine, where
|
||||
// the periodic Fourier definition of the analytic signal is exact.
|
||||
func hilbertTone(t *testing.T, cycles, n int) *core.Array {
|
||||
t.Helper()
|
||||
vals := make([]float64, n)
|
||||
for i := range n {
|
||||
vals[i] = math.Cos(2 * math.Pi * float64(cycles) * float64(i) / float64(n))
|
||||
}
|
||||
a, err := core.FromFloats(vals, n)
|
||||
if err != nil {
|
||||
t.Fatalf("FromFloats: %v", err)
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// TestAnalyticSignalTone checks the defining property on a pure tone:
|
||||
// the analytic signal of cos is exp(iωt), so the real part is the
|
||||
// input, the imaginary part the quadrature sine and the modulus one.
|
||||
func TestAnalyticSignalTone(t *testing.T) {
|
||||
const cycles, n = 13, 256
|
||||
z, err := AnalyticSignal(hilbertTone(t, cycles, n))
|
||||
if err != nil {
|
||||
t.Fatalf("AnalyticSignal: %v", err)
|
||||
}
|
||||
bins := z.RawComplexes()[:z.Len()]
|
||||
for i, c := range bins {
|
||||
want := 2 * math.Pi * float64(cycles) * float64(i) / float64(n)
|
||||
if math.Abs(real(c)-math.Cos(want)) > 1e-9 {
|
||||
t.Fatalf("bin %d real part %.12g, want the input cosine", i, real(c))
|
||||
}
|
||||
if math.Abs(imag(c)-math.Sin(want)) > 1e-9 {
|
||||
t.Fatalf("bin %d imaginary part %.12g, want the quadrature sine", i, imag(c))
|
||||
}
|
||||
if e := math.Hypot(real(c), imag(c)); math.Abs(e-1) > 1e-9 {
|
||||
t.Fatalf("bin %d modulus %.12g, want 1", i, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestEnvelopeAmplitudeModulation checks the envelope on an AM
|
||||
// carrier: the modulus of the analytic signal must recover the
|
||||
// modulating wave, the quantity an AM receiver is after.
|
||||
func TestEnvelopeAmplitudeModulation(t *testing.T) {
|
||||
const n = 512
|
||||
vals := make([]float64, n)
|
||||
for i := range n {
|
||||
mod := 1 + 0.5*math.Cos(2*math.Pi*4*float64(i)/float64(n))
|
||||
carrier := math.Cos(2 * math.Pi * 40 * float64(i) / float64(n))
|
||||
vals[i] = mod * carrier
|
||||
}
|
||||
a, err := core.FromFloats(vals, n)
|
||||
if err != nil {
|
||||
t.Fatalf("FromFloats: %v", err)
|
||||
}
|
||||
env, err := Envelope(a)
|
||||
if err != nil {
|
||||
t.Fatalf("Envelope: %v", err)
|
||||
}
|
||||
vals = env.RawFloats()[:env.Len()]
|
||||
for i, got := range vals {
|
||||
want := 1 + 0.5*math.Cos(2*math.Pi*4*float64(i)/float64(n))
|
||||
if math.Abs(got-want) > 1e-9 {
|
||||
t.Fatalf("sample %d envelope %.12g, want %.12g", i, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestAnalyticSignalRefusals checks the shape and emptiness guards.
|
||||
func TestAnalyticSignalRefusals(t *testing.T) {
|
||||
if _, err := AnalyticSignal(core.New(core.Float, 3, 2)); err == nil {
|
||||
t.Fatal("matrix accepted")
|
||||
}
|
||||
if _, err := AnalyticSignal(core.New(core.Float, 0)); err == nil {
|
||||
t.Fatal("zero-length series accepted")
|
||||
}
|
||||
if _, err := Envelope(core.New(core.Float, 3, 3, 3)); err == nil {
|
||||
t.Fatal("3-D array accepted")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user