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
+236
View File
@@ -0,0 +1,236 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package integrate
import (
"math"
"strings"
"testing"
"sourcedock.dev/petrbalvin/tensor/internal/core"
)
// Regression pins: the event detector's blind first step, watch
// values that signed themselves across zero, PDE parameters that were
// only half guarded, the step recorder's rounded endpoint, and a
// cubature budget the dimension powers could switch off.
// TestEventFirstAcceptedStep: the detector seeded its comparison from
// the END of the first accepted step, so a crossing inside that step
// went unnoticed; the seed is now the watch value at the step's start
// state, and the crossing is refined like any other.
func TestEventFirstAcceptedStep(t *testing.T) {
zero := func(now float64, y *core.Array) (*core.Array, error) {
return mustFloats(t, []float64{0}, 1), nil
}
cross := func(now float64, y *core.Array) (float64, error) {
return now - 0.0005, nil
}
hits, _, err := IntegrateODEEvents(zero, 0, 1, mustFloats(t, []float64{1}, 1),
[]ODEWatch{{Function: cross}}, ODEOptions{RelTol: 1e-12, AbsTol: 1e-14})
if err != nil {
t.Fatalf("IntegrateODEEvents: %v", err)
}
if len(hits) != 1 {
t.Fatalf("hits = %d, want exactly the crossing near 0.0005", len(hits))
}
if math.Abs(hits[0].Time-0.0005) > 1e-9 {
t.Fatalf("hit at %.14g, want 0.0005", hits[0].Time)
}
}
// TestEventWatchNonFiniteRefused: a NaN from a watch compared false in
// the sign test and manufactured a crossing (or swallowed one); a
// non-finite watch value is now an error naming the value.
func TestEventWatchNonFiniteRefused(t *testing.T) {
zero := func(now float64, y *core.Array) (*core.Array, error) {
return mustFloats(t, []float64{0}, 1), nil
}
calls := 0
nanFirst := func(now float64, y *core.Array) (float64, error) {
calls++
if calls == 1 {
return math.NaN(), nil
}
return -1, nil
}
_, _, err := IntegrateODEEvents(zero, 0, 1, mustFloats(t, []float64{1}, 1),
[]ODEWatch{{Function: nanFirst}}, ODEOptions{})
if err == nil || !strings.Contains(err.Error(), "non-finite") {
t.Fatalf("a NaN watch value: err = %v", err)
}
infLater := func(now float64, y *core.Array) (float64, error) {
if now > 0.2 {
return math.Inf(-1), nil
}
return -1, nil
}
_, _, err = IntegrateODEEvents(zero, 0, 1, mustFloats(t, []float64{1}, 1),
[]ODEWatch{{Function: infLater}}, ODEOptions{})
if err == nil || !strings.Contains(err.Error(), "non-finite") {
t.Fatalf("an infinite watch value: err = %v", err)
}
}
// TestGridMeshFiniteExtents: a NaN or Inf extent passed the old
// positivity test (a NaN compares false against <= 0) and laid out a
// mesh of non-finite vertices.
func TestGridMeshFiniteExtents(t *testing.T) {
for name, extents := range map[string][2]float64{
"NaN width": {math.NaN(), 1},
"NaN height": {1, math.NaN()},
"Inf width": {math.Inf(1), 1},
"Inf height": {1, math.Inf(-1)},
} {
mesh, err := GridTriangleMesh2D(0, 0, extents[0], extents[1], 2, 2)
if err == nil || !strings.Contains(err.Error(), "finite") {
t.Fatalf("%s: err = %v, mesh = %v", name, err, mesh != nil)
}
}
// A valid grid still builds.
if _, err := GridTriangleMesh2D(0, 0, 1, 1, 2, 2); err != nil {
t.Fatalf("a valid grid: %v", err)
}
}
// TestFEMConstantKappaNonFinite: a +Inf constant conductivity slipped
// through the positivity test and died mid-factorisation.
func TestFEMConstantKappaNonFinite(t *testing.T) {
mesh, err := GridTriangleMesh2D(0, 0, 1, 1, 4, 4)
if err != nil {
t.Fatalf("GridTriangleMesh2D: %v", err)
}
opts := FEMPoissonOptions{Kappa: math.Inf(1), DirichletNodes: []int{0}, DirichletValues: []float64{0}}
if _, err := SolvePoissonFEM2D(mesh, nil, opts); err == nil || !strings.Contains(err.Error(), "positive") {
t.Fatalf("a +Inf constant conductivity: err = %v", err)
}
// With the conductivity field set, a non-finite placeholder for
// the constant is refused all the same.
opts.KappaFunc = func(x, y float64) float64 { return 1 }
if _, err := SolvePoissonFEM2D(mesh, nil, opts); err == nil || !strings.Contains(err.Error(), "positive") {
t.Fatalf("a +Inf placeholder conductivity beside KappaFunc: err = %v", err)
}
}
// TestPDEParameterNonFiniteRefusals walks the solvers' numeric
// parameters: each one used to slip a NaN or Inf past a comparison
// that reads false against NaN and publish an all-NaN history.
func TestPDEParameterNonFiniteRefusals(t *testing.T) {
u1, v1 := mustFloats(t, []float64{0, 1, 0, 1, 0}, 5), mustFloats(t, []float64{0, 0, 0, 0, 0}, 5)
u2 := mustFloats(t, []float64{0, 1, 0, 0, 1, 0, 0, 1, 0}, 3, 3)
cases := []struct {
name string
run func() (*core.Array, error)
}{
{"Heat1D kappa +Inf", func() (*core.Array, error) {
return IntegrateHeat1D(u1, math.Inf(1), 0.1, 0.1, 0.01, 2, 0, 0)
}},
{"Heat1D kappa NaN", func() (*core.Array, error) {
return IntegrateHeat1D(u1, math.NaN(), 0.1, 0.1, 0.01, 2, 0, 0)
}},
{"Heat1D NaN bound", func() (*core.Array, error) {
return IntegrateHeat1D(u1, 1, 0.1, 0.1, 0.01, 2, math.NaN(), 0)
}},
{"Heat1D Inf bound", func() (*core.Array, error) {
return IntegrateHeat1D(u1, 1, 0.1, 0.1, 0.01, 2, 0, math.Inf(1))
}},
{"Wave1D c NaN", func() (*core.Array, error) {
return IntegrateWave1D(u1, v1, math.NaN(), 0.1, 0.1, 0.01, 2)
}},
{"Wave1D c Inf", func() (*core.Array, error) {
return IntegrateWave1D(u1, v1, math.Inf(1), 0.1, 0.1, 0.01, 2)
}},
{"Wave1D v0 NaN", func() (*core.Array, error) {
return IntegrateWave1D(u1, mustFloats(t, []float64{0, math.NaN(), 0, 0, 0}, 5), 1, 0.1, 0.1, 0.01, 2)
}},
{"Heat2D kappa +Inf", func() (*core.Array, error) {
return IntegrateHeat2D(u2, math.Inf(1), 0.1, 0.1, 0.1, 0.01, 2, 0, 0, 0, 0)
}},
{"Heat2D NaN boundary", func() (*core.Array, error) {
return IntegrateHeat2D(u2, 1, 0.1, 0.1, 0.1, 0.01, 2, 0, 0, math.NaN(), 0)
}},
{"Heat2D Inf boundary", func() (*core.Array, error) {
return IntegrateHeat2D(u2, 1, 0.1, 0.1, 0.1, 0.01, 2, 0, 0, 0, math.Inf(1))
}},
{"Wave2D v0 NaN", func() (*core.Array, error) {
return IntegrateWave2D(u2, mustFloats(t, []float64{0, 0, 0, 0, math.NaN(), 0, 0, 0, 0}, 3, 3), 1, 0.1, 0.1, 0.1, 0.01, 2)
}},
}
for _, c := range cases {
if _, err := c.run(); err == nil || !strings.Contains(err.Error(), "finite") && !strings.Contains(err.Error(), "positive") {
t.Fatalf("%s: err = %v, want a finite/positive refusal", c.name, err)
}
}
}
// TestODEStepsEndpointExact: the recorder's last time was the run's
// accumulated t+h, a few ulps off t1; it is now t1 exactly.
func TestODEStepsEndpointExact(t *testing.T) {
decayF := func(now float64, y *core.Array) (*core.Array, error) {
return core.MulF(y, -1), nil
}
times, states, err := IntegrateODESteps(decayF, 0, 0.3, mustFloats(t, []float64{1}, 1), ODEOptions{})
if err != nil {
t.Fatalf("IntegrateODESteps: %v", err)
}
if last := times[len(times)-1]; last != 0.3 {
t.Fatalf("last recorded time = %.17g, want 0.3 exactly", last)
}
// The pinned endpoint still closes on the analytic curve.
if last := states[len(states)-1].FloatAt(0); math.Abs(last-math.Exp(-0.3)) > 1e-6 {
t.Fatalf("y(0.3) = %.14g, want %.14g", last, math.Exp(-0.3))
}
// Across magnitudes the run's own boundary misses t1 by whole
// ulps (t0 = 1e16 has an ulp of 2 and the span is 2): the endpoint
// is pinned regardless, and the recorded state there is the
// answer IntegrateODE itself returns for t1.
flat := func(now float64, y *core.Array) (*core.Array, error) {
return core.FromFloats([]float64{y.FloatAt(0)}, 1)
}
const (
big = 1e16
span = 2.0
)
times, states, err = IntegrateODESteps(flat, big, big+span, mustFloats(t, []float64{3}, 1), ODEOptions{})
if err != nil {
t.Fatalf("IntegrateODESteps across magnitudes: %v", err)
}
if last := times[len(times)-1]; last != big+span {
t.Fatalf("last recorded time = %.17g, want %.17g exactly", last, big+span)
}
if got := states[len(states)-1].FloatAt(0); got != 3 {
t.Fatalf("state at the endpoint = %g, want the constant 3", got)
}
}
// TestCubatureDimensionPowerSaturates: past the twenties the straight
// int powers wrapped, the bisection cost went negative and every
// budget check with it; the powers now saturate and a saturated power
// reads as above the budget.
func TestCubatureDimensionPowerSaturates(t *testing.T) {
lower := make([]float64, 25)
upper := make([]float64, 25)
for i := range upper {
upper[i] = 1
}
f := func(x []float64) float64 { return 1 }
// A small budget is refused on the single-bisection cost alone.
if _, err := IntegrateND(f, lower, upper, CubatureOptions{MaxEvals: 1024}); err == nil || !strings.Contains(err.Error(), "budget") {
t.Fatalf("d = 25 under a 1024-evaluation budget: err = %v", err)
}
// A budget of MaxInt used to walk the powers straight into the
// wrap and then evaluate the 5^25-point root box: the saturated
// computation refuses it before the first evaluation.
if _, err := IntegrateND(f, lower, upper, CubatureOptions{MaxEvals: math.MaxInt}); err == nil || !strings.Contains(err.Error(), "budget") {
t.Fatalf("d = 25 under a MaxInt budget: err = %v", err)
}
// A sane dimension and budget still integrate.
got, err := IntegrateND(f, lower[:3], upper[:3], CubatureOptions{})
if err != nil {
t.Fatalf("d = 3 under the default budget: %v", err)
}
if math.Abs(got-1) > 1e-10 {
t.Fatalf("integral of 1 over the unit cube = %.17g, want 1", got)
}
}