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
+219
View File
@@ -0,0 +1,219 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package optim_test
import (
"fmt"
"math"
"sourcedock.dev/petrbalvin/tensor"
"sourcedock.dev/petrbalvin/tensor/optim"
)
// ExampleFindRoot brackets the root of a cubic between 2 and 3. The
// bracket changes sign, which is all Brent's method needs.
func ExampleFindRoot() {
f := func(x float64) float64 { return x*x*x - 2*x - 5 }
root, err := optim.FindRoot(f, 2, 3, 1e-12)
if err != nil {
fmt.Println("root finding failed:", err)
return
}
fmt.Printf("root = %.10f\n", root)
fmt.Printf("residual = %.3e\n", math.Abs(f(root)))
// Output:
// root = 2.0945514815
// residual = 3.553e-15
}
// ExampleFindRootSystem solves a coupled pair of equations,
// x0 + x1 = 3 and x0² − x1 = 1, from the starting guess (1.5, 1.5).
func ExampleFindRootSystem() {
system := func(x *tensor.Array) (*tensor.Array, error) {
x0, x1 := x.FloatAt(0), x.FloatAt(1)
return tensor.FromFloats([]float64{x0 + x1 - 3, x0*x0 - x1 - 1}, 2)
}
x0, _ := tensor.FromFloats([]float64{1.5, 1.5}, 2)
x, residual, err := optim.FindRootSystem(system, x0, optim.RootSystemOptions{})
if err != nil {
fmt.Println("system solve failed:", err)
return
}
fmt.Printf("x = (%.6f, %.6f)\n", x.FloatAt(0), x.FloatAt(1))
fmt.Printf("residual = %.3e\n", residual)
// Output:
// x = (1.561553, 1.438447)
// residual = 4.841e-14
}
// ExampleLevenbergMarquardt fits the exponential decay y = a·exp(−b·t)
// to seven perturbed samples, recovering both parameters from the
// samples alone. The generator is seeded, so the data and the fit are
// reproduced exactly on every run.
func ExampleLevenbergMarquardt() {
const wantA, wantB = 2.5, 1.3
g := tensor.NewGenerator(7)
ts := make([]float64, 7)
ys := make([]float64, 7)
for i := range ts {
ts[i] = 0.5 * float64(i)
ys[i] = wantA*math.Exp(-wantB*ts[i]) + 0.02*g.NormalUnit()
}
// The residual the fit drives to zero holds one entry per
// observation: the model at the parameters minus the sample.
residual := func(p *tensor.Array) (*tensor.Array, error) {
out := make([]float64, len(ts))
for i, t := range ts {
out[i] = p.FloatAt(0)*math.Exp(-p.FloatAt(1)*t) - ys[i]
}
return tensor.FromFloats(out, len(out))
}
p0, _ := tensor.FromFloats([]float64{1, 1}, 2)
p, chi2, err := optim.LevenbergMarquardt(residual, p0, optim.LMOptions{})
if err != nil {
fmt.Println("fit failed:", err)
return
}
fmt.Printf("a = %.4f (want %.4f)\n", p.FloatAt(0), wantA)
fmt.Printf("b = %.4f (want %.4f)\n", p.FloatAt(1), wantB)
fmt.Printf("chi2 = %.6f\n", chi2)
// Output:
// a = 2.5325 (want 2.5000)
// b = 1.2978 (want 1.3000)
// chi2 = 0.000404
}
// ExampleMinimiseLBFGS minimises the Rosenbrock function inside a box,
// from a start that lies outside it: the iterate is projected onto the
// box rather than refused.
func ExampleMinimiseLBFGS() {
rosenbrock := func(x *tensor.Array) (float64, error) {
xx, yy := x.FloatAt(0), x.FloatAt(1)
return 100*(yy-xx*xx)*(yy-xx*xx) + (1-xx)*(1-xx), nil
}
gradient := func(x *tensor.Array) (*tensor.Array, error) {
xx, yy := x.FloatAt(0), x.FloatAt(1)
return tensor.FromFloats([]float64{
-400*xx*(yy-xx*xx) - 2*(1-xx),
200 * (yy - xx*xx),
}, 2)
}
x0, _ := tensor.FromFloats([]float64{-3, 5}, 2)
opts := optim.LBFGSOptions{
Lower: []float64{-2, -1},
Upper: []float64{2, 3},
}
x, f, err := optim.MinimiseLBFGS(rosenbrock, gradient, x0, opts)
if err != nil {
fmt.Println("minimisation failed:", err)
return
}
fmt.Printf("x = (%.4f, %.4f)\n", x.FloatAt(0), x.FloatAt(1))
fmt.Printf("f = %.3e\n", f)
// Output:
// x = (1.0000, 1.0000)
// f = 3.369e-21
}
// ExampleMinimiseConstrained minimises the distance to (1, 1) subject
// to the equality row x0 + x1 = 1, which the unconstrained answer
// violates. The augmented Lagrangian pulls the iterate onto the row.
func ExampleMinimiseConstrained() {
objective := func(x *tensor.Array) (float64, error) {
d0, d1 := x.FloatAt(0)-1, x.FloatAt(1)-1
return d0*d0 + d1*d1, nil
}
gradient := func(x *tensor.Array) (*tensor.Array, error) {
return tensor.FromFloats([]float64{2 * (x.FloatAt(0) - 1), 2 * (x.FloatAt(1) - 1)}, 2)
}
a, _ := tensor.FromFloats([]float64{1, 1}, 1, 2)
cons := optim.LinearConstraints{
A: a,
Lower: []float64{1},
Upper: []float64{1},
}
x0, _ := tensor.FromFloats([]float64{0, 0}, 2)
x, f, err := optim.MinimiseConstrained(objective, gradient, x0, cons, optim.LBFGSOptions{})
if err != nil {
fmt.Println("minimisation failed:", err)
return
}
fmt.Printf("x = (%.4f, %.4f)\n", x.FloatAt(0), x.FloatAt(1))
fmt.Printf("x0+x1 = %.4f\n", x.FloatAt(0)+x.FloatAt(1))
fmt.Printf("f = %.4f\n", f)
// Output:
// x = (0.5000, 0.5000)
// x0+x1 = 1.0000
// f = 0.5000
}
// ExampleMinimiseLinearRows solves a linear program in the house
// two-sided form: maximise 3x0 + 2x1, written as the minimisation of
// its negation, subject to x0 + x1 ≤ 4, x0 + 3x1 ≤ 6 and x0, x1 ≥ 0.
func ExampleMinimiseLinearRows() {
cost, _ := tensor.FromFloats([]float64{-3, -2}, 2)
a, _ := tensor.FromFloats([]float64{
1, 1,
1, 3,
1, 0,
0, 1,
}, 4, 2)
cons := optim.LinearConstraints{
A: a,
Lower: []float64{
math.Inf(-1), // -∞ ≤ x0 + x1
math.Inf(-1), // -∞ ≤ x0 + 3x1
0, // 0 ≤ x0
0, // 0 ≤ x1
},
Upper: []float64{
4, 6, // x0 + x1 ≤ 4, x0 + 3x1 ≤ 6
math.Inf(1), math.Inf(1),
},
}
x, value, err := optim.MinimiseLinearRows(cost, cons, optim.LinearProgramOptions{})
if err != nil {
fmt.Println("linear program failed:", err)
return
}
fmt.Printf("x = (%.4f, %.4f)\n", x.FloatAt(0), x.FloatAt(1))
fmt.Printf("c·x = %.4f\n", value)
// Output:
// x = (4.0000, 0.0000)
// c·x = -12.0000
}
// ExampleMinimiseDifferentialEvolution finds the global minimum of the
// two-dimensional Rastrigin function, a landscape of many local minima
// around one global minimum at the origin. The seed makes the run
// reproducible.
func ExampleMinimiseDifferentialEvolution() {
rastrigin := func(x *tensor.Array) (float64, error) {
sum := 20.0
for i := range 2 {
v := x.FloatAt(i)
sum += v*v - 10*math.Cos(2*math.Pi*v)
}
return sum, nil
}
lower, _ := tensor.FromFloats([]float64{-5.12, -5.12}, 2)
upper, _ := tensor.FromFloats([]float64{5.12, 5.12}, 2)
opts := optim.DifferentialEvolutionOptions{Seed: 7, Population: 60, Generations: 400}
x, f, err := optim.MinimiseDifferentialEvolution(rastrigin, lower, upper, opts)
if err != nil {
fmt.Println("global search failed:", err)
return
}
// The search reaches the origin. The residual coordinate is a
// round-off of the population's spread, so its sign is not fixed
// by the algorithm; the distance to the minimum is what the run
// guarantees.
fmt.Printf("f = %.6f\n", f)
fmt.Printf("|x| < 1e-6: %v\n", math.Hypot(x.FloatAt(0), x.FloatAt(1)) < 1e-6)
// Output:
// f = 0.000000
// |x| < 1e-6: true
}