Files
tensor/optim/simplex.go
T
petrbalvin af4ee19703
Release / gates (push) Successful in 4m38s
Test / test (push) Successful in 5m16s
Release / release (push) Successful in 35s
feat: initial release
Assisted-by: GLM 5.3 Flash
2026-09-03 10:00:00 +02:00

815 lines
26 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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]
}
}