feat: initial release
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
package optim
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/tensor/internal/core"
|
||||
)
|
||||
|
||||
// Solver-coverage pins: every test drives an optimiser on a problem
|
||||
// class with a closed-form answer the existing fixtures do not build,
|
||||
// from corner optima over degenerate active sets to mixed nonlinear
|
||||
// constraints.
|
||||
|
||||
func mustMatrix(t *testing.T, vals []float64, r, c int) *core.Array {
|
||||
t.Helper()
|
||||
a, err := core.FromFloats(vals, r, c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// TestLBFGSCornerOptimumWithEqualityBound minimises
|
||||
// f = (x-3)^2 + (y+1)^2 + (z-2)^2 over x <= 1 with z frozen at 2: the
|
||||
// constrained optimum (1, -1, 2) keeps the wall term at 4, so 4 is the
|
||||
// answer the projected-gradient machinery must report.
|
||||
func TestLBFGSCornerOptimumWithEqualityBound(t *testing.T) {
|
||||
f := func(a *core.Array) (float64, error) {
|
||||
x, y, z := a.FloatAt(0), a.FloatAt(1), a.FloatAt(2)
|
||||
return (x-3)*(x-3) + (y+1)*(y+1) + (z-2)*(z-2), nil
|
||||
}
|
||||
grad := func(a *core.Array) (*core.Array, error) {
|
||||
x, y, z := a.FloatAt(0), a.FloatAt(1), a.FloatAt(2)
|
||||
return core.FromFloats([]float64{2 * (x - 3), 2 * (y + 1), 2 * (z - 2)}, 3)
|
||||
}
|
||||
x0, _ := core.FromFloats([]float64{0, 0, 2}, 3)
|
||||
opts := LBFGSOptions{
|
||||
Tolerance: 1e-12,
|
||||
Lower: []float64{math.Inf(-1), math.Inf(-1), 2},
|
||||
Upper: []float64{1, math.Inf(1), 2},
|
||||
}
|
||||
p, fv, err := MinimiseLBFGS(f, grad, x0, opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if math.Abs(fv-4) > 1e-10 {
|
||||
t.Fatalf("f = %g, want 4", fv)
|
||||
}
|
||||
if math.Abs(p.FloatAt(0)-1) > 1e-9 || math.Abs(p.FloatAt(1)+1) > 1e-9 || p.FloatAt(2) != 2 {
|
||||
t.Fatalf("point (%g, %g, %g), want (1, -1, 2)", p.FloatAt(0), p.FloatAt(1), p.FloatAt(2))
|
||||
}
|
||||
}
|
||||
|
||||
// TestLBFGSCornerOptimumFiniteDifference repeats the corner problem
|
||||
// without a gradient callback, so the one-sided difference stencils at
|
||||
// the walls carry the run.
|
||||
func TestLBFGSCornerOptimumFiniteDifference(t *testing.T) {
|
||||
f := func(a *core.Array) (float64, error) {
|
||||
x, y := a.FloatAt(0), a.FloatAt(1)
|
||||
return (x-3)*(x-3) + (y+1)*(y+1), nil
|
||||
}
|
||||
x0, _ := core.FromFloats([]float64{0, 0}, 2)
|
||||
opts := LBFGSOptions{
|
||||
Tolerance: 1e-10,
|
||||
Lower: []float64{-2, math.Inf(-1)},
|
||||
Upper: []float64{1, math.Inf(1)},
|
||||
MaxIterations: 500,
|
||||
}
|
||||
p, fv, err := MinimiseLBFGS(f, nil, x0, opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if math.Abs(fv-4) > 1e-8 {
|
||||
t.Fatalf("f = %g, want 4", fv)
|
||||
}
|
||||
if math.Abs(p.FloatAt(0)-1) > 1e-5 || math.Abs(p.FloatAt(1)+1) > 1e-5 {
|
||||
t.Fatalf("point (%g, %g), want (1, -1)", p.FloatAt(0), p.FloatAt(1))
|
||||
}
|
||||
}
|
||||
|
||||
// TestQPDegenerateRatioTies builds a problem whose ratio test ties
|
||||
// three rows at one point and whose release cycle must then free a
|
||||
// slack row: min 1/2(x^2+y^2) - x - y over x+y >= 1, x >= 1/2,
|
||||
// y >= 1/2. All rows block at (1/2, 1/2); the optimum is (1, 1) with
|
||||
// the first row slack and a zero multiplier.
|
||||
func TestQPDegenerateRatioTies(t *testing.T) {
|
||||
h, _ := core.FromFloats([]float64{1, 0, 0, 1}, 2, 2)
|
||||
c, _ := core.FromFloats([]float64{-1, -1}, 2)
|
||||
cons := LinearConstraints{
|
||||
A: mustMatrix(t, []float64{1, 1, 1, 0, 0, 1}, 3, 2),
|
||||
Lower: []float64{1, 0.5, 0.5},
|
||||
Upper: []float64{math.Inf(1), math.Inf(1), math.Inf(1)},
|
||||
}
|
||||
x, fv, multipliers, err := MinimiseQP(h, c, cons, nil, QPOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if math.Abs(x.FloatAt(0)-1) > 1e-8 || math.Abs(x.FloatAt(1)-1) > 1e-8 {
|
||||
t.Fatalf("point (%g, %g), want (1, 1)", x.FloatAt(0), x.FloatAt(1))
|
||||
}
|
||||
if math.Abs(fv+1) > 1e-10 {
|
||||
t.Fatalf("f = %g, want -1", fv)
|
||||
}
|
||||
if len(multipliers) != 3 {
|
||||
t.Fatalf("multipliers %v", multipliers)
|
||||
}
|
||||
if multipliers[0] > 1e-10 {
|
||||
t.Fatalf("slack row multiplier %g, want 0", multipliers[0])
|
||||
}
|
||||
}
|
||||
|
||||
// TestCMAESRotatedIllConditionedQuadratic runs the strategy on a valley
|
||||
// rotated 45 degrees with a conditioning of 100, where a diagonal
|
||||
// sampler cannot turn and the full covariance must.
|
||||
func TestCMAESRotatedIllConditionedQuadratic(t *testing.T) {
|
||||
f := func(a *core.Array) (float64, error) {
|
||||
x, y := a.FloatAt(0), a.FloatAt(1)
|
||||
u, v := (x+y)/math.Sqrt2, (x-y)/math.Sqrt2
|
||||
return u*u + 100*v*v, nil
|
||||
}
|
||||
x0, _ := core.FromFloats([]float64{5, -5}, 2)
|
||||
p, fv, err := MinimiseCMAES(f, x0, CMAESOptions{Generations: 2000, Tolerance: 1e-10})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fv > 1e-8 {
|
||||
t.Fatalf("f = %g, want ~0", fv)
|
||||
}
|
||||
if math.Abs(p.FloatAt(0)) > 1e-3 || math.Abs(p.FloatAt(1)) > 1e-3 {
|
||||
t.Fatalf("point (%g, %g), want ~(0, 0)", p.FloatAt(0), p.FloatAt(1))
|
||||
}
|
||||
}
|
||||
|
||||
// TestDifferentialEvolutionBowl drives the rand/1/bin scheme on a
|
||||
// separable bowl to full precision against the seeded bounds.
|
||||
func TestDifferentialEvolutionBowl(t *testing.T) {
|
||||
f := func(a *core.Array) (float64, error) {
|
||||
s := 0.0
|
||||
for i := range a.Len() {
|
||||
d := a.FloatAt(i) - float64(i+1)
|
||||
s += d * d
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
lower, _ := core.FromFloats([]float64{-10, -10, -10}, 3)
|
||||
upper, _ := core.FromFloats([]float64{10, 10, 10}, 3)
|
||||
p, fv, err := MinimiseDifferentialEvolution(f, lower, upper, DifferentialEvolutionOptions{
|
||||
Generations: 600, Seed: 7,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fv > 1e-10 {
|
||||
t.Fatalf("f = %g, want ~0", fv)
|
||||
}
|
||||
for i := range 3 {
|
||||
if math.Abs(p.FloatAt(i)-float64(i+1)) > 1e-4 {
|
||||
t.Fatalf("point[%d] = %g, want %g", i, p.FloatAt(i), float64(i+1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLinearRowsRedundantEqualityExpelled adds a third equality that is
|
||||
// the sum of the first two, so phase 1 must expel a redundant
|
||||
// artificial through the row-drop path before phase 2 prices.
|
||||
func TestLinearRowsRedundantEqualityExpelled(t *testing.T) {
|
||||
c, _ := core.FromFloats([]float64{1, 1}, 2)
|
||||
cons := LinearConstraints{
|
||||
A: mustMatrix(t, []float64{1, 1, 1, -1, 2, 0}, 3, 2),
|
||||
Lower: []float64{2, 0, 2},
|
||||
Upper: []float64{2, 0, 2},
|
||||
}
|
||||
x, fv, err := MinimiseLinearRows(c, cons, LinearProgramOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if math.Abs(x.FloatAt(0)-1) > 1e-7 || math.Abs(x.FloatAt(1)-1) > 1e-7 {
|
||||
t.Fatalf("point (%g, %g), want (1, 1)", x.FloatAt(0), x.FloatAt(1))
|
||||
}
|
||||
if math.Abs(fv-2) > 1e-7 {
|
||||
t.Fatalf("value %g, want 2", fv)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSimplexRosenbrock walks the derivative-free simplex down the
|
||||
// banana valley to its floor at (1, 1).
|
||||
func TestSimplexRosenbrock(t *testing.T) {
|
||||
f := func(a *core.Array) (float64, error) {
|
||||
x, y := a.FloatAt(0), a.FloatAt(1)
|
||||
return (1-x)*(1-x) + 100*(y-x*x)*(y-x*x), nil
|
||||
}
|
||||
x0, _ := core.FromFloats([]float64{-1.2, 1}, 2)
|
||||
p, fv, err := Minimise(f, x0, MinimiseOptions{MaxIterations: 20000, Tolerance: 1e-12})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fv > 1e-12 {
|
||||
t.Fatalf("f = %g, want ~0", fv)
|
||||
}
|
||||
if math.Abs(p.FloatAt(0)-1) > 1e-4 || math.Abs(p.FloatAt(1)-1) > 1e-4 {
|
||||
t.Fatalf("point (%g, %g), want (1, 1)", p.FloatAt(0), p.FloatAt(1))
|
||||
}
|
||||
}
|
||||
|
||||
// TestRootSystemBroyden solves a mildly nonlinear system with the
|
||||
// maintained inverse and verifies both equations at the answer.
|
||||
func TestRootSystemBroyden(t *testing.T) {
|
||||
f := func(x *core.Array) (*core.Array, error) {
|
||||
a, b := x.FloatAt(0), x.FloatAt(1)
|
||||
return core.FromFloats([]float64{
|
||||
a*a + b*b - 4,
|
||||
math.Exp(a) + b - 3,
|
||||
}, 2)
|
||||
}
|
||||
x0, _ := core.FromFloats([]float64{1, 1}, 2)
|
||||
x, res, err := FindRootSystem(f, x0, RootSystemOptions{Tolerance: 1e-11, UseBroyden: true, MaxIterations: 200})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res > 1e-11 {
|
||||
t.Fatalf("residual %g", res)
|
||||
}
|
||||
a, b := x.FloatAt(0), x.FloatAt(1)
|
||||
if math.Abs(a*a+b*b-4) > 1e-9 || math.Abs(math.Exp(a)+b-3) > 1e-9 {
|
||||
t.Fatalf("solution (%g, %g) does not satisfy the system", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLevenbergMarquardtWeightedCovariance fits a line under per-point
|
||||
// variances and checks the parameters and the reported covariance.
|
||||
func TestLevenbergMarquardtSigmaWeightsAndCovariance(t *testing.T) {
|
||||
xs := []float64{0, 1, 2, 3, 4}
|
||||
ys := []float64{0.5, 2.49, 4.52, 6.48, 8.51}
|
||||
residual := func(p *core.Array) (*core.Array, error) {
|
||||
out := core.New(core.Float, len(xs))
|
||||
v := out.RawFloats()
|
||||
for i, x := range xs {
|
||||
v[i] = p.FloatAt(0)*x + p.FloatAt(1) - ys[i]
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
sigma, _ := core.FromFloats([]float64{1, 1, 4, 1, 4}, 5)
|
||||
p0, _ := core.FromFloats([]float64{0, 0}, 2)
|
||||
res, err := LevenbergMarquardtFit(residual, p0, LMOptions{Sigma: sigma, RequestCovariance: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.Status != FitConverged {
|
||||
t.Fatalf("status %v", res.Status)
|
||||
}
|
||||
if math.Abs(res.Parameters.FloatAt(0)-2) > 0.05 || math.Abs(res.Parameters.FloatAt(1)-0.5) > 0.05 {
|
||||
t.Fatalf("parameters (%g, %g), want ~(2, 0.5)",
|
||||
res.Parameters.FloatAt(0), res.Parameters.FloatAt(1))
|
||||
}
|
||||
if res.Covariance == nil {
|
||||
t.Fatal("covariance missing")
|
||||
}
|
||||
}
|
||||
|
||||
// TestNonlinearConstrainedMixedRows minimises a bowl over an equality
|
||||
// and an inequality at once, with the inequality active at the answer:
|
||||
// min (x-2)^2 + (y-2)^2 over x + y = 1 and x <= 1/2 sits at
|
||||
// (1/2, 1/2) with f = 4.5 and a non-negative inequality multiplier.
|
||||
func TestNonlinearConstrainedMixedRows(t *testing.T) {
|
||||
f := func(a *core.Array) (float64, error) {
|
||||
x, y := a.FloatAt(0), a.FloatAt(1)
|
||||
return (x-2)*(x-2) + (y-2)*(y-2), nil
|
||||
}
|
||||
grad := func(a *core.Array) (*core.Array, error) {
|
||||
x, y := a.FloatAt(0), a.FloatAt(1)
|
||||
return core.FromFloats([]float64{2 * (x - 2), 2 * (y - 2)}, 2)
|
||||
}
|
||||
cons := NonlinearConstraints{
|
||||
Equalities: []func(*core.Array) (float64, error){func(a *core.Array) (float64, error) {
|
||||
return a.FloatAt(0) + a.FloatAt(1) - 1, nil
|
||||
}},
|
||||
Inequalities: []func(*core.Array) (float64, error){func(a *core.Array) (float64, error) {
|
||||
return a.FloatAt(0) - 0.5, nil
|
||||
}},
|
||||
}
|
||||
x0, _ := core.FromFloats([]float64{0, 0}, 2)
|
||||
p, fv, mult, err := MinimiseNonlinearConstrained(f, grad, x0, cons, LBFGSOptions{Tolerance: 1e-10})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if math.Abs(p.FloatAt(0)-0.5) > 1e-4 || math.Abs(p.FloatAt(1)-0.5) > 1e-4 {
|
||||
t.Fatalf("point (%g, %g), want (0.5, 0.5)", p.FloatAt(0), p.FloatAt(1))
|
||||
}
|
||||
if fv < 4.49 || fv > 4.51 {
|
||||
t.Fatalf("f = %g, want 4.5", fv)
|
||||
}
|
||||
if len(mult) != 2 || mult[1] < 0 {
|
||||
t.Fatalf("multipliers %v", mult)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSimulatedAnnealingTwoWell puts a deep left well against a
|
||||
// shallower right one, starting in the right: the Metropolis walk must
|
||||
// cross the barrier and report the global basin.
|
||||
func TestSimulatedAnnealingTwoWell(t *testing.T) {
|
||||
f := func(a *core.Array) (float64, error) {
|
||||
x := a.FloatAt(0)
|
||||
w1 := (x + 2) * (x + 2)
|
||||
w2 := (x-3)*(x-3) + 0.5
|
||||
return math.Min(w1, w2), nil
|
||||
}
|
||||
x0, _ := core.FromFloats([]float64{3}, 1)
|
||||
p, fv, err := MinimiseSimulatedAnnealing(f, x0, SimulatedAnnealingOptions{
|
||||
Steps: 40000, Seed: 5, StepScale: 0.5, AllowBudgetExit: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fv > 0.1 {
|
||||
t.Fatalf("f = %g, want the left well (~0)", fv)
|
||||
}
|
||||
if p.FloatAt(0) > 0 {
|
||||
t.Fatalf("point %g, want the left well", p.FloatAt(0))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user