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

226 lines
8.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package integrate_test
// Runnable examples for the package: the flagship workflows, each
// with a fixed output that `go test` checks, so the printed
// documentation cannot drift from the code.
import (
"fmt"
"log"
"math"
tensor "sourcedock.dev/petrbalvin/tensor"
"sourcedock.dev/petrbalvin/tensor/integrate"
)
// The stiff scalar problem y' = −1000·(y − cos t) − sin t with
// y(0) = 1, whose exact solution is y = cos t. The variable-order,
// variable-step BDF scheme takes the long steps the solution's
// smoothness allows where a fixed small step would be forced by the
// fast transient, and BDFVarStats reports what it did.
func ExampleIntegrateBDFVar() {
f := func(t float64, y *tensor.Array) (*tensor.Array, error) {
return tensor.FromFloats([]float64{-1000*(y.FloatAt(0)-math.Cos(t)) - math.Sin(t)}, 1)
}
y0, _ := tensor.FromFloats([]float64{1}, 1)
var stats integrate.BDFVarStats
y, err := integrate.IntegrateBDFVar(f, 0, 1, y0, integrate.BDFVarOptions{Stats: &stats})
if err != nil {
log.Fatal(err)
}
fmt.Printf("y(1) = %.6f, exact %.6f\n", y.FloatAt(0), math.Cos(1))
fmt.Printf("accepted %d steps, rejected %d, highest order %d\n", stats.Steps, stats.Rejected, stats.MaxOrder)
// Output:
// y(1) = 0.540302, exact 0.540302
// accepted 21 steps, rejected 1, highest order 5
}
// Event detection along a trajectory: the oscillator y″ = −y started
// at y = (1, 0) passes the level y = 0.5 falling at t = π/3 and rising
// at t = 5π/3. Each watch carries its own direction filter, and
// IntegrateODEEvents returns the crossings sorted by time alongside
// the final state.
func ExampleIntegrateODEEvents() {
f := func(t float64, y *tensor.Array) (*tensor.Array, error) {
return tensor.FromFloats([]float64{y.FloatAt(1), -y.FloatAt(0)}, 2)
}
y0, _ := tensor.FromFloats([]float64{1, 0}, 2)
// The same level function twice, with opposite direction filters;
// Direction 0 would record both crossings on one watch.
level := func(t float64, y *tensor.Array) (float64, error) { return y.FloatAt(0) - 0.5, nil }
watches := []integrate.ODEWatch{
{Function: level, Direction: -1},
{Function: level, Direction: +1},
}
hits, final, err := integrate.IntegrateODEEvents(f, 0, 7, y0, watches, integrate.ODEOptions{})
if err != nil {
log.Fatal(err)
}
for _, h := range hits {
direction := "falling"
if h.Rising {
direction = "rising"
}
fmt.Printf("watch %d fired at t = %.4f (%s), y = %.4f\n", h.Watch, h.Time, direction, h.State.FloatAt(0))
}
fmt.Printf("y(7) = %.4f\n", final.FloatAt(0))
// Output:
// watch 0 fired at t = 1.0472 (falling), y = 0.5000
// watch 1 fired at t = 5.2360 (rising), y = 0.5000
// y(7) = 0.7539
}
// A symplectic integrator on a separable Hamiltonian: the harmonic
// oscillator q″ = −q with unit mass, q(0) = 1 and p(0) = 0, whose
// energy ½(p² + q²) stays in a bounded band instead of drifting.
// The step stays fixed by design; only the number of steps is chosen.
func ExampleIntegrateVerlet() {
accel := func(q *tensor.Array) (*tensor.Array, error) {
return tensor.FromFloats([]float64{-q.FloatAt(0)}, 1)
}
q0, _ := tensor.FromFloats([]float64{1}, 1)
p0, _ := tensor.FromFloats([]float64{0}, 1)
const steps = 4000
positions, momenta, err := integrate.IntegrateVerlet(accel, 0, 2*math.Pi, q0, p0, steps)
if err != nil {
log.Fatal(err)
}
worst := 0.0
for s := range steps + 1 {
q, p := positions[s].FloatAt(0), momenta[s].FloatAt(0)
drift := math.Abs(0.5*(p*p+q*q) - 0.5)
worst = math.Max(worst, drift)
}
fmt.Printf("q(2π) = %.6f, p(2π) = %.2e\n", positions[steps].FloatAt(0), momenta[steps].FloatAt(0))
fmt.Printf("worst energy deviation over the period: %.2e\n", worst)
// Output:
// q(2π) = 1.000000, p(2π) = -6.46e-07
// worst energy deviation over the period: 3.08e-07
}
// Quadrature and cubature: a Gauss-Legendre rule read from the
// package's cache, an adaptive integral over an infinite range, and a
// two-dimensional integral by globally adaptive bisection.
func Example_quadratureAndCubature() {
nodes, weights, err := integrate.GaussLegendreNodes(3)
if err != nil {
log.Fatal(err)
}
for i := range nodes {
fmt.Printf("node %.6f, weight %.6f\n", nodes[i], weights[i])
}
value, errEst, err := integrate.IntegrateFunction(func(x float64) (float64, error) {
return math.Exp(-x * x), nil
}, 0, math.Inf(1), integrate.QuadratureOptions{})
if err != nil {
log.Fatal(err)
}
fmt.Printf("the Gaussian tail integrates to %.6f (error estimate %.1e)\n", value, errEst)
area, err := integrate.IntegrateND(func(x []float64) float64 {
return x[0] * x[1]
}, []float64{0, 0}, []float64{1, 1}, integrate.CubatureOptions{})
if err != nil {
log.Fatal(err)
}
fmt.Printf("x·y over the unit square integrates to %.6f\n", area)
// Output:
// node -0.774597, weight 0.555556
// node 0.000000, weight 0.888889
// node 0.774597, weight 0.555556
// the Gaussian tail integrates to 0.886227 (error estimate 9.8e-12)
// x·y over the unit square integrates to 0.250000
}
// Heat evolution in one dimension: u_t = u_xx on [0, 1] from
// u = sin(πx), Dirichlet ends held at zero. The sampled history is a
// (samples, n) array of interior states, and the centre decays as the
// exact e^(−π²t)·sin(π/2) predicts.
func ExampleIntegrateHeat1D() {
const n, samples = 399, 5
const dx, tFinal = 1.0 / 400, 0.1
u0 := make([]float64, n)
for i := range u0 {
u0[i] = math.Sin(math.Pi * float64(i+1) * dx)
}
state, err := tensor.FromFloats(u0, n)
if err != nil {
log.Fatal(err)
}
history, err := integrate.IntegrateHeat1D(state, 1, dx, tFinal, 1e-4, samples, 0, 0)
if err != nil {
log.Fatal(err)
}
centre := history.FloatAt((samples-1)*n + n/2)
exact := math.Exp(-math.Pi * math.Pi * tFinal)
fmt.Printf("history shape %v\n", history.Shape())
fmt.Printf("u(1/2, 0.1) = %.6f, exact %.6f\n", centre, exact)
// Output:
// history shape [5 399]
// u(1/2, 0.1) = 0.372710, exact 0.372708
}
// The finite element Poisson solve: −∇·(κ∇u) = f on the unit square
// with κ = 1 and Dirichlet data on the boundary ring. The manufactured
// solution u = sin(πx)·sin(πy) makes f = 2π²·sin(πx)·sin(πy), and the
// P1 solution reproduces it to the mesh's accuracy at the centre.
func ExampleSolvePoissonFEM2D() {
const cells = 16
mesh, err := integrate.GridTriangleMesh2D(0, 0, 1, 1, cells, cells)
if err != nil {
log.Fatal(err)
}
var nodes []int
var values []float64
for v := range mesh.Vertices2() {
x, y := mesh.Vertices[2*v], mesh.Vertices[2*v+1]
onEdge := x == 0 || x == 1 || y == 0 || y == 1
if onEdge {
nodes = append(nodes, v)
values = append(values, math.Sin(math.Pi*x)*math.Sin(math.Pi*y))
}
}
u, err := integrate.SolvePoissonFEM2D(mesh, func(x, y float64) float64 {
return 2 * math.Pi * math.Pi * math.Sin(math.Pi*x) * math.Sin(math.Pi*y)
}, integrate.FEMPoissonOptions{Kappa: 1, DirichletNodes: nodes, DirichletValues: values})
if err != nil {
log.Fatal(err)
}
centre := (cells/2)*(cells+1) + cells/2
fmt.Printf("mesh of %d vertices, %d triangles, %d boundary edges\n",
mesh.Vertices2(), mesh.Triangles3(), len(mesh.BoundaryEdges())/2)
fmt.Printf("u(1/2, 1/2) = %.4f on this mesh, exact 1.0000\n", u.FloatAt(centre))
// Output:
// mesh of 289 vertices, 512 triangles, 64 boundary edges
// u(1/2, 1/2) = 0.9946 on this mesh, exact 1.0000
}
// The two-point boundary value problem: y″ = −y with y(0) = 0 and
// y(π/2) = 1, solved by shooting on the free initial slope. The slope
// comes out as 1 and the sampled trajectory traces y = sin t.
func ExampleIntegrateBoundary() {
f := func(t float64, y *tensor.Array) (*tensor.Array, error) {
return tensor.FromFloats([]float64{y.FloatAt(1), -y.FloatAt(0)}, 2)
}
y0, _ := tensor.FromFloats([]float64{0, 0.5}, 2)
bc := integrate.BoundaryConditions{Start: []int{0}, End: []int{0}, EndValues: []float64{1}}
times, states, err := integrate.IntegrateBoundary(f, 0, math.Pi/2, y0, bc, 3,
integrate.ODEOptions{RelTol: 1e-10, AbsTol: 1e-13})
if err != nil {
log.Fatal(err)
}
fmt.Printf("shooting slope y'(0) = %.6f\n", states[0].FloatAt(1))
for i := range times {
fmt.Printf("y(%.4f) = %.6f, exact %.6f\n", times[i], states[i].FloatAt(0), math.Sin(times[i]))
}
// Output:
// shooting slope y'(0) = 1.000000
// y(0.0000) = 0.000000, exact 0.000000
// y(0.7854) = 0.707107, exact 0.707107
// y(1.5708) = 1.000000, exact 1.000000
}