feat: initial release
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -0,0 +1,450 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user