120 lines
4.8 KiB
Go
120 lines
4.8 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
|
|
// SPDX-License-Identifier: MIT
|
|||
|
|
|
|||
|
|
package signal
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"strings"
|
|||
|
|
"testing"
|
|||
|
|
|
|||
|
|
"sourcedock.dev/petrbalvin/tensor/internal/core"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// Complex-refusal pins: complex inputs that reached FloatAt's nil-int
|
|||
|
|
// branch, degenerate kernels and empty windows that answered NaN, and
|
|||
|
|
// contract gaps the sibling entry points had already closed.
|
|||
|
|
|
|||
|
|
// TestResampleComplexRefusal: Resample and Decimate widened a complex
|
|||
|
|
// series through widenFloats, which panicked instead of refusing.
|
|||
|
|
func TestResampleComplexRefusal(t *testing.T) {
|
|||
|
|
c := mustComplexes(t, []complex128{1 + 1i, 2, 3 - 1i, 4}, 4)
|
|||
|
|
if _, err := Resample(c, 2, 1, 0); err == nil || !strings.Contains(err.Error(), "complex") {
|
|||
|
|
t.Fatalf("Resample on complex: err = %v", err)
|
|||
|
|
}
|
|||
|
|
if _, err := Decimate(c, 2, 0); err == nil || !strings.Contains(err.Error(), "complex") {
|
|||
|
|
t.Fatalf("Decimate on complex: err = %v", err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestNUFFTComplexCoordinates: complex coordinates reached FloatAt's
|
|||
|
|
// nil-int branch and panicked.
|
|||
|
|
func TestNUFFTComplexCoordinates(t *testing.T) {
|
|||
|
|
x := mustComplexes(t, []complex128{0.1 + 0.2i, -0.2}, 2)
|
|||
|
|
c := mustComplexes(t, []complex128{1, 2i}, 2)
|
|||
|
|
if _, err := NUFFTType1(x, c, 8); err == nil || !strings.Contains(err.Error(), "real") {
|
|||
|
|
t.Fatalf("NUFFTType1 with complex coordinates: err = %v", err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestAnalyticSignalComplexRefusal: the analytic signal is defined for
|
|||
|
|
// a real series; a complex one was silently transformed.
|
|||
|
|
func TestAnalyticSignalComplexRefusal(t *testing.T) {
|
|||
|
|
c := mustComplexes(t, []complex128{1, 2, 3, 4}, 4)
|
|||
|
|
if _, err := AnalyticSignal(c); err == nil || !strings.Contains(err.Error(), "complex") {
|
|||
|
|
t.Fatalf("AnalyticSignal on complex: err = %v", err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestConvComplexBiasRefusal: the convolutions gate the input and the
|
|||
|
|
// kernel on dtype but widened the bias through FloatAt, whose complex
|
|||
|
|
// branch reads a nil payload and panics. A complex bias is refused
|
|||
|
|
// like a complex kernel.
|
|||
|
|
func TestConvComplexBiasRefusal(t *testing.T) {
|
|||
|
|
bias := mustComplexes(t, []complex128{1 + 2i}, 1)
|
|||
|
|
in2 := mustFloats(t, []float64{1, 2, 3, 4, 5, 6, 7, 8}, 1, 1, 2, 4)
|
|||
|
|
ker2 := mustFloats(t, []float64{1, 0, 0, 1}, 1, 1, 2, 2)
|
|||
|
|
in3 := mustFloats(t, []float64{1, 2, 3, 4, 5, 6, 7, 8}, 1, 1, 2, 2, 2)
|
|||
|
|
ker3 := mustFloats(t, []float64{1, 1, 1, 1, 1, 1, 1, 1}, 1, 1, 2, 2, 2)
|
|||
|
|
in1 := mustFloats(t, []float64{1, 2, 3, 4}, 1, 1, 4)
|
|||
|
|
ker1 := mustFloats(t, []float64{1, 1}, 1, 1, 2)
|
|||
|
|
for name, fn := range map[string]func() (*core.Array, error){
|
|||
|
|
"Conv1D": func() (*core.Array, error) { return Conv1D(in1, ker1, bias, 1, 0, 1) },
|
|||
|
|
"Conv2D": func() (*core.Array, error) { return Conv2D(in2, ker2, bias, 1, 0) },
|
|||
|
|
"Conv3D": func() (*core.Array, error) { return Conv3D(in3, ker3, bias, 1, [3]int{0, 0, 0}, [3]int{1, 1, 1}) },
|
|||
|
|
"ConvTranspose2D": func() (*core.Array, error) { return ConvTranspose2D(in2, ker2, bias, 1, 0) },
|
|||
|
|
} {
|
|||
|
|
if _, err := fn(); err == nil || !strings.Contains(err.Error(), "complex bias") {
|
|||
|
|
t.Fatalf("%s with a complex bias: err = %v", name, err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestDecimateOneTap: taps = 1 divided by taps−1 = 0 in the Kaiser
|
|||
|
|
// window and every output sample came back NaN; a one-tap kernel is
|
|||
|
|
// the identity, so decimation keeps every factor-th sample.
|
|||
|
|
func TestDecimateOneTap(t *testing.T) {
|
|||
|
|
x := mustFromFloats(t, []float64{10, 11, 12, 13, 14, 15, 16, 17}, 8)
|
|||
|
|
out, err := Decimate(x, 2, 1)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("Decimate taps=1: %v", err)
|
|||
|
|
}
|
|||
|
|
if out.Len() != 4 {
|
|||
|
|
t.Fatalf("output length %d, want 4", out.Len())
|
|||
|
|
}
|
|||
|
|
for i := range 4 {
|
|||
|
|
if got := out.FloatAt(i); got != float64(10+2*i) {
|
|||
|
|
t.Fatalf("out[%d] = %v, want %d", i, got, 10+2*i)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestAdaptivePoolEmptySpatial: a zero-length spatial dimension left
|
|||
|
|
// every window empty and max answered -Inf while avg divided by zero;
|
|||
|
|
// both are errors now, in every rank.
|
|||
|
|
func TestAdaptivePoolEmptySpatial(t *testing.T) {
|
|||
|
|
empty1D, err := core.Zeros(core.Float, 1, 1, 0)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatal(err)
|
|||
|
|
}
|
|||
|
|
empty2D, err := core.Zeros(core.Float, 1, 1, 0, 4)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatal(err)
|
|||
|
|
}
|
|||
|
|
empty3D, err := core.Zeros(core.Float, 1, 1, 2, 0, 4)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatal(err)
|
|||
|
|
}
|
|||
|
|
for name, fn := range map[string]func() (*core.Array, error){
|
|||
|
|
"max1D": func() (*core.Array, error) { return AdaptiveMaxPool1D(empty1D, 2) },
|
|||
|
|
"max2D": func() (*core.Array, error) { return AdaptiveMaxPool2D(empty2D, 2, 2) },
|
|||
|
|
"max3D": func() (*core.Array, error) { return AdaptiveMaxPool3D(empty3D, 2, 2, 2) },
|
|||
|
|
"avg1D": func() (*core.Array, error) { return AdaptiveAvgPool1D(empty1D, 2) },
|
|||
|
|
"avg2D": func() (*core.Array, error) { return AdaptiveAvgPool2D(empty2D, 2, 2) },
|
|||
|
|
"avg3D": func() (*core.Array, error) { return AdaptiveAvgPool3D(empty3D, 2, 2, 2) },
|
|||
|
|
} {
|
|||
|
|
if _, err := fn(); err == nil || !strings.Contains(err.Error(), "spatial") {
|
|||
|
|
t.Fatalf("%s on an empty spatial dimension: err = %v", name, err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|