815 lines
26 KiB
Go
815 lines
26 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
|
|
// SPDX-License-Identifier: MIT
|
|||
|
|
|
|||
|
|
package optim
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"math"
|
|||
|
|
|
|||
|
|
"sourcedock.dev/petrbalvin/tensor/internal/base"
|
|||
|
|
"sourcedock.dev/petrbalvin/tensor/internal/core"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// Linear programming by the revised simplex method on the standard
|
|||
|
|
// form
|
|||
|
|
//
|
|||
|
|
// min c·x subject to A·x = b, x ≥ 0.
|
|||
|
|
//
|
|||
|
|
// Free or two-sided quantities belong to the caller's own conversion:
|
|||
|
|
// the wrapper MinimiseLinearRows turns the house rows l ≤ A·x ≤ u into
|
|||
|
|
// this form mechanically (a free variable splits into the difference
|
|||
|
|
// of two non-negative ones, each finite row side gains a slack), so a
|
|||
|
|
// caller with ordinary bounds never touches the standard form at all.
|
|||
|
|
//
|
|||
|
|
// The method is the two-phase revised simplex. Phase 1 minimises the
|
|||
|
|
// sum of the artificial variables that carry the starting basis, so
|
|||
|
|
// its optimum is either zero, which leaves a feasible basis in hand,
|
|||
|
|
// or the total infeasibility of the rows, which refuses the problem
|
|||
|
|
// with that figure as the evidence. Phase 2 prices the real columns
|
|||
|
|
// from the feasible basis and walks along vertices to the optimum.
|
|||
|
|
//
|
|||
|
|
// Both phases pick the entering column by Bland's rule: the
|
|||
|
|
// lowest-indexed column whose reduced cost is negative, and, among the
|
|||
|
|
// rows tied at the minimum ratio, the lowest-indexed basic variable to
|
|||
|
|
// leave. The rule is slower than Dantzig's most-negative pricing but
|
|||
|
|
// it cannot cycle: on a degenerate problem, where several bases carry
|
|||
|
|
// the same vertex and the classic rule can pivot forever, Bland's rule
|
|||
|
|
// is guaranteed to terminate (Bland, 1977). Redundant rows surface in
|
|||
|
|
// phase 1 as artificial columns that will not leave: a row no real
|
|||
|
|
// column can pivot out is a linear combination of the others, so the
|
|||
|
|
// row and its artificial leave the problem together and the reduced
|
|||
|
|
// basis stays valid.
|
|||
|
|
//
|
|||
|
|
// The basis is refactorised by a dense LU with partial pivoting at
|
|||
|
|
// every pivot. The solver targets the small dense problems a library
|
|||
|
|
// of this shape meets, where O(m³) per pivot is cheap and a fresh
|
|||
|
|
// factorisation keeps the iteration honest where an updated inverse
|
|||
|
|
// would drift. The same factorisation machinery carries the
|
|||
|
|
// active-set solver in qp.go.
|
|||
|
|
|
|||
|
|
// LinearProgramOptions tunes MinimiseLinear and MinimiseLinearRows.
|
|||
|
|
// MaxIterations ≤ 0 means 10000 pivots, Tolerance ≤ 0 means 1e-9. The
|
|||
|
|
// tolerance prices reduced costs and separates ratio-test ties, and it
|
|||
|
|
// is absolute in the scale the caller's costs and rows carry, so a
|
|||
|
|
// badly scaled problem should be rescaled to O(1) first, as with the
|
|||
|
|
// other tolerances in the package.
|
|||
|
|
type LinearProgramOptions struct {
|
|||
|
|
MaxIterations int
|
|||
|
|
Tolerance float64
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MinimiseLinear returns the point and value of the minimum of c·x
|
|||
|
|
// over the standard-form polytope A·x = b with x ≥ 0. The contract is
|
|||
|
|
// the standard form exactly: every variable is non-negative, every row
|
|||
|
|
// is an equality, and a caller holding inequalities, free variables or
|
|||
|
|
// bounds converts them first (MinimiseLinearRows does that conversion
|
|||
|
|
// for the house two-sided rows). The returned point has one entry per
|
|||
|
|
// column of A, slack columns included when the caller built them into
|
|||
|
|
// the standard form.
|
|||
|
|
//
|
|||
|
|
// An infeasible problem is refused with the phase-1 evidence: the
|
|||
|
|
// total infeasibility the artificial phase ended with and the row that
|
|||
|
|
// carries the worst of it. An unbounded objective is refused with the
|
|||
|
|
// column that prices out as a profitable ray no row limits. A run that
|
|||
|
|
// spends the pivot budget without pricing out is an error, never a
|
|||
|
|
// silent answer: under Bland's rule an exhausted budget on a
|
|||
|
|
// well-scaled problem is the signature of a tolerance the data does
|
|||
|
|
// not support. A problem with no rows is the simplex over x ≥ 0: it
|
|||
|
|
// returns the origin when every cost is non-negative and refuses as
|
|||
|
|
// unbounded when one is not.
|
|||
|
|
func MinimiseLinear(c, a, b *core.Array, opts LinearProgramOptions) (*core.Array, float64, error) {
|
|||
|
|
const name = "MinimiseLinear"
|
|||
|
|
if c.NDim() != 1 || c.Len() == 0 {
|
|||
|
|
return nil, 0, base.Errf("%s: c must be a non-empty rank-1 cost vector", name)
|
|||
|
|
}
|
|||
|
|
if c.Dtype() == core.Complex {
|
|||
|
|
return nil, 0, base.Errf("%s: complex costs are not supported", name)
|
|||
|
|
}
|
|||
|
|
n := c.Len()
|
|||
|
|
if a == nil {
|
|||
|
|
return nil, 0, base.Errf("%s: the constraint matrix is nil", name)
|
|||
|
|
}
|
|||
|
|
if err := requireReal(name, "constraint matrices", a); err != nil {
|
|||
|
|
return nil, 0, err
|
|||
|
|
}
|
|||
|
|
if a.NDim() != 2 || a.Shape()[1] != n {
|
|||
|
|
return nil, 0, base.Errf("%s: the constraint matrix is %s, want m×%d", name, base.ShapeText(a.Shape()), n)
|
|||
|
|
}
|
|||
|
|
m := a.Shape()[0]
|
|||
|
|
if b.NDim() != 1 || b.Len() != m {
|
|||
|
|
return nil, 0, base.Errf("%s: b must be a rank-1 vector with one entry per row (%d)", name, m)
|
|||
|
|
}
|
|||
|
|
if b.Dtype() == core.Complex {
|
|||
|
|
return nil, 0, base.Errf("%s: complex right-hand sides are not supported", name)
|
|||
|
|
}
|
|||
|
|
cost := make([]float64, n)
|
|||
|
|
for j := range n {
|
|||
|
|
v := c.FloatAt(j)
|
|||
|
|
if math.IsNaN(v) || math.IsInf(v, 0) {
|
|||
|
|
return nil, 0, base.Errf("%s: the cost carries a non-finite entry at %d", name, j+1)
|
|||
|
|
}
|
|||
|
|
cost[j] = v
|
|||
|
|
}
|
|||
|
|
// One backing block for every standard row: a row is built once
|
|||
|
|
// here, extended in place by graftArtificials and never outgrows
|
|||
|
|
// its slot, so one allocation carries the whole block.
|
|||
|
|
rows := make([][]float64, m)
|
|||
|
|
back := make([]float64, m*(n+m))
|
|||
|
|
rhs := make([]float64, m)
|
|||
|
|
for i := range m {
|
|||
|
|
// Rows carry their artificial column from the start: the tail
|
|||
|
|
// stays zero until graftArtificials writes the unit entry, so
|
|||
|
|
// the phases read the same values a freshly extended row held.
|
|||
|
|
row := back[i*(n+m) : (i+1)*(n+m)]
|
|||
|
|
for j := range n {
|
|||
|
|
v := a.FloatAt(i*n + j)
|
|||
|
|
if math.IsNaN(v) || math.IsInf(v, 0) {
|
|||
|
|
return nil, 0, base.Errf("%s: row %d carries a non-finite coefficient", name, i+1)
|
|||
|
|
}
|
|||
|
|
row[j] = v
|
|||
|
|
}
|
|||
|
|
v := b.FloatAt(i)
|
|||
|
|
if math.IsNaN(v) || math.IsInf(v, 0) {
|
|||
|
|
return nil, 0, base.Errf("%s: the right-hand side carries a non-finite entry at %d", name, i+1)
|
|||
|
|
}
|
|||
|
|
// The artificial basis needs b ≥ 0, so a negative row is
|
|||
|
|
// negated whole: the feasible set is unchanged.
|
|||
|
|
if v < 0 {
|
|||
|
|
for j := range n {
|
|||
|
|
row[j] = -row[j]
|
|||
|
|
}
|
|||
|
|
v = -v
|
|||
|
|
}
|
|||
|
|
rows[i], rhs[i] = row, v
|
|||
|
|
}
|
|||
|
|
prob := &standardForm{rows: rows, b: rhs, nreal: n}
|
|||
|
|
x, value, err := solveTwoPhase(prob, cost, opts, name)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, 0, err
|
|||
|
|
}
|
|||
|
|
out, fv := packResult(x, value)
|
|||
|
|
return out, fv, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MinimiseLinearRows returns the point and value of the minimum of c·x
|
|||
|
|
// subject to the two-sided rows l ≤ A·x ≤ u carried by cons, the same
|
|||
|
|
// rows LinearConstraints holds for MinimiseConstrained. The variables
|
|||
|
|
// are free: a bound on a variable is just a row with a unit
|
|||
|
|
// coefficient, as the linear-constraint tests build them. A row with
|
|||
|
|
// Lower = Upper is an equality; an infinite bound opens that side; a
|
|||
|
|
// row open at both ends constrains nothing and is dropped from the
|
|||
|
|
// standard form.
|
|||
|
|
//
|
|||
|
|
// The conversion is mechanical and exact: each variable x splits into
|
|||
|
|
// the difference of two non-negative columns, each finite upper side
|
|||
|
|
// gains a slack column added to the row, each finite lower side a
|
|||
|
|
// slack subtracted, and an equality row passes through bare. The two
|
|||
|
|
// entries a variable splits into cancel in the objective, so the
|
|||
|
|
// standard-form optimum back-substitutes to the original variables and
|
|||
|
|
// the reported value is c·x computed on them.
|
|||
|
|
//
|
|||
|
|
// Infeasibility, unboundedness and budget exhaustion are refused
|
|||
|
|
// exactly as MinimiseLinear refuses them.
|
|||
|
|
func MinimiseLinearRows(c *core.Array, cons LinearConstraints, opts LinearProgramOptions) (*core.Array, float64, error) {
|
|||
|
|
const name = "MinimiseLinearRows"
|
|||
|
|
if c.NDim() != 1 || c.Len() == 0 {
|
|||
|
|
return nil, 0, base.Errf("%s: c must be a non-empty rank-1 cost vector", name)
|
|||
|
|
}
|
|||
|
|
if c.Dtype() == core.Complex {
|
|||
|
|
return nil, 0, base.Errf("%s: complex costs are not supported", name)
|
|||
|
|
}
|
|||
|
|
n := c.Len()
|
|||
|
|
if cons.A == nil {
|
|||
|
|
return nil, 0, base.Errf("%s: the constraint matrix is nil", name)
|
|||
|
|
}
|
|||
|
|
if err := requireReal(name, "constraint matrices", cons.A); err != nil {
|
|||
|
|
return nil, 0, err
|
|||
|
|
}
|
|||
|
|
if cons.A.NDim() != 2 || cons.A.Shape()[1] != n {
|
|||
|
|
return nil, 0, base.Errf("%s: the constraint matrix is %s, want r×%d", name, base.ShapeText(cons.A.Shape()), n)
|
|||
|
|
}
|
|||
|
|
r := cons.A.Shape()[0]
|
|||
|
|
if r == 0 {
|
|||
|
|
return nil, 0, base.Errf("%s: the constraint matrix has no rows", name)
|
|||
|
|
}
|
|||
|
|
if len(cons.Lower) != r || len(cons.Upper) != r {
|
|||
|
|
return nil, 0, base.Errf("%s: the bounds hold %d and %d entries for %d rows",
|
|||
|
|
name, len(cons.Lower), len(cons.Upper), r)
|
|||
|
|
}
|
|||
|
|
cost := make([]float64, n)
|
|||
|
|
for j := range n {
|
|||
|
|
v := c.FloatAt(j)
|
|||
|
|
if math.IsNaN(v) || math.IsInf(v, 0) {
|
|||
|
|
return nil, 0, base.Errf("%s: the cost carries a non-finite entry at %d", name, j+1)
|
|||
|
|
}
|
|||
|
|
cost[j] = v
|
|||
|
|
}
|
|||
|
|
// The standard form: n split pairs, then one slack per finite
|
|||
|
|
// non-equality side. Count the slacks and the materialised rows
|
|||
|
|
// first so every row slice is allocated once, wide enough for its
|
|||
|
|
// artificial column.
|
|||
|
|
slacks := 0
|
|||
|
|
built := 0
|
|||
|
|
for i := range r {
|
|||
|
|
lo, up := cons.Lower[i], cons.Upper[i]
|
|||
|
|
if math.IsNaN(lo) || math.IsNaN(up) || lo > up {
|
|||
|
|
return nil, 0, base.Errf("%s: row %d has bounds [%g, %g]", name, i+1, lo, up)
|
|||
|
|
}
|
|||
|
|
if lo == up && math.IsInf(lo, 0) {
|
|||
|
|
return nil, 0, base.Errf("%s: row %d is an equality at infinity", name, i+1)
|
|||
|
|
}
|
|||
|
|
if lo == up {
|
|||
|
|
built++
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
if up < math.Inf(1) {
|
|||
|
|
slacks++
|
|||
|
|
built++
|
|||
|
|
}
|
|||
|
|
if lo > math.Inf(-1) {
|
|||
|
|
slacks++
|
|||
|
|
built++
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
for i := range r {
|
|||
|
|
for j := range n {
|
|||
|
|
if v := cons.A.FloatAt(i*n + j); math.IsNaN(v) || math.IsInf(v, 0) {
|
|||
|
|
return nil, 0, base.Errf("%s: row %d carries a non-finite coefficient", name, i+1)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
cols := 2*n + slacks
|
|||
|
|
prob := &standardForm{nreal: cols}
|
|||
|
|
rows := make([][]float64, 0, r)
|
|||
|
|
// One backing block for every built row: a row is written once
|
|||
|
|
// here, extended in place by graftArtificials and never outgrows
|
|||
|
|
// its slot, so one allocation carries the whole block.
|
|||
|
|
back := make([]float64, built*(cols+built))
|
|||
|
|
rhs := make([]float64, 0, r)
|
|||
|
|
slackCol := 2 * n
|
|||
|
|
for i := range r {
|
|||
|
|
lo, up := cons.Lower[i], cons.Upper[i]
|
|||
|
|
// build materialises one standard row for one finite side. The
|
|||
|
|
// slack argument is +1 on an upper side, -1 on a lower one and
|
|||
|
|
// 0 on a bare equality. A negative right-hand side is negated
|
|||
|
|
// whole, coefficients, slack and all, because the artificial
|
|||
|
|
// basis the two-phase start needs requires b >= 0 in every
|
|||
|
|
// row; negating flips the slack's side but the sign convention
|
|||
|
|
// of the bound row survives the flip.
|
|||
|
|
build := func(slack float64, bound float64) {
|
|||
|
|
// The row carries its artificial column from the start, the
|
|||
|
|
// same in-place extension MinimiseLinear builds.
|
|||
|
|
row := back[len(rows)*(cols+built) : (len(rows)+1)*(cols+built)]
|
|||
|
|
for j := range n {
|
|||
|
|
v := cons.A.FloatAt(i*n + j)
|
|||
|
|
row[j], row[n+j] = v, -v
|
|||
|
|
}
|
|||
|
|
if slack != 0 {
|
|||
|
|
row[slackCol] = slack
|
|||
|
|
slackCol++
|
|||
|
|
}
|
|||
|
|
if bound < 0 {
|
|||
|
|
for j := range row {
|
|||
|
|
row[j] = -row[j]
|
|||
|
|
}
|
|||
|
|
bound = -bound
|
|||
|
|
}
|
|||
|
|
rows = append(rows, row)
|
|||
|
|
rhs = append(rhs, bound)
|
|||
|
|
}
|
|||
|
|
switch {
|
|||
|
|
case lo == up:
|
|||
|
|
build(0, up)
|
|||
|
|
default:
|
|||
|
|
if up < math.Inf(1) {
|
|||
|
|
build(1, up)
|
|||
|
|
}
|
|||
|
|
if lo > math.Inf(-1) {
|
|||
|
|
build(-1, lo)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
prob.rows, prob.b = rows, rhs
|
|||
|
|
stdCost := make([]float64, cols)
|
|||
|
|
copy(stdCost, cost)
|
|||
|
|
for j := range n {
|
|||
|
|
stdCost[n+j] = -cost[j]
|
|||
|
|
}
|
|||
|
|
xStd, _, err := solveTwoPhase(prob, stdCost, opts, name)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, 0, err
|
|||
|
|
}
|
|||
|
|
// Back-substitute x = p − q and value the original cost on the
|
|||
|
|
// original variables: the split's two halves cancel only in exact
|
|||
|
|
// arithmetic, so the caller sees the recomputed figure.
|
|||
|
|
x := make([]float64, n)
|
|||
|
|
value := 0.0
|
|||
|
|
for j := range n {
|
|||
|
|
x[j] = xStd[j] - xStd[n+j]
|
|||
|
|
value += cost[j] * x[j]
|
|||
|
|
}
|
|||
|
|
out, fv := packResult(x, value)
|
|||
|
|
return out, fv, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// standardForm is the working copy the two-phase method runs on: the
|
|||
|
|
// rows a·x = b with b ≥ 0 after negation, nreal real columns, and one
|
|||
|
|
// artificial column per row appended behind them. Row drops during the
|
|||
|
|
// phase transition shorten rows and b together with the basis.
|
|||
|
|
//
|
|||
|
|
// bm and fac are the reusable basis matrix and its factorisation: the
|
|||
|
|
// basis is gathered afresh and refactorised at every pivot, which
|
|||
|
|
// rewrites the whole m×m matrix, so one buffer per solve replaces one
|
|||
|
|
// per pivot. Every entry of bm is written before it is read. The
|
|||
|
|
// per-pivot vectors ride the same rule: the pricing, ratio and solution
|
|||
|
|
// sweeps each overwrite the whole live prefix before reading it, so one
|
|||
|
|
// set of buffers serves every pivot of one solve.
|
|||
|
|
type standardForm struct {
|
|||
|
|
rows [][]float64
|
|||
|
|
b []float64
|
|||
|
|
nreal int
|
|||
|
|
bm []float64
|
|||
|
|
fac lu
|
|||
|
|
xb []float64
|
|||
|
|
pi []float64
|
|||
|
|
cb []float64
|
|||
|
|
col []float64
|
|||
|
|
w []float64
|
|||
|
|
unit []float64
|
|||
|
|
y []float64
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// growF returns buf at length n, allocating only when the current
|
|||
|
|
// capacity falls short; every caller overwrites the whole prefix.
|
|||
|
|
func growF(buf []float64, n int) []float64 {
|
|||
|
|
if cap(buf) < n {
|
|||
|
|
return make([]float64, n)
|
|||
|
|
}
|
|||
|
|
return buf[:n]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// cols is the total column count: the real columns plus one artificial
|
|||
|
|
// per row still carried.
|
|||
|
|
func (s *standardForm) cols() int { return s.nreal + len(s.rows) }
|
|||
|
|
|
|||
|
|
// graftArtificials extends every row with the artificial identity
|
|||
|
|
// columns the artificial phase runs on: column nreal + r is the r-th
|
|||
|
|
// unit vector. It runs once, before phase 1. A row the entry points
|
|||
|
|
// built already wide enough for its artificial is extended in place:
|
|||
|
|
// the tail slots hold zeros until the unit entry is written, so the
|
|||
|
|
// values the phases read are the ones a freshly built row carried.
|
|||
|
|
func (s *standardForm) graftArtificials() {
|
|||
|
|
m := len(s.rows)
|
|||
|
|
for i := range m {
|
|||
|
|
if len(s.rows[i]) >= s.nreal+m {
|
|||
|
|
s.rows[i] = s.rows[i][:s.nreal+m]
|
|||
|
|
s.rows[i][s.nreal+i] = 1
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
row := make([]float64, s.nreal+m)
|
|||
|
|
copy(row, s.rows[i])
|
|||
|
|
row[s.nreal+i] = 1
|
|||
|
|
s.rows[i] = row
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// solveTwoPhase runs the artificial phase, refuses an infeasible
|
|||
|
|
// problem with its evidence, expels the surviving artificials, and
|
|||
|
|
// runs the real phase. It returns the real part of the solution and
|
|||
|
|
// the objective c·x valued on it.
|
|||
|
|
func solveTwoPhase(s *standardForm, cost []float64, opts LinearProgramOptions, name string) ([]float64, float64, error) {
|
|||
|
|
tol := opts.Tolerance
|
|||
|
|
if tol <= 0 {
|
|||
|
|
tol = 1e-9
|
|||
|
|
}
|
|||
|
|
budget := opts.MaxIterations
|
|||
|
|
if budget <= 0 {
|
|||
|
|
budget = 10000
|
|||
|
|
}
|
|||
|
|
m := len(s.rows)
|
|||
|
|
s.graftArtificials()
|
|||
|
|
basis := make([]int, m)
|
|||
|
|
inBasic := make([]bool, s.cols())
|
|||
|
|
for i := range m {
|
|||
|
|
basis[i] = s.nreal + i
|
|||
|
|
inBasic[basis[i]] = true
|
|||
|
|
}
|
|||
|
|
if m > 0 {
|
|||
|
|
// Phase 1: minimise the sum of the artificials. They start as
|
|||
|
|
// the basis (the identity, with b ≥ 0), and once one leaves it
|
|||
|
|
// never re-enters: canEnter admits the real columns only.
|
|||
|
|
cost1 := make([]float64, s.cols())
|
|||
|
|
for j := s.nreal; j < s.cols(); j++ {
|
|||
|
|
cost1[j] = 1
|
|||
|
|
}
|
|||
|
|
enter1 := make([]bool, s.cols())
|
|||
|
|
for j := range s.nreal {
|
|||
|
|
enter1[j] = true
|
|||
|
|
}
|
|||
|
|
if err := s.pivotLoop(basis, inBasic, cost1, enter1, tol, budget, name, "phase 1", true); err != nil {
|
|||
|
|
return nil, 0, err
|
|||
|
|
}
|
|||
|
|
// The phase-1 optimum is the total infeasibility: anything
|
|||
|
|
// above the tolerance is an infeasible problem, refused with
|
|||
|
|
// the figure and the worst offending row as the evidence.
|
|||
|
|
residual, worst, worstRow, aerr := s.artificialSum(basis)
|
|||
|
|
if aerr != nil {
|
|||
|
|
return nil, 0, base.Errf("%s: %w", name, aerr)
|
|||
|
|
}
|
|||
|
|
if residual > tol*math.Max(1, maxAbs(s.b)) {
|
|||
|
|
return nil, 0, base.Errf("%s: the problem is infeasible: phase 1 ended with an infeasibility of %g (row %d still carries %g)",
|
|||
|
|
name, residual, worstRow+1, worst)
|
|||
|
|
}
|
|||
|
|
var err error
|
|||
|
|
if basis, err = s.expelArtificials(basis, inBasic, tol); err != nil {
|
|||
|
|
return nil, 0, base.Errf("%s: %w", name, err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// Phase 2: the real costs over a feasible basis. The artificials
|
|||
|
|
// are gone from the basis and canEnter keeps them out of the
|
|||
|
|
// pricing.
|
|||
|
|
enter2 := make([]bool, s.cols())
|
|||
|
|
for j := range s.nreal {
|
|||
|
|
enter2[j] = true
|
|||
|
|
}
|
|||
|
|
if err := s.pivotLoop(basis, inBasic, cost, enter2, tol, budget, name, "phase 2", false); err != nil {
|
|||
|
|
return nil, 0, err
|
|||
|
|
}
|
|||
|
|
return s.solution(basis, cost)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// basisMatrix gathers the basis columns into dst as a row-major m×m
|
|||
|
|
// matrix for the factorisation. dst is grown to m² if it is too short
|
|||
|
|
// and returned; every entry of the m×m block is written.
|
|||
|
|
func (s *standardForm) basisMatrix(dst []float64, basis []int) []float64 {
|
|||
|
|
m := len(s.rows)
|
|||
|
|
if cap(dst) < m*m {
|
|||
|
|
dst = make([]float64, m*m)
|
|||
|
|
}
|
|||
|
|
dst = dst[:m*m]
|
|||
|
|
for r := range m {
|
|||
|
|
row := s.rows[r]
|
|||
|
|
for k, col := range basis {
|
|||
|
|
dst[r*m+k] = row[col]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return dst
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// refactor gathers the basis columns and factors them into the form's
|
|||
|
|
// own reusable factorisation, which is fully rewritten: the pivot loop,
|
|||
|
|
// the artificial sum, the artificial expulsion and the final solution
|
|||
|
|
// all read the basis this way.
|
|||
|
|
func (s *standardForm) refactor(basis []int) (*lu, error) {
|
|||
|
|
s.bm = s.basisMatrix(s.bm, basis)
|
|||
|
|
if err := s.fac.factor(s.bm, len(s.rows)); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
return &s.fac, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// pivotLoop is the revised simplex iteration: refactorise the basis,
|
|||
|
|
// price the eligible non-basic columns, and pivot under Bland's rule
|
|||
|
|
// until no eligible column prices out negatively. The phase1 flag
|
|||
|
|
// shapes the diagnostics only: an unbounded ray is how phase 2 reports
|
|||
|
|
// an unbounded objective and a contradiction in phase 1, whose
|
|||
|
|
// objective is bounded below by zero.
|
|||
|
|
func (s *standardForm) pivotLoop(basis []int, inBasic []bool, cost []float64, canEnter []bool, tol float64, budget int,
|
|||
|
|
name, phase string, phase1 bool) error {
|
|||
|
|
m := len(s.rows)
|
|||
|
|
xb := growF(s.xb, m)
|
|||
|
|
pi := growF(s.pi, m)
|
|||
|
|
cb := growF(s.cb, m)
|
|||
|
|
col := growF(s.col, m)
|
|||
|
|
w := growF(s.w, m)
|
|||
|
|
s.xb, s.pi, s.cb, s.col, s.w = xb, pi, cb, col, w
|
|||
|
|
// A basic value that rounds a hair below zero after a solve is
|
|||
|
|
// clamped; one that is genuinely negative means the basis lost its
|
|||
|
|
// primal feasibility, which is a defect, not an answer.
|
|||
|
|
floor := -1e-9 * math.Max(1, maxAbs(s.b))
|
|||
|
|
for piv := range budget {
|
|||
|
|
f, err := s.refactor(basis)
|
|||
|
|
if err != nil {
|
|||
|
|
return base.Errf("%s: %s: %w after %d pivots", name, phase, err, piv)
|
|||
|
|
}
|
|||
|
|
f.solve(s.b, xb)
|
|||
|
|
for i := range m {
|
|||
|
|
if xb[i] < 0 {
|
|||
|
|
if xb[i] < floor {
|
|||
|
|
return base.Errf("%s: %s: the basis lost primal feasibility at row %d (%g) after %d pivots",
|
|||
|
|
name, phase, i+1, xb[i], piv)
|
|||
|
|
}
|
|||
|
|
xb[i] = 0
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
for i, c := range basis {
|
|||
|
|
cb[i] = cost[c]
|
|||
|
|
}
|
|||
|
|
f.solveT(cb, pi)
|
|||
|
|
// Bland's entering rule: the lowest-indexed eligible column
|
|||
|
|
// whose reduced cost is negative.
|
|||
|
|
enter := -1
|
|||
|
|
for j := range s.cols() {
|
|||
|
|
if inBasic[j] || !canEnter[j] {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
d := cost[j]
|
|||
|
|
for r := range m {
|
|||
|
|
d -= pi[r] * s.rows[r][j]
|
|||
|
|
}
|
|||
|
|
if d < -tol {
|
|||
|
|
enter = j
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if enter == -1 {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
for r := range m {
|
|||
|
|
col[r] = s.rows[r][enter]
|
|||
|
|
}
|
|||
|
|
f.solve(col, w)
|
|||
|
|
theta := math.Inf(1)
|
|||
|
|
for i := range m {
|
|||
|
|
if w[i] > tol {
|
|||
|
|
theta = math.Min(theta, xb[i]/w[i])
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if math.IsInf(theta, 1) {
|
|||
|
|
if phase1 {
|
|||
|
|
return base.Errf("%s: %s: an unbounded ray contradicts the phase-1 objective, which is bounded below by zero", name, phase)
|
|||
|
|
}
|
|||
|
|
return base.Errf("%s: the objective is unbounded below: column %d prices out as a profitable ray no row limits",
|
|||
|
|
name, enter+1)
|
|||
|
|
}
|
|||
|
|
// Bland's leaving rule: among the rows tied at the minimum
|
|||
|
|
// ratio, the lowest-indexed basic variable leaves. The index,
|
|||
|
|
// not the row position, is what the anti-cycling proof needs.
|
|||
|
|
tie := 1e-9 * math.Max(1, math.Abs(theta))
|
|||
|
|
leave := -1
|
|||
|
|
for i := range m {
|
|||
|
|
if w[i] > tol && xb[i]/w[i] <= theta+tie {
|
|||
|
|
if leave == -1 || basis[i] < basis[leave] {
|
|||
|
|
leave = i
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
inBasic[basis[leave]] = false
|
|||
|
|
basis[leave] = enter
|
|||
|
|
inBasic[enter] = true
|
|||
|
|
}
|
|||
|
|
return base.Errf("%s: %s: the pivot budget of %d ran out without pricing out", name, phase, budget)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// artificialSum totals the basic artificials' values after phase 1:
|
|||
|
|
// their sum is the total infeasibility phase 1 minimised.
|
|||
|
|
func (s *standardForm) artificialSum(basis []int) (total, worst float64, worstRow int, err error) {
|
|||
|
|
// The identical basis was just factored without error at the top
|
|||
|
|
// of the pivot loop's final iteration; the guard keeps the
|
|||
|
|
// invariant explicit rather than trusted.
|
|||
|
|
f, err := s.refactor(basis)
|
|||
|
|
if err != nil {
|
|||
|
|
return 0, 0, -1, err
|
|||
|
|
}
|
|||
|
|
xb := growF(s.xb, len(s.rows))
|
|||
|
|
s.xb = xb
|
|||
|
|
f.solve(s.b, xb)
|
|||
|
|
total, worst, worstRow = 0, 0, -1
|
|||
|
|
for i, c := range basis {
|
|||
|
|
if c >= s.nreal {
|
|||
|
|
total += xb[i]
|
|||
|
|
// Row 0 is a legal carrier of the worst infeasibility, so
|
|||
|
|
// the unset sentinel is −1, not the zero the scan starts
|
|||
|
|
// from: with 0 here any later, smaller artificial would
|
|||
|
|
// overwrite the evidence through the disjunct.
|
|||
|
|
if worstRow < 0 || xb[i] > worst {
|
|||
|
|
worst, worstRow = xb[i], i
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return total, worst, worstRow, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// expelArtificials drives every artificial still basic after phase 1
|
|||
|
|
// out of the basis. A pivot on any real column with a non-zero entry
|
|||
|
|
// in the artificial's row removes it directly (the pivot is
|
|||
|
|
// degenerate: the artificial's value is zero at the phase-1 optimum).
|
|||
|
|
// A row where no real column has such an entry is redundant, a linear
|
|||
|
|
// combination of the others at the current vertex, so the row and its
|
|||
|
|
// artificial leave the problem together and the reduced basis stays
|
|||
|
|
// non-singular.
|
|||
|
|
func (s *standardForm) expelArtificials(basis []int, inBasic []bool, tol float64) ([]int, error) {
|
|||
|
|
// One unit vector and one solve target for the whole expulsion: each
|
|||
|
|
// round clears the previous round's basis vector and the solve
|
|||
|
|
// overwrites y whole.
|
|||
|
|
for {
|
|||
|
|
r := -1
|
|||
|
|
for i := range basis {
|
|||
|
|
if basis[i] >= s.nreal {
|
|||
|
|
r = i
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if r == -1 {
|
|||
|
|
return basis, nil
|
|||
|
|
}
|
|||
|
|
m := len(s.rows)
|
|||
|
|
unit := growF(s.unit, m)
|
|||
|
|
y := growF(s.y, m)
|
|||
|
|
s.unit, s.y = unit, y
|
|||
|
|
f, err := s.refactor(basis)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, base.Errf("phase 1: %w while expelling an artificial", err)
|
|||
|
|
}
|
|||
|
|
// Row r of B⁻¹: solve Bᵀ y = e_r, then the row is yᵀ.
|
|||
|
|
clear(unit)
|
|||
|
|
unit[r] = 1
|
|||
|
|
f.solveT(unit, y)
|
|||
|
|
choice := -1
|
|||
|
|
for j := range s.nreal {
|
|||
|
|
if inBasic[j] {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
dot := 0.0
|
|||
|
|
for i := range m {
|
|||
|
|
dot += y[i] * s.rows[i][j]
|
|||
|
|
}
|
|||
|
|
if math.Abs(dot) > tol {
|
|||
|
|
choice = j
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if choice >= 0 {
|
|||
|
|
inBasic[basis[r]] = false
|
|||
|
|
basis[r] = choice
|
|||
|
|
inBasic[choice] = true
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
s.rows = append(s.rows[:r], s.rows[r+1:]...)
|
|||
|
|
s.b = append(s.b[:r], s.b[r+1:]...)
|
|||
|
|
inBasic[basis[r]] = false
|
|||
|
|
basis = append(basis[:r], basis[r+1:]...)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// solution reconstructs the point from the final basis and values the
|
|||
|
|
// cost on it. Basic values that round a hair below zero are clamped:
|
|||
|
|
// x ≥ 0 is the contract the caller sees.
|
|||
|
|
func (s *standardForm) solution(basis []int, cost []float64) ([]float64, float64, error) {
|
|||
|
|
m := len(s.rows)
|
|||
|
|
f, err := s.refactor(basis)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, 0, err
|
|||
|
|
}
|
|||
|
|
xb := growF(s.xb, m)
|
|||
|
|
s.xb = xb
|
|||
|
|
f.solve(s.b, xb)
|
|||
|
|
x := make([]float64, s.nreal)
|
|||
|
|
value := 0.0
|
|||
|
|
for i, c := range basis {
|
|||
|
|
if c < s.nreal {
|
|||
|
|
v := math.Max(xb[i], 0)
|
|||
|
|
x[c] = v
|
|||
|
|
value += cost[c] * v
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return x, value, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// lu holds an LU factorisation with partial pivoting of a small dense
|
|||
|
|
// square matrix: PA = LU with the swaps recorded in piv. The simplex
|
|||
|
|
// refactorises it once per pivot and the active-set solver in qp.go
|
|||
|
|
// factors a KKT system with it per iteration, so the type is shared
|
|||
|
|
// machinery for both.
|
|||
|
|
type lu struct {
|
|||
|
|
n int
|
|||
|
|
a []float64 // row-major, factored in place
|
|||
|
|
piv []int // row swaps in application order
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// factorLU factorises the n×n row-major matrix mat into a fresh
|
|||
|
|
// factorisation. A pivot vanishing against the matrix's scale is a
|
|||
|
|
// singular matrix, reported as an error naming the column: for the
|
|||
|
|
// simplex that is a basis no longer invertible, for the KKT system an
|
|||
|
|
// active set that has lost rank.
|
|||
|
|
func factorLU(mat []float64, n int) (*lu, error) {
|
|||
|
|
f := &lu{}
|
|||
|
|
if err := f.factor(mat, n); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
return f, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// factor refactorises the receiver on the n×n row-major matrix mat,
|
|||
|
|
// reusing the storage a previous factorisation left behind: the
|
|||
|
|
// simplex's basis and the active-set solver's KKT system are both
|
|||
|
|
// refactorised once per iteration, so one factor per solve replaces one
|
|||
|
|
// per iteration. mat is left untouched; every entry of the workspace is
|
|||
|
|
// overwritten from it, which is what makes the reuse invisible in the
|
|||
|
|
// result.
|
|||
|
|
func (f *lu) factor(mat []float64, n int) error {
|
|||
|
|
if n == 0 {
|
|||
|
|
f.n, f.a, f.piv = 0, f.a[:0], f.piv[:0]
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
if cap(f.a) < n*n {
|
|||
|
|
f.a = make([]float64, n*n)
|
|||
|
|
}
|
|||
|
|
if cap(f.piv) < n {
|
|||
|
|
f.piv = make([]int, n)
|
|||
|
|
}
|
|||
|
|
f.n, f.a, f.piv = n, f.a[:n*n], f.piv[:n]
|
|||
|
|
a, piv := f.a, f.piv
|
|||
|
|
// The copy and the scale scan are one fused pass: the scale is the
|
|||
|
|
// maximum over the same values either way.
|
|||
|
|
scale := 0.0
|
|||
|
|
for i, v := range mat[:n*n] {
|
|||
|
|
a[i] = v
|
|||
|
|
if x := math.Abs(v); x > scale {
|
|||
|
|
scale = x
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if scale == 0 {
|
|||
|
|
return base.Errf("the matrix is singular (a zero matrix)")
|
|||
|
|
}
|
|||
|
|
for k := range n {
|
|||
|
|
p, best := k, math.Abs(a[k*n+k])
|
|||
|
|
for i := k + 1; i < n; i++ {
|
|||
|
|
if v := math.Abs(a[i*n+k]); v > best {
|
|||
|
|
p, best = i, v
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
piv[k] = p
|
|||
|
|
if best <= 1e-14*scale {
|
|||
|
|
return base.Errf("the matrix is singular to working precision (pivot %g in column %d)", best, k+1)
|
|||
|
|
}
|
|||
|
|
if p != k {
|
|||
|
|
for j := range n {
|
|||
|
|
a[k*n+j], a[p*n+j] = a[p*n+j], a[k*n+j]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
inv := 1 / a[k*n+k]
|
|||
|
|
for i := k + 1; i < n; i++ {
|
|||
|
|
e := a[i*n+k] * inv
|
|||
|
|
a[i*n+k] = e
|
|||
|
|
if e != 0 {
|
|||
|
|
for j := k + 1; j < n; j++ {
|
|||
|
|
a[i*n+j] -= e * a[k*n+j]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// solve writes A⁻¹ b into x: the recorded swaps forward, then the unit
|
|||
|
|
// lower triangle forward, then the upper triangle back. b is left
|
|||
|
|
// untouched.
|
|||
|
|
func (f *lu) solve(b, x []float64) {
|
|||
|
|
n := f.n
|
|||
|
|
copy(x, b)
|
|||
|
|
for k := range n {
|
|||
|
|
x[k], x[f.piv[k]] = x[f.piv[k]], x[k]
|
|||
|
|
}
|
|||
|
|
for i := 1; i < n; i++ {
|
|||
|
|
s := x[i]
|
|||
|
|
for k := range i {
|
|||
|
|
s -= f.a[i*n+k] * x[k]
|
|||
|
|
}
|
|||
|
|
x[i] = s
|
|||
|
|
}
|
|||
|
|
for i := n - 1; i >= 0; i-- {
|
|||
|
|
s := x[i]
|
|||
|
|
for k := i + 1; k < n; k++ {
|
|||
|
|
s -= f.a[i*n+k] * x[k]
|
|||
|
|
}
|
|||
|
|
x[i] = s / f.a[i*n+i]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// solveT writes Aᵀ⁻¹ b into x. With PA = LU the transpose factors as
|
|||
|
|
// Aᵀ = UᵀLᵀP, so the solve runs Uᵀ forward, Lᵀ back and undoes the
|
|||
|
|
// swaps in reverse. The dual prices of the simplex and the redundant
|
|||
|
|
// row scan of the phase transition both come through here.
|
|||
|
|
func (f *lu) solveT(b, x []float64) {
|
|||
|
|
n := f.n
|
|||
|
|
copy(x, b)
|
|||
|
|
for i := range n { // Uᵀ w = b, forward, diagonal uᵢᵢ
|
|||
|
|
s := x[i]
|
|||
|
|
for k := range i {
|
|||
|
|
s -= f.a[k*n+i] * x[k]
|
|||
|
|
}
|
|||
|
|
x[i] = s / f.a[i*n+i]
|
|||
|
|
}
|
|||
|
|
for i := n - 1; i >= 0; i-- { // Lᵀ v = w, back, unit diagonal
|
|||
|
|
s := x[i]
|
|||
|
|
for k := i + 1; k < n; k++ {
|
|||
|
|
s -= f.a[k*n+i] * x[k]
|
|||
|
|
}
|
|||
|
|
x[i] = s
|
|||
|
|
}
|
|||
|
|
for k := n - 1; k >= 0; k-- { // x = Pᵀ v: the swaps in reverse
|
|||
|
|
x[k], x[f.piv[k]] = x[f.piv[k]], x[k]
|
|||
|
|
}
|
|||
|
|
}
|