Files
tensor/optim/simplex.go
T

815 lines
26 KiB
Go
Raw Normal View History

2026-09-03 10:00:00 +02:00
// 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]
}
}