199 lines
5.8 KiB
Go
199 lines
5.8 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|||
|
|
// SPDX-License-Identifier: MIT
|
||
|
|
|
||
|
|
package core
|
||
|
|
|
||
|
|
import (
|
||
|
|
"math"
|
||
|
|
"testing"
|
||
|
|
)
|
||
|
|
|
||
|
|
// TestInterpolateMonotoneShape checks the two promises of the
|
||
|
|
// Fritsch-Carlson tangents: the curve passes through every knot, and
|
||
|
|
// interpolating monotone data never overshoots the data's range.
|
||
|
|
func TestInterpolateMonotoneShape(t *testing.T) {
|
||
|
|
xv := []float64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||
|
|
yv := []float64{0, 0.1, 1.9, 2.0, 2.1, 2.2, 3.9, 4.0, 4.1, 8}
|
||
|
|
xs := mustFloats(t, xv, len(xv))
|
||
|
|
ys := mustFloats(t, yv, len(yv))
|
||
|
|
dense := make([]float64, 0, 801)
|
||
|
|
for i := range 801 {
|
||
|
|
dense = append(dense, -1+float64(i)*10/800)
|
||
|
|
}
|
||
|
|
q := mustFloats(t, dense, len(dense))
|
||
|
|
out, err := InterpolateMonotone(xs, ys, q)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("InterpolateMonotone: %v", err)
|
||
|
|
}
|
||
|
|
for k := range xv {
|
||
|
|
if got := out.FloatAt((k + 1) * 80); math.Abs(got-yv[k]) > 1e-12 {
|
||
|
|
t.Fatalf("knot %d: %.12g, want %.12g", k, got, yv[k])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for i := range out.Len() {
|
||
|
|
if v := out.FloatAt(i); v < -1e-12 || v > 8+1e-12 {
|
||
|
|
t.Fatalf("overshoot at %g: %.12g outside the data range", dense[i], v)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestInterpolateMonotoneBasics checks the two-knot limit (a
|
||
|
|
// straight line), the flat clamp outside, and the refusal of
|
||
|
|
// repeated knots.
|
||
|
|
func TestInterpolateMonotoneBasics(t *testing.T) {
|
||
|
|
xs := mustFloats(t, []float64{0, 1}, 2)
|
||
|
|
ys := mustFloats(t, []float64{10, 20}, 2)
|
||
|
|
q := mustFloats(t, []float64{0.25, -3, 4}, 3)
|
||
|
|
out, err := InterpolateMonotone(xs, ys, q)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("InterpolateMonotone: %v", err)
|
||
|
|
}
|
||
|
|
if got := out.FloatAt(0); math.Abs(got-12.5) > 1e-12 {
|
||
|
|
t.Fatalf("linear read = %.12g, want 12.5", got)
|
||
|
|
}
|
||
|
|
if got := out.FloatAt(1); got != 10 {
|
||
|
|
t.Fatalf("clamp below = %.12g, want 10", got)
|
||
|
|
}
|
||
|
|
if got := out.FloatAt(2); got != 20 {
|
||
|
|
t.Fatalf("clamp above = %.12g, want 20", got)
|
||
|
|
}
|
||
|
|
bad := mustFloats(t, []float64{1, 1, 2}, 3)
|
||
|
|
if _, err := InterpolateMonotone(bad, ys, q); err == nil {
|
||
|
|
t.Fatal("repeated knots accepted")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestInterpolateGrid checks multilinear reads: exact at the nodes,
|
||
|
|
// the bilinear average at a cell centre, clamped outside, and the
|
||
|
|
// argument guards.
|
||
|
|
func TestInterpolateGrid(t *testing.T) {
|
||
|
|
grid := mustFloats(t, []float64{
|
||
|
|
0, 10,
|
||
|
|
20, 30,
|
||
|
|
}, 2, 2)
|
||
|
|
origins := []float64{0, 0}
|
||
|
|
steps := []float64{1, 0.5}
|
||
|
|
// Node reads land exactly; the cell centre averages the corners.
|
||
|
|
queries := mustFloats(t, []float64{
|
||
|
|
0, 0,
|
||
|
|
1, 0.5,
|
||
|
|
0.5, 0.25,
|
||
|
|
-9, 9,
|
||
|
|
}, 4, 2)
|
||
|
|
out, err := InterpolateGrid(grid, origins, steps, queries)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("InterpolateGrid: %v", err)
|
||
|
|
}
|
||
|
|
if got := out.FloatAt(0); got != 0 {
|
||
|
|
t.Fatalf("node (0,0) = %.12g, want 0", got)
|
||
|
|
}
|
||
|
|
if got := out.FloatAt(1); got != 30 {
|
||
|
|
t.Fatalf("node (1,0.5) = %.12g, want 30", got)
|
||
|
|
}
|
||
|
|
if got := out.FloatAt(2); math.Abs(got-15) > 1e-12 {
|
||
|
|
t.Fatalf("cell centre = %.12g, want 15", got)
|
||
|
|
}
|
||
|
|
if got := out.FloatAt(3); got != 10 {
|
||
|
|
t.Fatalf("clamp = %.12g, want 10", got)
|
||
|
|
}
|
||
|
|
if _, err := InterpolateGrid(grid, origins, []float64{1, -1}, queries); err == nil {
|
||
|
|
t.Fatal("negative step accepted")
|
||
|
|
}
|
||
|
|
badQueries := mustFloats(t, []float64{0, 0, 0}, 3, 1)
|
||
|
|
if _, err := InterpolateGrid(grid, origins, steps, badQueries); err == nil {
|
||
|
|
t.Fatal("wrong query columns accepted")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestDiff checks the finite differences on a known series and along
|
||
|
|
// a chosen axis of a matrix, plus the argument guards.
|
||
|
|
func TestDiff(t *testing.T) {
|
||
|
|
a := mustFloats(t, []float64{1, 4, 9, 16}, 4)
|
||
|
|
d1, err := Diff(a, 1, 0)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Diff: %v", err)
|
||
|
|
}
|
||
|
|
want := []float64{3, 5, 7}
|
||
|
|
for i := range want {
|
||
|
|
if d1.FloatAt(i) != want[i] {
|
||
|
|
t.Fatalf("first difference %d = %g, want %g", i, d1.FloatAt(i), want[i])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
d2, err := Diff(a, 2, 0)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Diff order 2: %v", err)
|
||
|
|
}
|
||
|
|
if d2.FloatAt(0) != 2 || d2.FloatAt(1) != 2 {
|
||
|
|
t.Fatalf("second difference = [%g, %g], want [2, 2]", d2.FloatAt(0), d2.FloatAt(1))
|
||
|
|
}
|
||
|
|
m := mustFloats(t, []float64{1, 4, 2, 8}, 2, 2)
|
||
|
|
dm, err := Diff(m, 1, 0)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Diff axis 0: %v", err)
|
||
|
|
}
|
||
|
|
if dm.Shape()[0] != 1 || dm.Shape()[1] != 2 {
|
||
|
|
t.Fatalf("shape %v, want [1 2]", dm.Shape())
|
||
|
|
}
|
||
|
|
if dm.FloatAt(1) != 4 {
|
||
|
|
t.Fatalf("axis-0 difference column 1 = %g, want 4", dm.FloatAt(1))
|
||
|
|
}
|
||
|
|
if _, err := Diff(a, 4, 0); err == nil {
|
||
|
|
t.Fatal("order at the axis length accepted")
|
||
|
|
}
|
||
|
|
if _, err := Diff(m, 1, 5); err == nil {
|
||
|
|
t.Fatal("axis past the rank accepted")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestDiffNonLeadingAxis pins the run walk of Diff along an axis that is
|
||
|
|
// neither leading nor trailing: several outer positions (head) each hold
|
||
|
|
// a run of trailing elements (tail), so a run's destination is not its
|
||
|
|
// own start and the source offsets step by the line length.
|
||
|
|
func TestDiffNonLeadingAxis(t *testing.T) {
|
||
|
|
vals := make([]int64, 24)
|
||
|
|
for i := range vals {
|
||
|
|
vals[i] = int64(i)
|
||
|
|
}
|
||
|
|
src := mustFromInts(t, vals, 2, 3, 4)
|
||
|
|
|
||
|
|
// Axis 1: head 2, line 3, tail 4.
|
||
|
|
got, err := Diff(src, 1, 1)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Diff axis 1: %v", err)
|
||
|
|
}
|
||
|
|
if sh := got.Shape(); len(sh) != 3 || sh[0] != 2 || sh[1] != 2 || sh[2] != 4 {
|
||
|
|
t.Fatalf("Diff axis 1 shape: %v, want [2 2 4]", sh)
|
||
|
|
}
|
||
|
|
for i := range 2 {
|
||
|
|
for j := range 2 {
|
||
|
|
for k := range 4 {
|
||
|
|
hi, _ := IntAt(src, i, j+1, k)
|
||
|
|
lo, _ := IntAt(src, i, j, k)
|
||
|
|
if v, _ := IntAt(got, i, j, k); v != hi-lo {
|
||
|
|
t.Fatalf("Diff axis 1 [%d %d %d] = %d, want %d", i, j, k, v, hi-lo)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Axis 2: head 6, line 4, tail 1.
|
||
|
|
got2, err := Diff(src, 1, 2)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Diff axis 2: %v", err)
|
||
|
|
}
|
||
|
|
if sh := got2.Shape(); len(sh) != 3 || sh[0] != 2 || sh[1] != 3 || sh[2] != 3 {
|
||
|
|
t.Fatalf("Diff axis 2 shape: %v, want [2 3 3]", sh)
|
||
|
|
}
|
||
|
|
for i := range 2 {
|
||
|
|
for j := range 3 {
|
||
|
|
for k := range 3 {
|
||
|
|
hi, _ := IntAt(src, i, j, k+1)
|
||
|
|
lo, _ := IntAt(src, i, j, k)
|
||
|
|
if v, _ := IntAt(got2, i, j, k); v != hi-lo {
|
||
|
|
t.Fatalf("Diff axis 2 [%d %d %d] = %d, want %d", i, j, k, v, hi-lo)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|