// Copyright (c) 2026 Petr Balvín (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] } }