// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: MIT package optim import ( "math" "strings" "testing" "sourcedock.dev/petrbalvin/tensor/internal/core" ) // TestLUFactorSolve pins the shared factorisation machinery on a // non-symmetric matrix: both triangle directions are load-bearing, // solve for the primal ratios and solveT for the dual prices and the // redundant-row scan. func TestLUFactorSolve(t *testing.T) { // A = [[2, 1], [4, 3]], det = 2: A(1, 3) = (5, 13) and // Aᵀ(2, 1) = (8, 5). f, err := factorLU([]float64{2, 1, 4, 3}, 2) if err != nil { t.Fatalf("factorLU: %v", err) } x := make([]float64, 2) f.solve([]float64{5, 13}, x) if math.Abs(x[0]-1) > 1e-12 || math.Abs(x[1]-3) > 1e-12 { t.Fatalf("solve = (%g, %g), want (1, 3)", x[0], x[1]) } f.solveT([]float64{8, 5}, x) if math.Abs(x[0]-2) > 1e-12 || math.Abs(x[1]-1) > 1e-12 { t.Fatalf("solveT = (%g, %g), want (2, 1)", x[0], x[1]) } // A singular matrix is refused, not divided through. if _, err := factorLU([]float64{1, 2, 2, 4}, 2); err == nil { t.Fatal("a singular matrix was factored") } // So is a zero matrix, named as such rather than a pivot. if _, err := factorLU(make([]float64, 4), 2); err == nil { t.Fatal("a zero matrix was factored") } if _, err := factorLU(nil, 0); err != nil { t.Fatalf("a zero-sized factorisation was refused: %v", err) } } // TestMinimiseLinearStandardVertex pins a hand-built optimum at a // known vertex: min −(2x + 3y) over x + y ≤ 4, 2x + y ≤ 6 with the // slacks carried explicitly. The vertices price out at 0, 6, 10 and // 12, so the answer is the vertex (0, 4, 0, 2) with value −12. func TestMinimiseLinearStandardVertex(t *testing.T) { c := mustFloats(t, []float64{-2, -3, 0, 0}) a := mustFloats(t, []float64{1, 1, 1, 0, 2, 1, 0, 1}, 2, 4) b := mustFloats(t, []float64{4, 6}) x, value, err := MinimiseLinear(c, a, b, LinearProgramOptions{}) if err != nil { t.Fatalf("MinimiseLinear: %v", err) } want := []float64{0, 4, 0, 2} for i, w := range want { if math.Abs(x.FloatAt(i)-w) > 1e-9 { t.Fatalf("x[%d] = %.12g, want %g", i, x.FloatAt(i), w) } } if math.Abs(value+12) > 1e-9 { t.Fatalf("value = %.12g, want −12", value) } } // TestMinimiseLinearRowsVertex drives the two-sided wrapper over the // same polytope expressed as house rows with free variables: max // x + 2y on x + y ≤ 4, x + 3y ≤ 6, x, y ≥ 0. The vertex prices are 0, // 4, 5 and 4, so the optimum is the vertex (3, 1) with value 5. func TestMinimiseLinearRowsVertex(t *testing.T) { c := mustFloats(t, []float64{-1, -2}) cons := LinearConstraints{ A: mustFloats(t, []float64{1, 1, 1, 3, 1, 0, 0, 1}, 4, 2), Lower: []float64{math.Inf(-1), math.Inf(-1), 0, 0}, Upper: []float64{4, 6, math.Inf(1), math.Inf(1)}, } x, value, err := MinimiseLinearRows(c, cons, LinearProgramOptions{}) if err != nil { t.Fatalf("MinimiseLinearRows: %v", err) } if math.Abs(x.FloatAt(0)-3) > 1e-9 || math.Abs(x.FloatAt(1)-1) > 1e-9 { t.Fatalf("x = (%.12g, %.12g), want the vertex (3, 1)", x.FloatAt(0), x.FloatAt(1)) } if math.Abs(value+5) > 1e-9 { t.Fatalf("value = %.12g, want −5", value) } } // TestMinimiseLinearNegativeRightHandSide pins the row negation: a // standard-form row arrives with b < 0 and must come out of the // artificial phase feasible all the same. func TestMinimiseLinearNegativeRightHandSide(t *testing.T) { c := mustFloats(t, []float64{1, 1}) a := mustFloats(t, []float64{-1, -1}, 1, 2) b := mustFloats(t, []float64{-2}) x, value, err := MinimiseLinear(c, a, b, LinearProgramOptions{}) if err != nil { t.Fatalf("MinimiseLinear: %v", err) } if math.Abs(value-2) > 1e-9 { t.Fatalf("value = %.12g, want 2", value) } if math.Abs(x.FloatAt(0)+x.FloatAt(1)-2) > 1e-9 { t.Fatalf("x + y = %.12g, want 2", x.FloatAt(0)+x.FloatAt(1)) } } // TestMinimiseLinearBudget pins the pivot-budget refusal: one pivot // cannot carry the vertex problem to its optimum, and an exhausted // budget is an error, never a silent answer. func TestMinimiseLinearBudget(t *testing.T) { c := mustFloats(t, []float64{-2, -3, 0, 0}) a := mustFloats(t, []float64{1, 1, 1, 0, 2, 1, 0, 1}, 2, 4) b := mustFloats(t, []float64{4, 6}) _, _, err := MinimiseLinear(c, a, b, LinearProgramOptions{MaxIterations: 1}) if err == nil || !strings.Contains(err.Error(), "budget") { t.Fatalf("error = %v, want the pivot-budget refusal", err) } } // TestMinimiseLinearDegenerateTerminates pins the anti-cycling // guarantee where it earns its keep: two identical equality rows plus // a third at twice the scale leave the problem degenerate and // redundant at once, the classic rules can pivot forever on such a // vertex, and Bland's rule must terminate, dropping the redundant rows // and their artificials, with a point on the feasible segment. func TestMinimiseLinearDegenerateTerminates(t *testing.T) { c := mustFloats(t, []float64{-1, -1}) cons := LinearConstraints{ A: mustFloats(t, []float64{1, 1, 1, 1, 2, 2}, 3, 2), Lower: []float64{1, 1, 2}, Upper: []float64{1, 1, 2}, } x, value, err := MinimiseLinearRows(c, cons, LinearProgramOptions{}) if err != nil { t.Fatalf("MinimiseLinearRows on a degenerate problem: %v", err) } if math.Abs(value+1) > 1e-9 { t.Fatalf("value = %.12g, want −1", value) } if x.FloatAt(0) < -1e-9 || x.FloatAt(1) < -1e-9 { t.Fatalf("x = (%g, %g) left the non-negative quadrant", x.FloatAt(0), x.FloatAt(1)) } if math.Abs(x.FloatAt(0)+x.FloatAt(1)-1) > 1e-9 { t.Fatalf("x + y = %.12g, want 1", x.FloatAt(0)+x.FloatAt(1)) } } // TestMinimiseLinearDegenerateOrigin pins the other degenerate shape: // a zero right-hand side keeps the artificials basic at the phase-1 // optimum, so the phase transition must pivot them out with degenerate // steps before phase 2. The feasible set of x + y = 0, x − y = 0 over // x, y ≥ 0 is the origin alone. func TestMinimiseLinearDegenerateOrigin(t *testing.T) { c := mustFloats(t, []float64{-1, -1}) a := mustFloats(t, []float64{1, 1, 1, -1}, 2, 2) b := mustFloats(t, []float64{0, 0}) x, value, err := MinimiseLinear(c, a, b, LinearProgramOptions{}) if err != nil { t.Fatalf("MinimiseLinear: %v", err) } if value != 0 { t.Fatalf("value = %g, want 0", value) } for i := range 2 { if x.FloatAt(i) != 0 { t.Fatalf("x[%d] = %g, want 0", i, x.FloatAt(i)) } } } // TestMinimiseLinearBeale pins Beale's cycling example, the classic // demonstration that Dantzig's rule can pivot forever (Beale, 1955; // the form in Chvátal's Linear Programming, chapter 3). The optimum is // 0.05 at (1/25, 0, 1, 0) with the first slack carrying the 0.03 the // first row leaves loose, so minimising the negated objective returns // −0.05 there. func TestMinimiseLinearBeale(t *testing.T) { c := mustFloats(t, []float64{-0.75, 150, -0.02, 6, 0, 0, 0}) a := mustFloats(t, []float64{ 0.25, -60, -0.04, 9, 1, 0, 0, 0.5, -90, -0.02, 3, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, }, 3, 7) b := mustFloats(t, []float64{0, 0, 1}) x, value, err := MinimiseLinear(c, a, b, LinearProgramOptions{}) if err != nil { t.Fatalf("MinimiseLinear on Beale's example: %v", err) } want := []float64{1.0 / 25.0, 0, 1, 0, 0.03, 0, 0} for i, w := range want { if math.Abs(x.FloatAt(i)-w) > 1e-9 { t.Fatalf("x[%d] = %.12g, want %.12g", i, x.FloatAt(i), w) } } if math.Abs(value+0.05) > 1e-9 { t.Fatalf("value = %.12g, want −0.05", value) } } // TestMinimiseLinearInfeasible pins the phase-1 refusal: x ≥ 1 and // x ≤ 0 share no feasible point, and the error must carry the // infeasibility the artificial phase ended with. func TestMinimiseLinearInfeasible(t *testing.T) { c := mustFloats(t, []float64{1}) cons := LinearConstraints{ A: mustFloats(t, []float64{1, 1}, 2, 1), Lower: []float64{math.Inf(-1), 1}, Upper: []float64{0, math.Inf(1)}, } _, _, err := MinimiseLinearRows(c, cons, LinearProgramOptions{}) if err == nil { t.Fatal("an infeasible problem returned a solution") } if !strings.Contains(err.Error(), "infeasible") { t.Fatalf("error = %v, want the phase-1 infeasibility evidence", err) } // The named row must be the one that carries the worst figure, not // whichever artificial the scan touched after row 1: two empty rows // leave their artificials at 1 and 0.5, and the zero-initialised // worst-row sentinel once let the smaller overwrite the evidence. _, _, err = MinimiseLinear(mustFloats(t, []float64{0}), mustFloats(t, []float64{0, 0}, 2, 1), mustFloats(t, []float64{1, 0.5}, 2), LinearProgramOptions{}) if err == nil || !strings.Contains(err.Error(), "row 1 still carries 1") { t.Fatalf("worst row misreported: %v", err) } } // TestMinimiseLinearUnbounded pins the ray refusal in both entries. func TestMinimiseLinearUnbounded(t *testing.T) { // Standard form: x = y with min −x runs along the ray (t, t). _, _, err := MinimiseLinear(mustFloats(t, []float64{-1, 0}), mustFloats(t, []float64{1, -1}, 1, 2), mustFloats(t, []float64{0}), LinearProgramOptions{}) if err == nil || !strings.Contains(err.Error(), "unbounded") { t.Fatalf("standard form: error = %v, want an unbounded refusal", err) } // Rows: x ≥ 0 with min −x. _, _, err = MinimiseLinearRows(mustFloats(t, []float64{-1}), LinearConstraints{A: mustFloats(t, []float64{1}, 1, 1), Lower: []float64{0}, Upper: []float64{math.Inf(1)}}, LinearProgramOptions{}) if err == nil || !strings.Contains(err.Error(), "unbounded") { t.Fatalf("rows: error = %v, want an unbounded refusal", err) } } // TestMinimiseLinearNoRows pins the row-free standard form: over x ≥ 0 // alone a non-negative cost bottoms out at the origin and a negative // cost is unbounded. func TestMinimiseLinearNoRows(t *testing.T) { x, value, err := MinimiseLinear(mustFloats(t, []float64{1, 2}), core.New(core.Float, 0, 2), core.New(core.Float, 0), LinearProgramOptions{}) if err != nil { t.Fatalf("MinimiseLinear: %v", err) } if value != 0 { t.Fatalf("value = %g, want 0", value) } for i := range 2 { if x.FloatAt(i) != 0 { t.Fatalf("x[%d] = %g, want 0", i, x.FloatAt(i)) } } if _, _, err := MinimiseLinear(mustFloats(t, []float64{-1, 2}), core.New(core.Float, 0, 2), core.New(core.Float, 0), LinearProgramOptions{}); err == nil { t.Fatal("a negative cost without rows was not refused as unbounded") } } // TestMinimiseLinearRefusals checks every input gate of both entries. func TestMinimiseLinearRefusals(t *testing.T) { empty := core.New(core.Float, 0) c2 := mustFloats(t, []float64{1, 2}) a22 := mustFloats(t, []float64{1, 1, 0, 1}, 2, 2) b2 := mustFloats(t, []float64{1, 1}) cases := []struct { name string run func() error }{ {"empty cost", func() error { _, _, err := MinimiseLinear(empty, a22, b2, LinearProgramOptions{}) return err }}, {"complex cost", func() error { _, _, err := MinimiseLinear(mustComplexPoint(t), a22, b2, LinearProgramOptions{}) return err }}, {"nil matrix", func() error { _, _, err := MinimiseLinear(c2, nil, b2, LinearProgramOptions{}) return err }}, {"matrix shape", func() error { _, _, err := MinimiseLinear(c2, mustFloats(t, []float64{1, 1}), b2, LinearProgramOptions{}) return err }}, {"right-hand side length", func() error { _, _, err := MinimiseLinear(c2, a22, mustFloats(t, []float64{1}), LinearProgramOptions{}) return err }}, {"NaN coefficient", func() error { _, _, err := MinimiseLinear(c2, mustFloats(t, []float64{math.NaN(), 1, 0, 1}, 2, 2), b2, LinearProgramOptions{}) return err }}, {"NaN cost", func() error { _, _, err := MinimiseLinear(mustFloats(t, []float64{math.NaN(), 1}), a22, b2, LinearProgramOptions{}) return err }}, {"complex constraint matrix", func() error { ca, _ := core.FromComplexes([]complex128{1, 0, 0, 1}, 2, 2) _, _, err := MinimiseLinear(c2, ca, b2, LinearProgramOptions{}) return err }}, {"complex right-hand side", func() error { cb, _ := core.FromComplexes([]complex128{1, 1}, 2) _, _, err := MinimiseLinear(c2, a22, cb, LinearProgramOptions{}) return err }}, {"NaN right-hand side", func() error { _, _, err := MinimiseLinear(c2, a22, mustFloats(t, []float64{math.NaN(), 1}), LinearProgramOptions{}) return err }}, } for _, c := range cases { if err := c.run(); err == nil { t.Fatalf("MinimiseLinear: %s accepted", c.name) } } rows := []struct { name string cons LinearConstraints }{ {"nil matrix", LinearConstraints{Lower: []float64{0}, Upper: []float64{1}}}, {"matrix shape", LinearConstraints{A: mustFloats(t, []float64{1, 1}), Lower: []float64{0}, Upper: []float64{1}}}, {"no rows", LinearConstraints{A: core.New(core.Float, 0, 2), Lower: nil, Upper: nil}}, {"short bounds", LinearConstraints{A: a22, Lower: []float64{0}, Upper: []float64{}}}, {"crossed bounds", LinearConstraints{A: a22, Lower: []float64{1, 0}, Upper: []float64{0, 1}}}, {"equality at infinity", LinearConstraints{A: a22, Lower: []float64{math.Inf(1), 0}, Upper: []float64{math.Inf(1), 1}}}, {"NaN bound", LinearConstraints{A: a22, Lower: []float64{math.NaN(), 0}, Upper: []float64{1, 1}}}, {"NaN coefficient", LinearConstraints{A: mustFloats(t, []float64{math.NaN(), 1, 0, 1}, 2, 2), Lower: []float64{0, 0}, Upper: []float64{1, 1}}}, } for _, r := range rows { if _, _, err := MinimiseLinearRows(c2, r.cons, LinearProgramOptions{}); err == nil { t.Fatalf("MinimiseLinearRows: %s accepted", r.name) } } // The rows entry's own cost gates. rowCosts := []struct { name string c *core.Array }{ {"empty cost", core.New(core.Float, 0)}, {"complex cost", mustComplexPoint(t)}, {"NaN cost", mustFloats(t, []float64{math.NaN(), 1})}, } for _, rc := range rowCosts { if _, _, err := MinimiseLinearRows(rc.c, LinearConstraints{A: a22, Lower: []float64{0, 0}, Upper: []float64{1, 1}}, LinearProgramOptions{}); err == nil { t.Fatalf("MinimiseLinearRows: %s accepted", rc.name) } } // Complex constraint matrices are refused here too. ca, _ := core.FromComplexes([]complex128{1, 0, 0, 1}, 2, 2) if _, _, err := MinimiseLinearRows(c2, LinearConstraints{A: ca, Lower: []float64{0, 0}, Upper: []float64{1, 1}}, LinearProgramOptions{}); err == nil { t.Fatal("MinimiseLinearRows: a complex constraint matrix was accepted") } } // TestMinimiseLinearRowsNegativeBounds pins the negation of a // right-hand side the artificial basis needs: a two-sided model whose // bound rows come out negative (a lower bound below zero, a bare // negative equality) builds standard rows with b >= 0 and solves, // where an unnegated row lost primal feasibility at pivot 0. func TestMinimiseLinearRowsNegativeBounds(t *testing.T) { a, err := core.FromFloats([]float64{1, 0, 0, 1}, 2, 2) if err != nil { t.Fatal(err) } cons := LinearConstraints{ A: a, Lower: []float64{-1, 2}, Upper: []float64{math.Inf(1), math.Inf(1)}, } x, v, err := MinimiseLinearRows(mustFloats(t, []float64{1, 1}), cons, LinearProgramOptions{}) if err != nil { t.Fatalf("negative lower bound refused: %v", err) } if v != 1 || x.FloatAt(0) != -1 || x.FloatAt(1) != 2 { t.Fatalf("x = (%g, %g) value %g, want (-1, 2) at 1", x.FloatAt(0), x.FloatAt(1), v) } // The bare negative equality rides the same negation through the // QP entry's feasibility path. h, herr := core.FromFloats([]float64{2, 0, 0, 2}, 2, 2) if herr != nil { t.Fatal(herr) } eq, eerr := core.FromFloats([]float64{1, 1}, 1, 2) if eerr != nil { t.Fatal(eerr) } eqCons := LinearConstraints{A: eq, Lower: []float64{-1}, Upper: []float64{-1}} q, qv, _, qerr := MinimiseQP(h, mustFloats(t, []float64{4, 0}), eqCons, nil, QPOptions{}) if qerr != nil { t.Fatalf("negative equality refused: %v", qerr) } if math.Abs(q.FloatAt(0)-(-1.5)) > 1e-9 || math.Abs(q.FloatAt(1)-0.5) > 1e-9 || math.Abs(qv+3.5) > 1e-9 { t.Fatalf("x = (%g, %g) value %g, want (-1.5, 0.5) at -3.5", q.FloatAt(0), q.FloatAt(1), qv) } } // TestMinimiseLinearExpelsTwoArtificials pins the reused unit vector in // the artificial expulsion. The two rows below are exact negatives, so // the phase-1 reduced costs cancel for every column: the artificial // phase ends with both artificials still basic at zero and the // expulsion runs twice, the second round reading a freshly cleared unit // vector. A stale one scores the columns through the sum of two rows of // B⁻¹ and swaps in a column that leaves the basis singular, so a solve // that must answer the origin fails instead. func TestMinimiseLinearExpelsTwoArtificials(t *testing.T) { a := mustFloats(t, []float64{1, 2, -1, -2}, 2, 2) b := mustFloats(t, []float64{0, 0}) c := mustFloats(t, []float64{1, 1}) x, value, err := MinimiseLinear(c, a, b, LinearProgramOptions{}) if err != nil { t.Fatalf("MinimiseLinear: %v", err) } if value != 0 { t.Fatalf("value = %.12g, want 0", value) } for i := range x.Len() { if x.FloatAt(i) != 0 { t.Fatalf("x[%d] = %.12g, want 0", i, x.FloatAt(i)) } } // The same two-round expulsion over a scaled right-hand side: the // origin is the only feasible point whatever the rows' scale. scaled := mustFloats(t, []float64{3, 6, -3, -6}, 2, 2) x, value, err = MinimiseLinear(c, scaled, b, LinearProgramOptions{}) if err != nil { t.Fatalf("MinimiseLinear over the scaled rows: %v", err) } if value != 0 || x.FloatAt(0) != 0 || x.FloatAt(1) != 0 { t.Fatalf("scaled rows: x = (%g, %g), value %g, want the origin", x.FloatAt(0), x.FloatAt(1), value) } }