143 lines
4.6 KiB
Go
143 lines
4.6 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
|
|
// SPDX-License-Identifier: MIT
|
|||
|
|
|
|||
|
|
package linalg
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"sourcedock.dev/petrbalvin/tensor/internal/base"
|
|||
|
|
"sourcedock.dev/petrbalvin/tensor/internal/core"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// Nonsymmetric sparse solve. SpSolve's conjugate gradient leans on
|
|||
|
|
// symmetry for its short recurrences; the convection terms that make
|
|||
|
|
// systems nonsymmetric break that structure, and the fix is van der
|
|||
|
|
// Vorst's BiCGSTAB: two nested three-term recurrences whose second
|
|||
|
|
// half smooths the erratic convergence the plain biconjugate gradient
|
|||
|
|
// shows. Every step costs two sparse products and two preconditioner
|
|||
|
|
// applications, the same order as one CG step pair.
|
|||
|
|
//
|
|||
|
|
// The Jacobi preconditioner needs a nonzero diagonal, which for the
|
|||
|
|
// systems this solver targets (shifted Laplacians with convection,
|
|||
|
|
// discretised transport) is present. Breakdowns (the recurrences
|
|||
|
|
// collapsing) are reported as errors with the step they happened at,
|
|||
|
|
// never swallowed.
|
|||
|
|
|
|||
|
|
// SpSolveBiCGSTAB returns the vector x solving A·x = b for a general
|
|||
|
|
// real square sparse A, by preconditioned BiCGSTAB. Unlike SpSolve it
|
|||
|
|
// asks for no symmetry; like SpSolve the stopping rule is the
|
|||
|
|
// relative residual ‖b − A·x‖₂ ≤ tol·‖b‖₂ (tol ≤ 0 means 1e-10),
|
|||
|
|
// maxIter ≤ 0 means n steps, and an unconverged solve is an error
|
|||
|
|
// naming the residual achieved. The default preconditioner is Jacobi
|
|||
|
|
// scaling, which divides by the diagonal and refuses a zero or
|
|||
|
|
// missing entry; passing an ILU(0) factorisation from NewSparseILU
|
|||
|
|
// replaces it and usually cuts the step count on hard systems.
|
|||
|
|
func SpSolveBiCGSTAB(a *core.SparseCOO, b *core.Array, tol float64, maxIter int, precond ...*SparseILU) (*core.Array, error) {
|
|||
|
|
const name = "SpSolveBiCGSTAB"
|
|||
|
|
n, err := checkSparseSquare(name, a, b)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
c, err := cooToCSR(a, name)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
ilu, diag, err := pickPreconditioner(name, c, precond)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
if tol <= 0 {
|
|||
|
|
tol = spSolveTol
|
|||
|
|
}
|
|||
|
|
if maxIter <= 0 {
|
|||
|
|
maxIter = n
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
r := vectorF64(b, n)
|
|||
|
|
bNorm := norm2F64(r)
|
|||
|
|
if bNorm == 0 {
|
|||
|
|
return core.Zeros(core.Float, n)
|
|||
|
|
}
|
|||
|
|
rHat := append([]float64(nil), r...)
|
|||
|
|
x := make([]float64, n)
|
|||
|
|
v := make([]float64, n)
|
|||
|
|
p := make([]float64, n)
|
|||
|
|
s := make([]float64, n)
|
|||
|
|
t := make([]float64, n)
|
|||
|
|
z := make([]float64, n)
|
|||
|
|
y := make([]float64, n)
|
|||
|
|
mt := make([]float64, n)
|
|||
|
|
|
|||
|
|
rho := 1.0
|
|||
|
|
alpha := 1.0
|
|||
|
|
omega := 1.0
|
|||
|
|
for iter := range maxIter {
|
|||
|
|
rhoNext := dotF64(rHat, r)
|
|||
|
|
if !finiteF64(rhoNext) || rhoNext == 0 {
|
|||
|
|
return nil, base.Errf("SpSolveBiCGSTAB: breakdown at step %d (rho vanished)", iter+1)
|
|||
|
|
}
|
|||
|
|
if iter > 0 {
|
|||
|
|
beta := rhoNext / rho * (alpha / omega)
|
|||
|
|
for i := range n {
|
|||
|
|
p[i] = r[i] + beta*(p[i]-omega*v[i])
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
copy(p, r)
|
|||
|
|
}
|
|||
|
|
iluPrecondition(z, p, ilu, diag)
|
|||
|
|
c.matVec(z, v)
|
|||
|
|
rhatV := dotF64(rHat, v)
|
|||
|
|
if rhatV == 0 {
|
|||
|
|
return nil, base.Errf("SpSolveBiCGSTAB: breakdown at step %d (direction orthogonal to shadow)", iter+1)
|
|||
|
|
}
|
|||
|
|
alpha = rhoNext / rhatV
|
|||
|
|
for i := range n {
|
|||
|
|
s[i] = r[i] - alpha*v[i]
|
|||
|
|
}
|
|||
|
|
if !vecFinite(s) {
|
|||
|
|
return nil, base.Errf("SpSolveBiCGSTAB: breakdown at step %d (non-finite residual)", iter+1)
|
|||
|
|
}
|
|||
|
|
// The half step may already be exact. The check is affirmative so
|
|||
|
|
// a NaN norm (which fails every comparison) can never read as
|
|||
|
|
// converged.
|
|||
|
|
if norm2F64(s) > tol*bNorm {
|
|||
|
|
iluPrecondition(y, s, ilu, diag)
|
|||
|
|
c.matVec(y, t)
|
|||
|
|
iluPrecondition(mt, t, ilu, diag)
|
|||
|
|
mtS := dotF64(mt, s)
|
|||
|
|
mtT := dotF64(mt, t)
|
|||
|
|
if mtT == 0 {
|
|||
|
|
return nil, base.Errf("SpSolveBiCGSTAB: breakdown at step %d (stabiliser vanished)", iter+1)
|
|||
|
|
}
|
|||
|
|
omega = mtS / mtT
|
|||
|
|
// The finiteness half of the guard belongs before the
|
|||
|
|
// update: a NaN omega poisons x and r below, and the
|
|||
|
|
// all-NaN residual then reads as an exact solve through
|
|||
|
|
// the NaN-skipping norm.
|
|||
|
|
if !finiteF64(omega) {
|
|||
|
|
return nil, base.Errf("SpSolveBiCGSTAB: breakdown at step %d (stabiliser not finite)", iter+1)
|
|||
|
|
}
|
|||
|
|
for i := range n {
|
|||
|
|
x[i] += alpha*z[i] + omega*y[i]
|
|||
|
|
r[i] = s[i] - omega*t[i]
|
|||
|
|
}
|
|||
|
|
if !vecFinite(r) {
|
|||
|
|
return nil, base.Errf("SpSolveBiCGSTAB: non-finite residual at step %d", iter+1)
|
|||
|
|
}
|
|||
|
|
if norm2F64(r) <= tol*bNorm {
|
|||
|
|
return floatsToArray(x, []int{n}), nil
|
|||
|
|
}
|
|||
|
|
if omega == 0 {
|
|||
|
|
return nil, base.Errf("SpSolveBiCGSTAB: stagnation at step %d (omega zero)", iter+1)
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
for i := range n {
|
|||
|
|
x[i] += alpha * z[i]
|
|||
|
|
}
|
|||
|
|
return floatsToArray(x, []int{n}), nil
|
|||
|
|
}
|
|||
|
|
rho = rhoNext
|
|||
|
|
}
|
|||
|
|
return nil, base.Errf("SpSolveBiCGSTAB: no convergence in %d steps, residual %.3g (tolerance %.3g)",
|
|||
|
|
maxIter, norm2F64(r), tol*bNorm)
|
|||
|
|
}
|