// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: MIT // Pins for the convolution fast-path window arithmetic, the Decimate // output-length rule, the float32 and int dtype paths of // Resample/Decimate/IDWT and the SolvePoissonNeumann solve. package signal import ( "fmt" "math" "strings" "testing" "sourcedock.dev/petrbalvin/tensor/internal/core" ) // TestConv1DStride1TapWindowPastBlock drives a kernel tap whose whole // window lies outside an output block at unit stride: the stride == 1 // fast path must skip the tap instead of slicing a range whose high // bound is under its low one (the finding's "slice bounds out of // range" panic). The result is pinned against the direct convolution // definition, so the skipped taps are also checked to contribute // nothing. func TestConv1DStride1TapWindowPastBlock(t *testing.T) { const ( n = 202 factor = 3 padding = 200 ) // 202 samples, kernel [1, 2, 3], padding 200: lOut = 600 spans two // 512-wide blocks, and block 1 sits past the last tap's reach. vals := make([]float64, n) for i := range vals { vals[i] = float64(i%7) - 3 } x := mustFloats(t, vals, 1, 1, n) k := mustFloats(t, []float64{1, 2, 3}, 1, 1, factor) out, err := Conv1D(x, k, nil, 1, padding, 1) if err != nil { t.Fatalf("Conv1D: %v", err) } if got := out.Shape(); got[0] != 1 || got[1] != 1 || got[2] != 600 { t.Fatalf("Conv1D: shape %v, want [1 1 600]", got) } for ol := range 600 { want := 0.0 for kl := range factor { idx := ol + kl - padding if idx < 0 || idx >= n { continue } want += vals[idx] * float64(kl+1) } if got := out.FloatAt(ol); got != want { t.Fatalf("Conv1D: out[%d] = %g, want %g (the direct definition)", ol, got, want) } } } // TestConv2DStride1PaddingOnlyTapWindow drives a 2-D kernel tap whose // window lies entirely in the padding: the stride == 1 fast path must // skip it rather than slice rank 1 into the tap row. func TestConv2DStride1PaddingOnlyTapWindow(t *testing.T) { x := mustFloats(t, []float64{1}, 1, 1, 1, 1) k := mustFloats(t, []float64{1, 2, 3, 4, 5}, 1, 1, 1, 5) out, err := Conv2D(x, k, nil, 1, 2) if err != nil { t.Fatalf("Conv2D: %v", err) } if got := out.Shape(); got[0] != 1 || got[1] != 1 || got[2] != 5 || got[3] != 1 { t.Fatalf("Conv2D: shape %v, want [1 1 5 1]", got) } // Only the kernel's centre tap overlaps the single input sample; // the taps at kw 0, 1, 3 and 4 reach no output at all. for oh := range 5 { want := 0.0 if oh == 2 { want = 3 // x[0] * k[2] } if got := out.FloatAt(oh); got != want { t.Fatalf("Conv2D: out[0,0,%d,0] = %g, want %g", oh, got, want) } } } // TestConv3DStride1PaddingOnlyTapWindow drives the 3-D twin of the // same padding-only tap window. func TestConv3DStride1PaddingOnlyTapWindow(t *testing.T) { x := mustFloats(t, []float64{1}, 1, 1, 1, 1, 1) k := mustFloats(t, []float64{1, 2, 3, 4, 5}, 1, 1, 1, 1, 5) out, err := Conv3D(x, k, nil, 1, [3]int{0, 0, 2}, [3]int{1, 1, 1}) if err != nil { t.Fatalf("Conv3D: %v", err) } if got := out.Shape(); got[0] != 1 || got[1] != 1 || got[2] != 1 || got[3] != 1 || got[4] != 1 { t.Fatalf("Conv3D: shape %v, want [1 1 1 1 1]", got) } if got := out.FloatAt(0); got != 3 { t.Fatalf("Conv3D: out[0] = %g, want 3 (x[0] * k[2])", got) } } // TestDecimateOutLenOutOfRange calls Decimate with a custom tap count // whose filter delay plus first kept index runs to or past the end of // the filtered signal: the truncated division used to round a negative // numerator toward zero, producing one output sample read out of // range. func TestDecimateOutLenOutOfRange(t *testing.T) { cases := []struct { name string n, factor, taps int }{ {"ReportedSmallFactor", 26, 8, 25}, {"FactorLargerThanN", 10, 100, 3}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { vals := make([]float64, tc.n) for i := range vals { vals[i] = math.Sin(0.3 * float64(i)) } x := mustFloats(t, vals, tc.n) out, err := Decimate(x, tc.factor, tc.taps) if err != nil { t.Fatalf("Decimate(n=%d, factor=%d, taps=%d): %v", tc.n, tc.factor, tc.taps, err) } want := decimateOutLen(tc.n, tc.factor, tc.taps) if out.Len() != want { t.Fatalf("Decimate(n=%d, factor=%d, taps=%d) = %d samples, want %d", tc.n, tc.factor, tc.taps, out.Len(), want) } t.Logf("Decimate(n=%d, factor=%d, taps=%d) -> %d samples", tc.n, tc.factor, tc.taps, out.Len()) }) } } // TestResampleDecimateFloat32Input feeds float32 and int series into // Resample and Decimate: both read the payload without a dtype guard, // so a non-float input panicked on the nil float64 payload instead of // widening through FloatAt like DWT and the rest of the package. The // results are pinned to the float64 run on the same values. func TestResampleDecimateFloat32Input(t *testing.T) { const n = 256 f64 := make([]float64, n) f32 := make([]float32, n) for i := range n { f32[i] = float32(math.Cos(2 * math.Pi * 3 * float64(i) / float64(n))) // The float64 twin holds the widened float32 values, so the // two runs see bit-identical inputs. f64[i] = float64(f32[i]) } a32 := mustFloat32s(t, f32, n) a64 := mustFloats(t, f64, n) t.Run("ResampleFloat32", func(t *testing.T) { got, err := Resample(a32, 3, 1, 0) if err != nil { t.Fatalf("Resample(float32): %v", err) } want, err := Resample(a64, 3, 1, 0) if err != nil { t.Fatalf("Resample(float64): %v", err) } if got.Len() != want.Len() { t.Fatalf("Resample(float32) = %d samples, want %d", got.Len(), want.Len()) } for i := range want.Len() { // FloatAt widens exactly, so both runs must agree bit // for bit on the same input values. if g, w := got.FloatAt(i), want.FloatAt(i); g != w { t.Fatalf("Resample sample %d = %g, want %g", i, g, w) } } }) t.Run("DecimateFloat32", func(t *testing.T) { got, err := Decimate(a32, 2, 0) if err != nil { t.Fatalf("Decimate(float32): %v", err) } want, err := Decimate(a64, 2, 0) if err != nil { t.Fatalf("Decimate(float64): %v", err) } if got.Len() != want.Len() { t.Fatalf("Decimate(float32) = %d samples, want %d", got.Len(), want.Len()) } // FilterApply keeps the float32 dtype, so its output samples // are the float64 ones rounded to float32 on store; the // decimation picks the same ones. The comparison is exact. for i := range want.Len() { w := float64(float32(want.FloatAt(i))) if g := got.FloatAt(i); g != w { t.Fatalf("Decimate sample %d = %g, want %g", i, g, w) } } }) t.Run("Int", func(t *testing.T) { ivals := make([]int64, n) fvals := make([]float64, n) for i := range n { ivals[i] = int64(i%7) - 3 fvals[i] = float64(ivals[i]) } ai, err := core.FromInts(ivals, n) if err != nil { t.Fatalf("FromInts: %v", err) } af := mustFloats(t, fvals, n) gi, err := Resample(ai, 2, 1, 0) if err != nil { t.Fatalf("Resample(int): %v", err) } gf, err := Resample(af, 2, 1, 0) if err != nil { t.Fatalf("Resample(float64): %v", err) } for i := range gf.Len() { if g, w := gi.FloatAt(i), gf.FloatAt(i); g != w { t.Fatalf("Resample int sample %d = %g, want %g", i, g, w) } } di, err := Decimate(ai, 4, 0) if err != nil { t.Fatalf("Decimate(int): %v", err) } df, err := Decimate(af, 4, 0) if err != nil { t.Fatalf("Decimate(float64): %v", err) } for i := range df.Len() { if g, w := di.FloatAt(i), df.FloatAt(i); g != w { t.Fatalf("Decimate int sample %d = %g, want %g", i, g, w) } } }) } // TestIDWTFloat32Coefficients inverts a float32 coefficient array: // DWT accepts float32 through FloatAt, and IDWT reading the nil // float64 payload instead returned all zeros with no error. The // float32 and float64 runs must agree sample for sample. func TestIDWTFloat32Coefficients(t *testing.T) { // A genuine packed coefficient array, so the assertion covers a // realistic layout: the DWT of a ramp at one level. ramp := make([]float64, 8) for i := range ramp { ramp[i] = float64(i + 1) } coef, err := DWT(mustFloats(t, ramp, 8), 1) if err != nil { t.Fatalf("DWT: %v", err) } f32 := make([]float32, 8) f64 := make([]float64, 8) for i := range 8 { v := coef.FloatAt(i) f32[i] = float32(v) f64[i] = float64(f32[i]) } got, err := IDWT(mustFloat32s(t, f32, 8), 1) if err != nil { t.Fatalf("IDWT(float32): %v", err) } want, err := IDWT(mustFloats(t, f64, 8), 1) if err != nil { t.Fatalf("IDWT(float64): %v", err) } nonzero := 0 for i := range 8 { g, w := got.FloatAt(i), want.FloatAt(i) if g != w { t.Fatalf("IDWT sample %d = %g, want %g", i, g, w) } if g != 0 { nonzero++ } } if nonzero == 0 { t.Fatal("IDWT returned all zeros for a float32 coefficient array") } } // mustFloat32s builds a float32 array, failing the test on a bad shape. func mustFloat32s(t *testing.T, vals []float32, shape ...int) *core.Array { t.Helper() a, err := core.FromFloat32s(vals, shape...) if err != nil { t.Fatalf("FromFloat32s(%v, %v): %v", vals, shape, err) } return a } // decimateOutLen mirrors the documented output rule for the tap count // the caller passes: the filter delay plus the first kept sample must // leave whole factors behind it. It is what the regression test // compares the library against, written from the rule rather than from // the implementation. func decimateOutLen(n, factor, taps int) int { if taps%2 == 0 { taps++ } if taps >= n { return 0 } delay := (taps - 1) / 2 // A kept sample needs m + delay <= n - 1 for some multiple m of // factor with m >= delay, i.e. floor((n-1-delay)/factor) - skip + 1 // samples exist below the end of the signal. skip := (delay + factor - 1) / factor lastMultiple := (n - 1 - delay) / factor if lastMultiple < skip { return 0 } return lastMultiple - skip + 1 } // assertFinite fails the test on the first non-finite sample: a // maximum-error loop cannot see a NaN, because every comparison // against one is false, which is how the shipped manufactured test // passed on an all-NaN solve. Every solve assertion below runs after // this check. func assertFinite(t *testing.T, what string, vals []float64) { t.Helper() for i, v := range vals { if math.IsNaN(v) || math.IsInf(v, 0) { t.Fatalf("%s: sample %d is %g, the result must be finite", what, i, v) } } } // stencilTap is one entry of a mirrored-stencil operator row: the // column index and the coefficient in units of 1/h². type stencilTap struct { j int v float64 } // mirroredStencilRows returns the taps of the ghost-mirror operator's // row i of an N-point axis: [2,-2] on the boundary, where the mirrored // neighbour outside the interval stands in for zero slope, and // [-1,2,-1] inside. No code in the library shares these taps; they are // the definition the regression tests hold the solve against. func mirroredStencilRows(n, i int) []stencilTap { switch { case i == 0: return []stencilTap{{0, 2}, {1, -2}} case i == n-1: return []stencilTap{{n - 2, -2}, {n - 1, 2}} default: return []stencilTap{{i - 1, -1}, {i, 2}, {i + 1, -1}} } } // mirroredStencilApply applies the 2-D mirrored operator to a // row-major grid. func mirroredStencilApply(u []float64, rows, cols int, hx, hy float64) []float64 { out := make([]float64, rows*cols) for r := range rows { for c := range cols { s := 0.0 for _, e := range mirroredStencilRows(cols, c) { s += e.v * u[r*cols+e.j] / (hx * hx) } for _, e := range mirroredStencilRows(rows, r) { s += e.v * u[e.j*cols+c] / (hy * hy) } out[r*cols+c] = s } } return out } // mirroredStencilMatrix builds the explicit operator matrix of the // (rows·cols)-point grid: column j is the operator applied to a unit // input at j, so row i holds the coefficients of equation i. func mirroredStencilMatrix(rows, cols int, hx, hy float64) [][]float64 { n := rows * cols m := make([][]float64, n) for i := range n { m[i] = make([]float64, n) } for j := range n { u := make([]float64, n) u[j] = 1 col := mirroredStencilApply(u, rows, cols, hx, hy) for i := range n { m[i][j] = col[i] } } return m } // neumannTrapz returns the trapezoidal-weighted sum of a row-major // grid and the measure's total weight: the boundary rows and columns // count half. That weight vector is the mirrored operator's left null // vector, the measure its compatibility condition is stated in. func neumannTrapz(u []float64, rows, cols int) (sum, weight float64) { for r := range rows { wr := 1.0 if r == 0 || r == rows-1 { wr = 0.5 } for c := range cols { wc := 1.0 if c == 0 || c == cols-1 { wc = 0.5 } sum += wr * wc * u[r*cols+c] weight += wr * wc } } return sum, weight } // neumannTrapzSum returns the trapezoidal-weighted sum of a row-major // grid, the gate's measure. func neumannTrapzSum(u []float64, rows, cols int) float64 { sum, _ := neumannTrapz(u, rows, cols) return sum } // neumannTrapzMean returns the trapezoidal-weighted mean, the sum over // its total weight. func neumannTrapzMean(u []float64, rows, cols int) float64 { sum, weight := neumannTrapz(u, rows, cols) return sum / weight } // neumannDenseSolve solves the singular mirrored system M u = f by // replacing the last equation with the zero-trapezoidal-mean // constraint, which makes it nonsingular (the trapezoidal weight is // the left null vector, so it cannot lie in the row space), then // eliminating with partial pivoting. Test scaffolding: it panics on a // singular matrix rather than returning a wrong answer. func neumannDenseSolve(m [][]float64, f []float64, rows, cols int) []float64 { n := len(f) a := make([][]float64, n) for i := range n { a[i] = append(append([]float64(nil), m[i]...), f[i]) } for j := range n { r, c := j/cols, j%cols wr, wc := 1.0, 1.0 if r == 0 || r == rows-1 { wr = 0.5 } if c == 0 || c == cols-1 { wc = 0.5 } a[n-1][j] = wr * wc } a[n-1][n] = 0 for col := range n { piv := col for r := col + 1; r < n; r++ { if math.Abs(a[r][col]) > math.Abs(a[piv][col]) { piv = r } } a[col], a[piv] = a[piv], a[col] p := a[col][col] if p == 0 { panic("neumannDenseSolve: singular matrix") } for r := col + 1; r < n; r++ { fac := a[r][col] / p for c := col; c <= n; c++ { a[r][c] -= fac * a[col][c] } } } u := make([]float64, n) for i := n - 1; i >= 0; i-- { s := a[i][n] for j := i + 1; j < n; j++ { s -= a[i][j] * u[j] } u[i] = s / a[i][i] } return u } // TestSolvePoissonNeumannMirroredStencil holds the solve against the // explicit ghost-mirror matrix: the operator is built entry by entry // for the small grids, inverted through a dense elimination with the // zero-trapezoidal-mean constraint, and the library's solution must // agree with it, satisfy M u = f and carry no trapezoidal mean. func TestSolvePoissonNeumannMirroredStencil(t *testing.T) { for _, g := range []struct { rows, cols int lx, ly float64 }{ {3, 3, 1, 1}, {4, 4, 1, 1}, {5, 3, 1.7, 0.9}, {9, 9, 1, 1}, } { hx, hy := g.lx/float64(g.cols-1), g.ly/float64(g.rows-1) // A compatible source: a cosine mode minus its trapezoidal // mean, so the constant mode of the transform vanishes. f := make([]float64, g.rows*g.cols) for r := range g.rows { for c := range g.cols { f[r*g.cols+c] = math.Cos(2*math.Pi*float64(c)/float64(g.cols-1)) * math.Cos(3*math.Pi*float64(r)/float64(g.rows-1)) } } mu := neumannTrapzMean(f, g.rows, g.cols) for i := range f { f[i] -= mu } want := neumannDenseSolve(mirroredStencilMatrix(g.rows, g.cols, hx, hy), f, g.rows, g.cols) got, err := SolvePoissonNeumann(mustFloats(t, f, g.rows, g.cols), g.lx, g.ly) if err != nil { t.Fatalf("%dx%d: SolvePoissonNeumann: %v", g.rows, g.cols, err) } vals := make([]float64, got.Len()) for i := range vals { vals[i] = got.FloatAt(i) } assertFinite(t, fmt.Sprintf("%dx%d solve", g.rows, g.cols), vals) worst := 0.0 for i := range f { worst = max(worst, math.Abs(vals[i]-want[i])) } // The dense solve carries its own round-off; the agreement is // at the 1e-16 level on these grids, so the bound below keeps // orders of margin and still pins an operator error. if worst > 1e-12 { t.Fatalf("%dx%d: solve differs from the dense inverse by %.3g", g.rows, g.cols, worst) } resid := mirroredStencilApply(vals, g.rows, g.cols, hx, hy) rus := 0.0 for i := range f { rus = max(rus, math.Abs(resid[i]-f[i])) } if rus > 1e-12 { t.Fatalf("%dx%d: residual |M u - f| = %.3g", g.rows, g.cols, rus) } if mean := neumannTrapzMean(vals, g.rows, g.cols); math.Abs(mean) > 1e-14 { t.Fatalf("%dx%d: trapezoidal mean of the solution is %.3g, want zero", g.rows, g.cols, mean) } t.Logf("%dx%d lx=%g ly=%g: max |solve - dense inverse| = %.3g", g.rows, g.cols, g.lx, g.ly, worst) } } // TestSolvePoissonNeumannDiscreteEigenfunction pins the two halves of // the construction: the mirrored stencil really has the cosine // eigenfunctions with the eigenvalues 2/h²(1-cos(πk/(N-1))) per axis, // and the solve inverts the stencil on an eigenfunction to transform // round-off, which is the exactness the spectral solve promises. func TestSolvePoissonNeumannDiscreteEigenfunction(t *testing.T) { for _, g := range []struct { rows, cols, kx, ky int }{ {9, 9, 2, 3}, {17, 17, 4, 5}, {12, 8, 3, 5}, } { hx, hy := 1/float64(g.cols-1), 1/float64(g.rows-1) mode := make([]float64, g.rows*g.cols) for r := range g.rows { for c := range g.cols { mode[r*g.cols+c] = math.Cos(math.Pi*float64(g.kx)*float64(c)/float64(g.cols-1)) * math.Cos(math.Pi*float64(g.ky)*float64(r)/float64(g.rows-1)) } } lambda := 2/(hx*hx)*(1-math.Cos(math.Pi*float64(g.kx)/float64(g.cols-1))) + 2/(hy*hy)*(1-math.Cos(math.Pi*float64(g.ky)/float64(g.rows-1))) applied := mirroredStencilApply(mode, g.rows, g.cols, hx, hy) for i := range mode { if e := math.Abs(applied[i] - lambda*mode[i]); e > 1e-12*lambda { t.Fatalf("%dx%d k=(%d,%d): M v - lambda v = %.3g at %d, the cosine mode is not an eigenfunction", g.rows, g.cols, g.kx, g.ky, e, i) } } got, err := SolvePoissonNeumann(mustFloats(t, applied, g.rows, g.cols), 1, 1) if err != nil { t.Fatalf("%dx%d: SolvePoissonNeumann: %v", g.rows, g.cols, err) } mu := neumannTrapzMean(mode, g.rows, g.cols) gotVals := make([]float64, got.Len()) for i := range gotVals { gotVals[i] = got.FloatAt(i) } assertFinite(t, fmt.Sprintf("%dx%d solve", g.rows, g.cols), gotVals) worst := 0.0 for i := range mode { worst = max(worst, math.Abs(gotVals[i]-(mode[i]-mu))) } // The acceptance level is the transform's round-off, about // 1e-14; the measured worst across these grids is 6.1e-15. if worst > 1e-13 { t.Fatalf("%dx%d k=(%d,%d): eigenfunction recovered to %.3g only", g.rows, g.cols, g.kx, g.ky, worst) } t.Logf("%dx%d k=(%d,%d): eigenfunction error %.3g", g.rows, g.cols, g.kx, g.ky, worst) } } // TestSolvePoissonNeumannSecondOrder runs the manufactured solution // u = cos(πx)cos(πy), f = 2π²u (which the trapezoidal measure already // sees as zero-mean) on three grids: every sample must be finite, the // recentred error must sit inside the stencil's O(h²) budget, the // error must fall by about four per grid doubling, and the result must // carry no trapezoidal mean. The shipped test of the same construction // compares NaN samples as "not larger", so this one asserts finiteness // first. func TestSolvePoissonNeumannSecondOrder(t *testing.T) { worstOf := map[int]float64{} for _, n := range []int{9, 17, 33} { h := 1 / float64(n-1) flatF := make([]float64, n*n) exact := make([]float64, n*n) for r := range n { for c := range n { x := float64(c) * h y := float64(r) * h exact[r*n+c] = math.Cos(math.Pi*x) * math.Cos(math.Pi*y) flatF[r*n+c] = 2 * math.Pi * math.Pi * exact[r*n+c] } } u, err := SolvePoissonNeumann(mustFloats(t, flatF, n, n), 1, 1) if err != nil { t.Fatalf("grid %d: SolvePoissonNeumann: %v", n, err) } vals := make([]float64, u.Len()) for i := range vals { vals[i] = u.FloatAt(i) } assertFinite(t, fmt.Sprintf("grid %d", n), vals) if mean := neumannTrapzMean(vals, n, n); math.Abs(mean) > 1e-14 { t.Fatalf("grid %d: trapezoidal mean of the solution is %.3g, want zero", n, mean) } // Recentring: the solve fixes the constant mode, the exact // solution's own trapezoidal mean is zero, so this is the // contract's comparison and not a fudge. got := neumannTrapzMean(vals, n, n) worst := 0.0 for i := range exact { worst = max(worst, math.Abs(vals[i]-got-exact[i])) } if worst > 3*h*h { t.Fatalf("grid %d: worst error %.3g above the O(h²) budget %.3g", n, worst, 3*h*h) } worstOf[n] = worst } // O(h²) means the error falls by about four per doubling; the // measured ratios sit near 4, so anything under 3 fails the claim. if r := worstOf[9] / worstOf[17]; r < 3 { t.Fatalf("error fell by only %.3f from 9 to 17 points, not the O(h²) factor", r) } if r := worstOf[17] / worstOf[33]; r < 3 { t.Fatalf("error fell by only %.3f from 17 to 33 points, not the O(h²) factor", r) } t.Logf("worst errors (h² = %.3g, %.3g, %.3g): %.3g, %.3g, %.3g", 1/64.0/64.0, 1/16.0/16.0, 1/32.0/32.0, worstOf[9], worstOf[17], worstOf[33]) } // TestSolvePoissonNeumannTrapzRefusal checks the compatibility gate on // its own measure: a source whose plain mean is zero to rounding but // whose trapezoidal-weighted sum is not has no mirrored-stencil // solution, and the solve must refuse it by name rather than divide // the constant mode by its zero eigenvalue. The corners-only source is // the smallest such case: the boundary samples carry half the // trapezoidal weight, so removing the plain mean leaves a large // trapezoidal component. func TestSolvePoissonNeumannTrapzRefusal(t *testing.T) { const n = 9 flat := make([]float64, n*n) for _, i := range []int{0, n - 1, (n - 1) * n, n*n - 1} { flat[i] = 1 } plain := 0.0 for _, v := range flat { plain += v } plain /= float64(n * n) for i := range flat { flat[i] -= plain } // The construction is only meaningful if the plain mean really // vanished: otherwise the old gate would have caught it. residual := 0.0 for _, v := range flat { residual += v } if math.Abs(residual) > 1e-14 { t.Fatalf("test construction: residual plain sum %g", residual) } if sum := neumannTrapzSum(flat, n, n); math.Abs(sum) < 1 { t.Fatalf("test construction: trapezoidal sum %.3g is too small to exercise the gate", sum) } f := mustFloats(t, flat, n, n) out, err := SolvePoissonNeumann(f, 1, 1) if err == nil { t.Fatalf("zero-plain-mean but trapezoidally incompatible source accepted, solution = %v of %d samples", out.FloatAt(0), out.Len()) } if !strings.Contains(err.Error(), "SolvePoissonNeumann") || !strings.Contains(err.Error(), "trapezoidal") { t.Fatalf("refusal %q does not name the function and the measure", err) } // The all-ones source has no solution on either measure, and the // shipped refusal test's shape guards stay intact. if _, err := SolvePoissonNeumann(mustFloats(t, []float64{1, 1, 1, 1, 1, 1, 1, 1, 1}, 3, 3), 1, 1); err == nil { t.Fatal("nonzero-mean source accepted") } } // The view contract of the new entry points: a rebased view's payload // is longer than its element count and starts past offset zero, so a // kernel that reaches for the raw payload instead of the elements // reads the wrong window of the backing array. Each test compares the // view answer against the same values in a fresh array. func TestEntryPointsOnView(t *testing.T) { ramp := make([]float64, 20) for i := range ramp { ramp[i] = float64(i) } back, err := core.FromFloats(ramp, 20) if err != nil { t.Fatal(err) } view, err := core.Slice(back, 0, 3, 19) if err != nil { t.Fatal(err) } fresh, err := core.FromFloats(append([]float64{}, ramp[3:19]...), 16) if err != nil { t.Fatal(err) } median, err := MedianFilter(view, 3) if err != nil { t.Fatalf("MedianFilter: %v", err) } wantMedian, err := MedianFilter(fresh, 3) if err != nil { t.Fatalf("MedianFilter: %v", err) } for i := range 16 { if median.FloatAt(i) != wantMedian.FloatAt(i) { t.Fatalf("MedianFilter(view)[%d] = %v, want %v", i, median.FloatAt(i), wantMedian.FloatAt(i)) } } rank, err := RankFilter(view, 5, 0) if err != nil { t.Fatalf("RankFilter: %v", err) } wantRank, err := RankFilter(fresh, 5, 0) if err != nil { t.Fatalf("RankFilter: %v", err) } for i := range 16 { if rank.FloatAt(i) != wantRank.FloatAt(i) { t.Fatalf("RankFilter(view)[%d] = %v, want %v", i, rank.FloatAt(i), wantRank.FloatAt(i)) } } b, a, err := ButterworthLowPass(2, 2.0, 0.4) if err != nil { t.Fatal(err) } filtered, err := Filtfilt(b, a, view) if err != nil { t.Fatalf("Filtfilt: %v", err) } wantFiltered, err := Filtfilt(b, a, fresh) if err != nil { t.Fatalf("Filtfilt: %v", err) } for i := range 16 { if math.Float64bits(filtered.FloatAt(i)) != math.Float64bits(wantFiltered.FloatAt(i)) { t.Fatalf("Filtfilt(view)[%d] = %v, want %v", i, filtered.FloatAt(i), wantFiltered.FloatAt(i)) } } coef, err := DaubechiesDWT(view, DB2, 1, DWTPeriodic) if err != nil { t.Fatalf("DaubechiesDWT: %v", err) } wantCoef, err := DaubechiesDWT(fresh, DB2, 1, DWTPeriodic) if err != nil { t.Fatalf("DaubechiesDWT: %v", err) } for i := range coef.Len() { if math.Float64bits(coef.FloatAt(i)) != math.Float64bits(wantCoef.FloatAt(i)) { t.Fatalf("DaubechiesDWT(view)[%d] = %v, want %v", i, coef.FloatAt(i), wantCoef.FloatAt(i)) } } } // TestWindowKaiserRefusesNonFiniteBeta pins the refusal of a beta the // Bessel ratio cannot carry: +Inf used to answer an all-NaN window. func TestWindowKaiserRefusesNonFiniteBeta(t *testing.T) { for _, beta := range []float64{math.Inf(1), math.Inf(-1), math.NaN()} { if _, err := WindowKaiser(8, beta, false); err == nil { t.Fatalf("WindowKaiser beta %g: expected an error, got none", beta) } } }