Files
petrbalvin af4ee19703
Release / gates (push) Successful in 4m38s
Test / test (push) Successful in 5m16s
Release / release (push) Successful in 35s
feat: initial release
Assisted-by: GLM 5.3 Flash
2026-09-03 10:00:00 +02:00

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)
}
}
}
}
}