feat: initial release
Release / gates (push) Successful in 4m38s
Test / test (push) Successful in 5m16s
Release / release (push) Successful in 35s

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-03 10:00:00 +02:00
commit af4ee19703
617 changed files with 191195 additions and 0 deletions
+156
View File
@@ -0,0 +1,156 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package core
import (
"errors"
"math"
"strings"
"testing"
)
// TestJacobianAnalytic pins the Jacobian of f(x, y) = (x·y, x+y),
// whose exact derivative [[y, x], [1, 1]] the central differences
// must reproduce to 1e-8, at points that exercise the step scaling.
func TestJacobianAnalytic(t *testing.T) {
f := func(x *Array) (*Array, error) {
xv, yv := x.FloatAt(0), x.FloatAt(1)
return FromFloats([]float64{xv * yv, xv + yv}, 2)
}
cases := []struct {
name string
x, y float64
want []float64
}{
{"first quadrant", 2, 3, []float64{3, 2, 1, 1}},
{"negative coordinate", -1.5, 4, []float64{4, -1.5, 1, 1}},
{"origin", 0, 0, []float64{0, 0, 1, 1}},
}
for _, c := range cases {
j, err := Jacobian(f, mustFloats(t, []float64{c.x, c.y}), JacobianOptions{})
if err != nil {
t.Fatalf("%s: %v", c.name, err)
}
if shape := j.Shape(); len(shape) != 2 || shape[0] != 2 || shape[1] != 2 {
t.Fatalf("%s: shape %v, want (2, 2)", c.name, shape)
}
for i, want := range c.want {
if got := j.FloatAt(i); math.Abs(got-want) > 1e-8 {
t.Errorf("%s: entry %d = %v, want %v", c.name, i, got, want)
}
}
}
}
// TestJacobianNonlinear pins a nonlinear scalar-input case,
// f(x) = (x², x³) at x = 1.7, whose Jacobian is the column
// (2x, 3x²) = (3.4, 8.67).
func TestJacobianNonlinear(t *testing.T) {
f := func(x *Array) (*Array, error) {
xv := x.FloatAt(0)
return FromFloats([]float64{xv * xv, xv * xv * xv}, 2)
}
j, err := Jacobian(f, mustFloats(t, []float64{1.7}), JacobianOptions{})
if err != nil {
t.Fatalf("Jacobian: %v", err)
}
// The x³ column sits at the default step's rounding floor
// (~2·10⁻⁸ of cancellation noise), so the pin is 2e-8 there.
tols := []float64{1e-8, 2e-8}
for i, want := range []float64{3.4, 8.67} {
if got := j.FloatAt(i); math.Abs(got-want) > tols[i] {
t.Errorf("entry %d = %v, want %v", i, got, want)
}
}
}
// TestJacobianCustomStep checks that a caller-supplied step is
// honoured: on a linear field any step is exact, so the result pins
// the matrix rather than the step heuristic.
func TestJacobianCustomStep(t *testing.T) {
f := func(x *Array) (*Array, error) {
return FromFloats([]float64{2*x.FloatAt(0) - x.FloatAt(1)}, 1)
}
j, err := Jacobian(f, mustFloats(t, []float64{5, -7}), JacobianOptions{Step: 1e-6})
if err != nil {
t.Fatalf("Jacobian: %v", err)
}
if shape := j.Shape(); len(shape) != 2 || shape[0] != 1 || shape[1] != 2 {
t.Fatalf("shape %v, want (1, 2)", shape)
}
for i, want := range []float64{2, -1} {
// The wide 1e-6 step leaves ~2e-9 of cancellation noise on
// values of size ~17; the pin still says which matrix it is.
if got := j.FloatAt(i); math.Abs(got-want) > 1e-8 {
t.Errorf("entry %d = %v, want %v", i, got, want)
}
}
}
// TestJacobianRejects pins the contracts: complex points, empty
// points, non-vector outputs, outputs whose length changes between
// columns, and errors from f all come back as errors.
func TestJacobianRejects(t *testing.T) {
complexPoint, _ := FromComplexes([]complex128{1, 2}, 2)
if _, err := Jacobian(vectorIdentity, complexPoint, JacobianOptions{}); err == nil {
t.Error("expected an error for a complex point")
} else if !strings.Contains(err.Error(), "complex") {
t.Errorf("complex point: error %q lacks \"complex\"", err)
}
if _, err := Jacobian(vectorIdentity, mustFloats(t, nil), JacobianOptions{}); err == nil {
t.Error("expected an error for an empty point")
}
matrixOut := func(x *Array) (*Array, error) {
return FromFloats([]float64{1, 0, 0, 1}, 2, 2)
}
if _, err := Jacobian(matrixOut, mustFloats(t, []float64{1}), JacobianOptions{}); err == nil {
t.Error("expected an error for a matrix-shaped output")
} else if !strings.Contains(err.Error(), "vector") {
t.Errorf("matrix output: error %q lacks \"vector\"", err)
}
emptyOut := func(x *Array) (*Array, error) {
return FromFloats([]float64{}, 0)
}
if _, err := Jacobian(emptyOut, mustFloats(t, []float64{1}), JacobianOptions{}); err == nil {
t.Error("expected an error for an empty output")
}
calls := 0
changingLength := func(x *Array) (*Array, error) {
calls++
if calls == 1 {
return FromFloats([]float64{1, 2}, 2)
}
return FromFloats([]float64{1, 2, 3}, 3)
}
if _, err := Jacobian(changingLength, mustFloats(t, []float64{1, 2}), JacobianOptions{}); err == nil {
t.Error("expected an error for an output length that changes between columns")
}
sentinel := errf("f blew up")
failing := func(x *Array) (*Array, error) {
if x.FloatAt(0) != 1 {
return nil, sentinel
}
return FromFloats([]float64{1}, 1)
}
_, err := Jacobian(failing, mustFloats(t, []float64{1}), JacobianOptions{})
if err == nil {
t.Error("expected the probe failure to propagate")
} else if !strings.Contains(err.Error(), "blew up") {
t.Errorf("probe failure: error %q lacks the cause", err)
} else if !errors.Is(err, sentinel) {
// The wrap keeps the chain open, so the sentinel stays reachable
// through the entry point's context: a revert to a plain %v
// formatting fails exactly here.
t.Errorf("probe failure: error %q does not unwrap to the cause", err)
}
}
// vectorIdentity is a well-behaved f for the input-side rejections.
func vectorIdentity(x *Array) (*Array, error) {
return FromFloats([]float64{x.FloatAt(0), x.FloatAt(1)}, 2)
}