// Copyright (c) 2026 Petr BalvĂ­n (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) } }