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]
|
||
}
|
||
}
|