// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: MIT package linalg import ( "math" "math/cmplx" "sourcedock.dev/petrbalvin/tensor/internal/base" ) // Direct complex singular value decomposition by Golub-Kahan // bidiagonalisation. Reducing A through Aᴴ·A squares the condition // number, so a tiny singular value of A carries the accuracy of the // square of a tiny eigenvalue; the direct route never forms the // Gramian. core.Complex Householder reflectors chosen with real beta leave // a real bidiagonal matrix, and the Golub-Reinsch shifted QR iteration // then works on real numbers only, folding its rotations back into the // complex unitary factors. // svdBidiagonalise reduces the complex m×n matrix a (m ≥ n) to upper // bidiagonal form with real diagonal d and superdiagonal e, // accumulating the left unitary into u (m×m) and the right one into v // (n×n), both seeded with the identity. Real beta is the choice that // keeps both bidiagonal bands real: the reflector maps its head to a // real multiple of the first coordinate whatever the head's phase. func svdBidiagonalise(a []complex128, m, n int) (d, e []float64, u, v []complex128) { u = make([]complex128, m*m) v = make([]complex128, n*n) for i := range m { u[i*m+i] = 1 } for i := range n { v[i*n+i] = 1 } d = make([]float64, n) e = make([]float64, n) // Reflector scratch reused across the sweep; each step uses the // prefix it needs. vecBuf := make([]complex128, m) vecRBuf := make([]complex128, n) for k := range n { // Left reflector on column k, rows k..m-1. norm := 0.0 for i := k; i < m; i++ { norm += base.Real2(a[i*n+k]) } norm = math.Sqrt(norm) if norm > 0 { beta := -norm if real(a[k*n+k]) < 0 { beta = norm } vh := 0.0 vec := vecBuf[:m-k] for i := k; i < m; i++ { vec[i-k] = a[i*n+k] if i == k { vec[0] -= complex(beta, 0) } vh += base.Real2(vec[i-k]) } if vh > 0 { vhx := complex(0, 0) for i := k; i < m; i++ { vhx += cmplx.Conj(vec[i-k]) * a[i*n+k] } tau := complex(1, 0) / vhx for j := k; j < n; j++ { s := complex(0, 0) for i := k; i < m; i++ { s += cmplx.Conj(vec[i-k]) * a[i*n+j] } s *= tau for i := k; i < m; i++ { a[i*n+j] -= s * vec[i-k] } } for r := range m { s := complex(0, 0) for j := k; j < m; j++ { s += u[r*m+j] * vec[j-k] } s *= cmplx.Conj(tau) for j := k; j < m; j++ { u[r*m+j] -= s * cmplx.Conj(vec[j-k]) } } } } d[k] = real(a[k*n+k]) a[k*n+k] = complex(d[k], 0) for i := k + 1; i < m; i++ { a[i*n+k] = 0 } if k == n-1 { break } // Right reflector on row k, columns k+1..n-1. norm = 0.0 for j := k + 1; j < n; j++ { norm += base.Real2(a[k*n+j]) } norm = math.Sqrt(norm) if norm > 0 { beta := -norm if real(a[k*n+k+1]) < 0 { beta = norm } vh := 0.0 vecR := vecRBuf[:n-k-1] for j := k + 1; j < n; j++ { vecR[j-k-1] = a[k*n+j] if j == k+1 { vecR[0] -= complex(beta, 0) } vh += base.Real2(vecR[j-k-1]) } if vh > 0 { vhx := complex(0, 0) for j := k + 1; j < n; j++ { vhx += cmplx.Conj(vecR[j-k-1]) * a[k*n+j] } tau := complex(1, 0) / vhx for i := range m { s := complex(0, 0) for j := k + 1; j < n; j++ { s += a[i*n+j] * cmplx.Conj(vecR[j-k-1]) } s *= tau for j := k + 1; j < n; j++ { a[i*n+j] -= s * vecR[j-k-1] } } for r := range n { s := complex(0, 0) for j := k + 1; j < n; j++ { s += v[r*n+j] * cmplx.Conj(vecR[j-k-1]) } s *= tau for j := k + 1; j < n; j++ { v[r*n+j] -= s * vecR[j-k-1] } } } e[k] = real(a[k*n+k+1]) a[k*n+k+1] = complex(e[k], 0) for j := k + 2; j < n; j++ { a[k*n+j] = 0 } } } return d, e, u, v } // svdGolubReinsch diagonalises the real bidiagonal pair (d, e) in // place, folding every rotation into the columns of u (m×m) and v // (n×n). Each sweep is one implicit shifted QR step on BᵀB: a // Wilkinson shift from the trailing two-by-two opens the sweep, and // the bulge it raises is chased off the block's end by alternating // left and right rotations. A zero leading diagonal deflates through a // rotation chase that empties its column, which is how rank deficiency // leaves the iteration honestly. func svdGolubReinsch(d, e []float64, u, v []complex128, m, n int) error { const name = "SVDComplex" // Deflation floor relative to the bidiagonal norm, the same // backward-stability floor symmetricQr uses: a purely // neighbour-relative threshold never triggers when both diagonals // sit at rounding-zero, which the FMA contraction of the vector // build leaves the chase on, and the iteration then exhausts // itself over an already negligible block. scale := 0.0 for i := range n { scale += d[i] * d[i] if i+1 < n { scale += 2 * e[i] * e[i] } } tolAbs := base.EpsF * math.Sqrt(scale) negligible := func(i int) bool { a := math.Abs(e[i]) return a == 0 || a <= base.EpsF*(math.Abs(d[i])+math.Abs(d[i+1])) || a < tolAbs } for iter := 0; iter < 60*n+100; iter++ { // Retire negligible superdiagonal entries. for i := range n - 1 { if negligible(i) { e[i] = 0 } } // Locate the trailing unreduced block [l..r]. r := -1 for i := n - 2; i >= 0; i-- { if e[i] != 0 { r = i + 1 break } } if r < 0 { return nil // fully diagonal } l := r - 1 for l > 0 && e[l-1] != 0 { l-- } if d[l] == 0 || math.Abs(d[l]) < tolAbs { // The block's leading column is zero, or sits at the // rounding floor of the whole bidiagonal: the FMA // contraction of the vector build leaves a denormal there // where the portable build hits the exact zero, and a // shifted chase over such a block cycles without // deflating. Left rotations between row l and the rows // below push the superdiagonal e[l] through the block and // off it, deflating the negligible singular value without // touching the rest. w := e[l] for k := l + 1; k <= r; k++ { h := math.Hypot(w, d[k]) c, s := d[k]/h, -w/h d[k] = h if k < r { w = s * e[k] e[k] *= c } for row := range m { ul, uk := u[row*m+l], u[row*m+k] u[row*m+l] = complex(c, 0)*ul + complex(s, 0)*uk u[row*m+k] = complex(-s, 0)*ul + complex(c, 0)*uk } } e[l] = 0 continue } // Wilkinson shift from the trailing two-by-two of BᵀB. t11 := d[r-1] * d[r-1] if r-2 >= 0 { t11 += e[r-2] * e[r-2] } t22 := d[r]*d[r] + e[r-1]*e[r-1] t21 := d[r-1] * e[r-1] delta := (t11 - t22) / 2 denom := math.Abs(delta) + math.Sqrt(delta*delta+t21*t21) mu := t22 if denom > 0 { sign := 1.0 if delta < 0 { sign = -1.0 } mu = t22 - sign*t21*t21/denom } // Opening right rotation on columns (l, l+1), taken from the // first column of T − μI. g0 := d[l]*d[l] - mu h0 := math.Hypot(g0, d[l]*e[l]) c0, s0 := g0/h0, d[l]*e[l]/h0 dl, el, dl1 := d[l], e[l], d[l+1] d[l] = c0*dl + s0*el e[l] = -s0*dl + c0*el d[l+1] = c0 * dl1 bulge := s0 * dl1 // at (l+1, l) for row := range n { vl, vl1 := v[row*n+l], v[row*n+l+1] v[row*n+l] = complex(c0, 0)*vl + complex(s0, 0)*vl1 v[row*n+l+1] = complex(-s0, 0)*vl + complex(c0, 0)*vl1 } // Chase: the left rotation kills the bulge and raises a // super-bulge; the right rotation kills that and re-raises the // bulge one step down, until it falls off the block's end. for k := l; k < r; k++ { // Left rotation on rows (k, k+1). A deflated diagonal meets // a deflated bulge with h = 0: the rotation is the identity // there, where the division would raise 0/0 NaNs that spill // through the factor. h := math.Hypot(d[k], bulge) c1, s1 := 1.0, 0.0 if h > 0 { c1, s1 = d[k]/h, bulge/h } ekOld, dk1Old := e[k], d[k+1] d[k] = h e[k] = c1*ekOld + s1*dk1Old d[k+1] = -s1*ekOld + c1*dk1Old var sb float64 if k < r-1 { sb = s1 * e[k+1] e[k+1] *= c1 } for row := range m { uk, uk1 := u[row*m+k], u[row*m+k+1] u[row*m+k] = complex(c1, 0)*uk + complex(s1, 0)*uk1 u[row*m+k+1] = complex(-s1, 0)*uk + complex(c1, 0)*uk1 } if k >= r-1 { break } // Right rotation on columns (k+1, k+2). h2 := math.Hypot(e[k], sb) c2, s2 := e[k]/h2, sb/h2 ek1Old, dk1Old, dk2Old := e[k+1], d[k+1], d[k+2] e[k] = h2 d[k+1] = c2*dk1Old + s2*ek1Old e[k+1] = -s2*dk1Old + c2*ek1Old bulge = s2 * dk2Old d[k+2] = c2 * dk2Old for row := range n { vk1, vk2 := v[row*n+k+1], v[row*n+k+2] v[row*n+k+1] = complex(c2, 0)*vk1 + complex(s2, 0)*vk2 v[row*n+k+2] = complex(-s2, 0)*vk1 + complex(c2, 0)*vk2 } } } return base.Errf("SVDComplex: the bidiagonal iteration did not converge") }