92 lines
2.8 KiB
Go
92 lines
2.8 KiB
Go
// 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")
|
||
|
|
}
|
||
|
|
}
|