Source
stdlib/linalg/routines.hpp
1
// Copyright (c) 2026 BigBrain LLC. MIT-licensed (see LICENSE).2
// Original work; see ACKNOWLEDGMENTS.md for the open-source ideas we build upon.3
#pragma once5
/**6
* @file routines.hpp7
* @brief cheatah `linalg` — numpy's linear-algebra API on ndarray, with8
* SIMD-friendly contiguous kernels (a .purr program writes `linalg.solve(A, b)`).9
*10
* `import linalg` to use it (auto-links ndarray). Unit tests:11
* stdlib/tests/linalg_routines_test.cpp; SIMD reporting tested in12
* stdlib/tests/linalg_smoke_test.cpp. The suite runs under AddressSanitizer (the `asan`13
* preset) and Valgrind (security/run-valgrind.sh) on every QA-gate run.14
*15
* Routines operate on `ndarray::NDArray` (2-D = matrix, 1-D = vector). They mirror16
* https://numpy.org/doc/stable/reference/routines.linalg.html and are implemented17
* in routines.cpp (LU w/ partial pivoting, Cholesky, Householder QR, one-sided18
* Golub–Reinsch SVD, Householder-tridiagonal + QL symmetric eigen, Hessenberg + shifted-QR general19
* eigen) at -O3 -march=native so the hot loops auto-vectorize.20
*21
* SIMD here is pure compiler auto-vectorization (no intrinsics). On a scalar build22
* (no vector ISA) every routine still returns identical results, just slower — see23
* simd.hpp's file comment for the full SIMD model, the no-SIMD behavior, and the24
* compile-time-dispatch limitation.25
*26
* @note `n` below is the matrix dimension. The general eigensolvers `eig`/`eigvals`27
* return a **complex** spectrum (@ref CNDArray) — a real matrix can have28
* complex conjugate eigenvalue pairs — while the Hermitian solvers29
* `eigh`/`eigvalsh` return a guaranteed-real spectrum. The LU/SVD-based scalar30
* routines (`det`/`slogdet`/`cond`/`matrix_rank`) allocate scratch O(n²) for the31
* factorization even though they return a scalar; the products and reductions32
* (`dot`/`matmul`/`trace`/`norm`/…) read their operands in place — zero-copy33
* when contiguous, packing a strided view once.34
*35
* @concurrency every routine is DELIBERATELY single-threaded (no hidden thread pool —36
* the fastest-per-core contract); the caller composes parallelism across37
* independent problems.38
*/39
#include <complex>40
#include <vector>42
#include "backend.hpp"43
#include "concepts.hpp"44
#include "enums.hpp"45
#include "ndarray.hpp"47
namespace cheatah::linalg {49
// The routines operate on cheatah::ndarray::NDArray, re-exported unqualified for brevity in the50
// signatures below. The directive below hides this re-export from the API doc generator so it51
// does not emit a phantom duplicate cheatah::linalg::NDArray class in the namespace/XML structure.52
/// @cond INTERNAL53
using ndarray::NDArray;54
/// @endcond56
/// A complex scalar (`std::complex<double>`) — the element type of @ref CNDArray and57
/// the return type of the complex inner products @ref dot / @ref vdot.58
using Cplx = std::complex<double>;60
/// A complex array (`basic_ndarray<std::complex<double>>`) — what the general61
/// eigensolvers return, since a real matrix can have complex eigenvalues. Prints62
/// element-wise as `a+bj` via @ref cheatah::ndarray::to_string.63
using CNDArray = ndarray::basic_ndarray<Cplx>;65
// ---- Matrix and vector products ----66
// Dot / vdot / inner — the scalar reductions — live in backend.hpp as the scalar-out kernel67
// pattern: an allocating front `T dot(a, b)` plus a `void dot(out, a, b)` kernel split by the68
// HostArray/DeviceArray concepts (one pair serving real, complex, host, and — via a device69
// extension — device operands). Same for `trace`.70
// Outer — both the allocating front `outer(a,b)` and the out-parameter kernel `outer(out,a,b)`71
// are the two-layer overload pair in backend.hpp (the matmul pattern).72
// Matmul — both the allocating front `matmul(a,b)` and the out-parameter kernel `matmul(out,a,b)`73
// are the two-layer `template <Field T, template<typename> class Array>` overloads in backend.hpp74
// (one pair serving real, complex, host, and — via a device extension — device operands).76
// ---- complex products (complex inner-product spaces) ----77
// dot / vdot / inner for complex operands are the SAME two-layer templates in backend.hpp,78
// instantiated at T = std::complex<double>; vdot's conjugation is an `if constexpr` branch. No79
// separate symbols. Complex matmul and conj_transpose are likewise the one generic template each80
// in backend.hpp, instantiating the complex element type — no separate complex symbols.82
/// @cond INTERNAL83
/// the HOST kernels of the factorization/solver seam. Every routine below is the84
/// backend.hpp pattern: an allocating front (inline in this header, `requires NumericArray`) that85
/// validates metadata, allocates via `Array<T>::uninitialized`, and calls the same-named out-param86
/// (or scalar-out) kernel UNQUALIFIED — so these HostArray kernels (defined + instantiated in87
/// routines.cpp) serve host arrays, and a device extension's `requires DeviceArray` overloads are88
/// found by ADL. Declared here, before the fronts, so the fronts' unqualified calls see them.89
/// Each kernel's full documentation sits with its declaration here; the allocating front that90
/// calls it is documented further down this header.91
/**92
* Matrix power into the caller's buffer @p out (out FIRST) — the buffer-reuse overload of93
* @ref matrix_power (the HOST kernel of the seam pattern; a device extension supplies its own94
* `requires DeviceArray` overload, found by ADL).95
* @param out destination; a contiguous n×n matrix, overwritten with Aⁿ.96
* @param a square matrix.97
* @param n exponent.98
* @complexity O(n³·log|n|).99
* @alloc reuses @p out; the binary-exponentiation products allocate their own scratch.100
* @test LinalgRoutines.FactorizationOutReusesBuffer101
*/102
template <ndarray::Field T, template <typename> class Array>103
requires HostArray<Array<T>> && ndarray::FloatingPoint<T>104
void matrix_power(Array<T>& out, const Array<T>& a, long long n);105
/**106
* Cholesky factor into the caller's buffer @p out (out FIRST) — the buffer-reuse overload of107
* @ref cholesky (the HOST kernel of the seam pattern; a device extension supplies its own108
* `requires DeviceArray` overload, found by ADL).109
* @param out destination; a contiguous n×n matrix, overwritten with the lower-triangular L.110
* @param a square SPD matrix.111
* @complexity O(n³).112
* @alloc reuses @p out (the factor is computed into private scratch, then copied in).113
* @test LinalgRoutines.FactorizationOutReusesBuffer114
*/115
template <ndarray::Field T, template <typename> class Array>116
requires HostArray<Array<T>> && ndarray::FloatingPoint<T>117
void cholesky(Array<T>& out, const Array<T>& a);118
/**119
* Reduced QR into the caller's buffers (outs FIRST) — the buffer-reuse overload of @ref qr,120
* filling @p q and @p r instead of allocating a @ref QR (the HOST kernel of the seam pattern; a121
* device extension supplies its own `requires DeviceArray` overload, found by ADL).122
* @param q destination for the orthonormal factor; a contiguous m×n matrix, overwritten.123
* @param r destination for the upper-triangular factor; a contiguous n×n matrix, overwritten.124
* @param a m×n matrix.125
* @complexity O(m²·n).126
* @alloc reuses @p q and @p r (the factors are computed into private scratch, then copied in).127
* @test LinalgRoutines.DecompositionOutReusesBuffer128
*/129
template <ndarray::Field T, template <typename> class Array>130
requires HostArray<Array<T>> && ndarray::FloatingPoint<T>131
void qr(Array<T>& q, Array<T>& r, const Array<T>& a);132
/**133
* Full SVD into the caller's buffers (outs FIRST) — the buffer-reuse overload of @ref svd,134
* filling @p u, @p s and @p vh instead of allocating an @ref SVD (the HOST kernel of the seam135
* pattern; a device extension supplies its own `requires DeviceArray` overload, found by ADL).136
* @param u destination for the left singular vectors; a contiguous m×n matrix, overwritten.137
* @param s destination for the singular values; a contiguous length-n vector, overwritten.138
* @param vh destination for Vᵀ; a contiguous n×n matrix, overwritten.139
* @param a m×n matrix (rows ≥ cols).140
* @complexity iterative O(n³).141
* @alloc reuses @p u, @p s, @p vh (the factors are computed into private scratch, then copied in).142
* @test LinalgRoutines.DecompositionOutReusesBuffer143
*/144
template <ndarray::Field T, template <typename> class Array>145
requires HostArray<Array<T>> && ndarray::FloatingPoint<T>146
void svd(Array<T>& u, Array<T>& s, Array<T>& vh, const Array<T>& a);147
/**148
* Singular values into the caller's buffer @p out (out FIRST) — the buffer-reuse overload of149
* @ref svdvals (the HOST kernel of the seam pattern; a device extension supplies its own150
* `requires DeviceArray` overload, found by ADL).151
* @param out destination; a contiguous length-min(m,n) vector, overwritten with the descending values.152
* @param a m×n matrix.153
* @complexity iterative O(n³).154
* @alloc reuses @p out; the Golub–Reinsch reduction allocates its own scratch.155
* @test LinalgRoutines.FactorizationOutReusesBuffer156
*/157
template <ndarray::Field T, template <typename> class Array>158
requires HostArray<Array<T>> && ndarray::FloatingPoint<T>159
void svdvals(Array<T>& out, const Array<T>& a);160
/**161
* General eigendecomposition into the caller's buffers (outs FIRST) — the buffer-reuse overload of162
* @ref eig, filling @p values and @p vectors instead of allocating an @ref EigC (the HOST kernel163
* of the seam pattern; a device extension supplies its own `requires DeviceArray` overload).164
* @param values destination for the complex eigenvalues; a contiguous length-n vector, overwritten.165
* @param vectors destination for the complex eigenvectors (columns); a contiguous n×n matrix, overwritten.166
* @param a square matrix.167
* @complexity iterative O(n³) for the eigenvalues; general (non-symmetric) eigenvectors168
* add O(n⁴) (inverse iteration per eigenvalue — see @ref eig).169
* @alloc reuses @p values and @p vectors (computed into private scratch, then copied in).170
* @test LinalgRoutines.DecompositionOutReusesBuffer171
*/172
template <ndarray::Field T, template <typename> class Array>173
requires HostArray<Array<T>> && ndarray::FloatingPoint<T>174
void eig(Array<ndarray::complex_of_t<T>>& values, Array<ndarray::complex_of_t<T>>& vectors,175
const Array<T>& a);176
/**177
* General eigenvalues into the caller's buffer @p out (out FIRST) — the buffer-reuse overload of178
* @ref eigvals (the HOST kernel of the seam pattern; a device extension supplies its own179
* `requires DeviceArray` overload, found by ADL).180
* @param out destination; a contiguous length-n complex vector, overwritten with the descending spectrum.181
* @param a square matrix.182
* @complexity iterative O(n³).183
* @alloc reuses @p out; the Hessenberg + shifted-QR iteration allocates its own scratch.184
* @test LinalgRoutines.FactorizationOutReusesBuffer185
*/186
template <ndarray::Field T, template <typename> class Array>187
requires HostArray<Array<T>> && ndarray::FloatingPoint<T>188
void eigvals(Array<ndarray::complex_of_t<T>>& out, const Array<T>& a);189
/**190
* Symmetric/Hermitian eigendecomposition into the caller's buffers (outs FIRST) — the buffer-reuse191
* overload of @ref eigh, filling @p values and @p vectors instead of allocating a result struct192
* (the HOST kernel of the seam pattern; a device extension supplies its own `requires DeviceArray`193
* overload). ONE two-layer kernel serving the real symmetric AND complex Hermitian paths: values194
* are always the real spectrum, vectors match the input element.195
* @param values destination for the real eigenvalues; a contiguous length-n vector, overwritten.196
* @param vectors destination for the eigenvectors (columns); a contiguous n×n matrix, overwritten.197
* @param a square symmetric (real) / Hermitian (complex) matrix.198
* @complexity iterative O(n³).199
* @alloc reuses @p values and @p vectors (computed into private scratch, then copied in).200
* @test LinalgRoutines.DecompositionOutReusesBuffer201
*/202
template <ndarray::Field T, template <typename> class Array>203
requires HostArray<Array<T>> && ndarray::FloatingPoint<ndarray::real_base_t<T>>204
void eigh(Array<ndarray::real_base_t<T>>& values, Array<T>& vectors, const Array<T>& a);205
/**206
* Symmetric/Hermitian eigenvalues into the caller's buffer @p out (out FIRST) — the buffer-reuse207
* overload of @ref eigvalsh (the HOST kernel of the seam pattern; a device extension supplies its208
* own `requires DeviceArray` overload). One two-layer kernel: the complex Hermitian path is the209
* same template at T = std::complex<double>, still writing the REAL spectrum.210
* @param out destination; a contiguous length-n vector, overwritten with the descending eigenvalues.211
* @param a square symmetric (real) / Hermitian (complex) matrix.212
* @complexity iterative O(n³).213
* @alloc reuses @p out; the tridiagonal-QL solver allocates its own scratch.214
* @test LinalgRoutines.FactorizationOutReusesBuffer215
*/216
template <ndarray::Field T, template <typename> class Array>217
requires HostArray<Array<T>> && ndarray::FloatingPoint<ndarray::real_base_t<T>>218
void eigvalsh(Array<ndarray::real_base_t<T>>& out, const Array<T>& a);219
/**220
* Solve into the caller's buffer @p out (out FIRST) — the buffer-reuse overload of @ref solve.221
* @param out destination; a contiguous length-n vector, overwritten with the solution x.222
* @param a square coefficient matrix.223
* @param b right-hand-side vector.224
* @complexity O(n³).225
* @alloc reuses @p out; the LU factorization allocates its own scratch.226
* @test LinalgRoutines.FactorizationOutReusesBuffer227
*/228
template <ndarray::Field T, template <typename> class Array>229
requires HostArray<Array<T>> && ndarray::FloatingPoint<T>230
void solve(Array<T>& out, const Array<T>& a, const Array<T>& b);231
/**232
* Least-squares solution into the caller's buffer @p out (out FIRST) — the buffer-reuse overload of233
* @ref lstsq. Routes through the @ref matmul out-param so the final product is written into @p out234
* with no allocation.235
* @param out destination; a contiguous array of the solution's shape, overwritten.236
* @param a m×n matrix.237
* @param b right-hand side.238
* @complexity iterative O(n³).239
* @alloc reuses @p out; allocates the intermediate n×m pseudo-inverse and its SVD scratch.240
* @test LinalgRoutines.FactorizationOutReusesBuffer241
*/242
template <ndarray::Field T, template <typename> class Array>243
requires HostArray<Array<T>> && ndarray::FloatingPoint<T>244
void lstsq(Array<T>& out, const Array<T>& a, const Array<T>& b);245
/**246
* Inverse into the caller's buffer @p out (out FIRST) — the buffer-reuse overload of @ref inv.247
* @param out destination; a contiguous n×n matrix, overwritten with A⁻¹.248
* @param a square matrix.249
* @complexity O(n³).250
* @alloc reuses @p out; the LU factorization allocates its own scratch.251
* @test LinalgRoutines.FactorizationOutReusesBuffer252
*/253
template <ndarray::Field T, template <typename> class Array>254
requires HostArray<Array<T>> && ndarray::FloatingPoint<T>255
void inv(Array<T>& out, const Array<T>& a);256
/**257
* Pseudo-inverse into the caller's buffer @p out (out FIRST) — the buffer-reuse overload of258
* @ref pinv.259
* @param out destination; a contiguous n×m matrix (for an m×n input), overwritten with the pseudo-inverse.260
* @param a m×n matrix.261
* @complexity iterative O(n³).262
* @alloc reuses @p out; the Golub–Reinsch SVD allocates its own scratch.263
* @test LinalgRoutines.FactorizationOutReusesBuffer264
*/265
template <ndarray::Field T, template <typename> class Array>266
requires HostArray<Array<T>> && ndarray::FloatingPoint<T>267
void pinv(Array<T>& out, const Array<T>& a);268
template <ndarray::Field T, template <typename> class Array>269
requires HostArray<Array<T>> && ndarray::FloatingPoint<T>270
void det(T& out, const Array<T>& a);271
template <ndarray::Field T, template <typename> class Array>272
requires HostArray<Array<T>> && ndarray::FloatingPoint<T>273
void cond(T& out, const Array<T>& a);274
template <ndarray::Field T, template <typename> class Array>275
requires HostArray<Array<T>> && ndarray::FloatingPoint<T>276
void matrix_rank(long long& out, const Array<T>& a);277
template <ndarray::Field T, template <typename> class Array>278
requires HostArray<Array<T>> && ndarray::FloatingPoint<T>279
void norm(T& out, const Array<T>& a);280
/// @endcond281
/**282
* Integer matrix power Aⁿ (negative n via @ref inv).283
*284
* Requires a square matrix (throws otherwise); n == 0 returns the identity, and285
* negative n first inverts @p a via @ref inv (so it inherits @ref inv's286
* singular-matrix behavior) before raising to |n|.287
* @param a square matrix.288
* @param n exponent.289
* @return Aⁿ.290
* @complexity O(n³·log|n|) by binary exponentiation.291
* @alloc allocates a new NDArray result; the binary-exponentiation @ref matmul steps292
* allocate their own intermediates.293
* @test LinalgRoutines.MatrixPower294
* @crtest LinalgCompileRun.MatrixPower295
* @systest StdlibE2E.Linalg296
*/297
template <ndarray::Field T, template <typename> class Array>298
requires NumericArray<Array<T>> && ndarray::FloatingPoint<T>299
[[nodiscard]] Array<T> matrix_power(const Array<T>& a, long long n) {300
if (a.ndim() != 2) throw std::runtime_error("linalg: expected a 2-D matrix");301
if (a.shape()[0] != a.shape()[1]) throw std::runtime_error("linalg: expected a square matrix");302
Array<T> out = Array<T>::uninitialized({a.shape()[0], a.shape()[0]});303
matrix_power(out, a, n);304
return out;305
}306
// ---- Decompositions ----307
/**308
* Cholesky factor of a symmetric positive-definite matrix (throws otherwise).309
*310
* Requires a square matrix and computes L column by column reading only the311
* lower triangle of @p a; if any pivot (the diagonal under the square root) is312
* non-positive it throws "matrix is not positive-definite", which also catches313
* non-SPD or non-symmetric input.314
* @param a square SPD matrix.315
* @return lower-triangular L with A = L·Lᵀ.316
* @complexity O(n³).317
* @alloc allocates a new NDArray result; the factor is computed into O(n²) private318
* scratch, then copied in.319
* @test LinalgRoutines.CholeskyAndQR320
* @crtest LinalgCompileRun.Cholesky321
* @systest StdlibE2E.Linalg322
*/323
template <ndarray::Field T, template <typename> class Array>324
requires NumericArray<Array<T>> && ndarray::FloatingPoint<T>325
[[nodiscard]] Array<T> cholesky(const Array<T>& a) { // lower-triangular L (A = L Lᵀ)326
if (a.ndim() != 2) throw std::runtime_error("linalg: expected a 2-D matrix");327
if (a.shape()[0] != a.shape()[1]) throw std::runtime_error("linalg: expected a square matrix");328
Array<T> out = Array<T>::uninitialized({a.shape()[0], a.shape()[0]});329
cholesky(out, a);330
return out;331
}332
/** Result of qr(): A = q·r with orthonormal q and upper-triangular r. */333
/// The factors of a QR decomposition. Templated over the container type so a host `qr` yields334
/// `QR<NDArray>` and a device `qr` yields `QR<device_array<T>>`; `ArrT` defaults to `NDArray`335
/// (the host double result), so plain `QR` still names the common host type.336
template <class ArrT = NDArray>337
struct QR {338
ArrT q; ///< Orthonormal columns, m×n (the Q in A = Q·R).339
ArrT r; ///< Upper-triangular factor, n×n (the R in A = Q·R).340
};341
/**342
* Reduced QR via Householder reflections (requires rows ≥ cols).343
*344
* Applies successive Householder reflectors to triangularize @p a, returning the345
* thin/reduced factors; throws "qr requires rows >= cols" for wide matrices.346
* Rank-deficient columns (zero pivot norm) are skipped, leaving the347
* corresponding R entries zero.348
* @param a m×n matrix.349
* @return @ref QR with q (m×n, orthonormal cols) and r (n×n, upper-triangular).350
* @complexity O(m²·n) — n reflectors each applied to the full m×m Q; O(n³) when square.351
* @alloc allocates both members; the factorization works in O(m²) private scratch352
* (a full m×m Q workspace), then copies the reduced factors in.353
* @test LinalgRoutines.CholeskyAndQR354
* @crtest LinalgCompileRun.Qr355
* @systest StdlibE2E.Linalg356
*/357
template <ndarray::Field T, template <typename> class Array>358
requires NumericArray<Array<T>> && ndarray::FloatingPoint<T>359
[[nodiscard]] QR<Array<T>> qr(const Array<T>& a) {360
if (a.ndim() != 2) throw std::runtime_error("linalg: expected a 2-D matrix");361
const std::size_t m = a.shape()[0], n = a.shape()[1];362
if (m < n) throw std::runtime_error("linalg: qr requires rows >= cols");363
QR<Array<T>> out{Array<T>::uninitialized({m, n}), Array<T>::uninitialized({n, n})};364
qr(out.q, out.r, a);365
return out;366
}367
/** Result of svd(): A = u·diag(s)·vh. */368
/// The factors of a singular value decomposition, templated over the container type (`ArrT`369
/// defaults to `NDArray`, the host double result, so plain `SVD` names the common host type).370
template <class ArrT = NDArray>371
struct SVD {372
ArrT u; ///< Left singular vectors, m×n.373
ArrT s; ///< Singular values in descending order (length n).374
ArrT vh; ///< Right singular vectors transposed, n×n (the Vᵀ in A = u·diag(s)·Vᵀ).375
};376
/**377
* Singular value decomposition (Golub–Reinsch; requires rows ≥ cols).378
*379
* Reduces @p a to upper-bidiagonal form by Householder reflections, then diagonalizes380
* it with implicit-shift QR (accumulating U and V), and sorts the singular values381
* descending — the world-standard dense SVD (what LAPACK's dgesvd reduces to). Throws382
* "svd requires rows >= cols" for wide matrices (transpose first); singular values come383
* out non-negative.384
* @param a m×n matrix.385
* @return @ref SVD with u (m×n), s (descending singular values), vh (n×n = Vᵀ).386
* @complexity iterative O(m·n²); O(n³) when square.387
* @alloc allocates all members; the Golub–Reinsch reduction allocates its own O(m·n) workspace.388
* @test LinalgRoutines.SvdAndEigh389
* @crtest LinalgCompileRun.Svd390
* @systest StdlibE2E.Linalg391
*/392
template <ndarray::Field T, template <typename> class Array>393
requires NumericArray<Array<T>> && ndarray::FloatingPoint<T>394
[[nodiscard]] SVD<Array<T>> svd(const Array<T>& a) {395
if (a.ndim() != 2) throw std::runtime_error("linalg: expected a 2-D matrix");396
const std::size_t m = a.shape()[0], n = a.shape()[1];397
if (m < n) throw std::runtime_error("linalg: svd requires rows >= cols (transpose otherwise)");398
SVD<Array<T>> out{Array<T>::uninitialized({m, n}), Array<T>::uninitialized({n}),399
Array<T>::uninitialized({n, n})};400
svd(out.u, out.s, out.vh, a);401
return out;402
}403
/**404
* Singular values only (≈ `numpy.linalg.svd(a, compute_uv=False)` / `svdvals`).405
*406
* Runs the same Golub–Reinsch reduction as @ref svd but takes the **values-only** fast407
* path — it never accumulates U or V, and skips the U/V Givens rotations in the QR408
* sweep — so it beats the full decomposition by a margin that grows with n (see the409
* linalg benchmark page). Accepts any shape (singular values of `a` and `aᵀ` coincide).410
* @param a m×n matrix.411
* @return length-min(m,n) vector of singular values, descending.412
* @complexity iterative O(n³), but a large constant factor below @ref svd.413
* @alloc allocates a new NDArray result; the reduction still allocates its O(m·n)414
* workspace (the values-only path skips the U/V accumulation work and result415
* copies, not the working buffers).416
* @test LinalgRoutines.SvdAndEigh417
* @crtest LinalgCompileRun.Svdvals418
* @systest StdlibE2E.Linalg419
*/420
template <ndarray::Field T, template <typename> class Array>421
requires NumericArray<Array<T>> && ndarray::FloatingPoint<T>422
[[nodiscard]] Array<T> svdvals(const Array<T>& a) {423
if (a.ndim() != 2) throw std::runtime_error("linalg: expected a 2-D matrix");424
const std::size_t m = a.shape()[0], n = a.shape()[1];425
Array<T> out = Array<T>::uninitialized({m < n ? m : n});426
svdvals(out, a);427
return out;428
}430
// ---- Matrix eigenvalues ----431
/** Result of eigh(): a real spectrum — column j of vectors is the eigenvector for values[j]. */432
template <class ArrT = NDArray>433
struct Eig {434
ArrT values; ///< Eigenvalues (length n), real.435
ArrT vectors; ///< Eigenvectors as columns: column j matches values[j] (empty if not computed).436
};437
/**438
* Result of the general eig(): a **complex** spectrum, since a real matrix can have439
* complex conjugate eigenvalue pairs. Column j of vectors is the eigenvector for values[j].440
*/441
template <class ArrT = CNDArray>442
struct EigC {443
ArrT values; ///< Eigenvalues (length n), complex.444
ArrT vectors; ///< Eigenvectors as columns: column j matches values[j].445
};446
/// Result of the complex Hermitian eigh(): **real** eigenvalues with **complex** eigenvectors —447
/// so the two members have DIFFERENT container/element types (`ValsT` real, `VecsT` complex).448
/// Defaults `NDArray`/`CNDArray` are the host result, so plain `EighC` names the common host type.449
template <class ValsT = NDArray, class VecsT = CNDArray>450
struct EighC {451
ValsT values; ///< Eigenvalues (length n), real and descending.452
VecsT vectors; ///< Eigenvectors as columns: column j matches values[j].453
};454
/// The result type of the unified @ref eigh: for a real element, `Eig<Array<T>>` (real values +455
/// vectors); for a complex element, `EighC<Array<real>, Array<T>>` (real values, complex vectors).456
/// eigh's return type differs per element, so it is expressed here (not `auto`) so the header457
/// declaration knows it without seeing the definition.458
template <ndarray::Field T, template <typename> class Array>459
using eigh_result_t = std::conditional_t<ndarray::is_complex_v<T>,460
EighC<Array<ndarray::real_base_t<T>>, Array<T>>,461
Eig<Array<T>>>;462
/**463
* Eigen-decomposition of a general square matrix (**complex** spectrum and464
* eigenvectors).465
*466
* For a symmetric @p a it delegates to @ref eigh (promoted to complex with zero467
* imaginary part); otherwise it uses Hessenberg reduction + shifted QR for the468
* eigenvalues, then **inverse iteration** for each eigenvector. A real matrix with a469
* complex conjugate pair (e.g. a rotation) yields those complex eigenvalues and470
* eigenvectors rather than throwing. Throws on a non-square matrix or if the QR471
* iteration fails to converge.472
* @param a square matrix.473
* @return @ref EigC with complex values and matching complex eigenvector columns.474
* @complexity iterative O(n³) via Hessenberg + shifted QR for the eigenvalues; the475
* general (non-symmetric) eigenvectors add O(n⁴) — one inverse iteration, each476
* with its own O(n³) complex LU factorization, per eigenvalue (a symmetric @p a477
* stays O(n³) via @ref eigh, which accumulates the vectors in the QL sweep).478
* @alloc allocates both members; plus O(n²) factorization scratch (on the non-symmetric479
* path, a fresh complex n×n LU per eigenvalue).480
* @test LinalgRoutines.GeneralEig481
* @crtest LinalgCompileRun.Eig482
* @systest StdlibE2E.Linalg483
*/484
template <ndarray::Field T, template <typename> class Array>485
requires NumericArray<Array<T>> && ndarray::FloatingPoint<T>486
[[nodiscard]] EigC<Array<ndarray::complex_of_t<T>>> eig(const Array<T>& a) { // general square matrix487
if (a.ndim() != 2) throw std::runtime_error("linalg: expected a 2-D matrix");488
if (a.shape()[0] != a.shape()[1]) throw std::runtime_error("linalg: expected a square matrix");489
const std::size_t n = a.shape()[0];490
using C = Array<ndarray::complex_of_t<T>>;491
EigC<C> out{C::uninitialized({n}), C::uninitialized({n, n})};492
eig(out.values, out.vectors, a);493
return out;494
}495
/**496
* Eigenvalues of a general square matrix (**complex**), descending.497
*498
* Routes symmetric input through tridiagonal QL and everything else through499
* Hessenberg + shifted QR, then sorts the result descending (by real part, then by500
* imaginary part). A real matrix with a complex conjugate pair yields those complex501
* eigenvalues rather than throwing. Throws on a non-square matrix or non-convergence502
* of the QR iteration.503
* @param a square matrix.504
* @return length-n complex vector of eigenvalues.505
* @complexity iterative O(n³).506
* @alloc allocates a new CNDArray result; the iteration allocates its own O(n²) scratch.507
* @test LinalgRoutines.SvdAndEigh508
* @crtest LinalgCompileRun.Eigvals509
* @systest StdlibE2E.Linalg510
*/511
template <ndarray::Field T, template <typename> class Array>512
requires NumericArray<Array<T>> && ndarray::FloatingPoint<T>513
[[nodiscard]] Array<ndarray::complex_of_t<T>> eigvals(const Array<T>& a) {514
if (a.ndim() != 2) throw std::runtime_error("linalg: expected a 2-D matrix");515
if (a.shape()[0] != a.shape()[1]) throw std::runtime_error("linalg: expected a square matrix");516
Array<ndarray::complex_of_t<T>> out =517
Array<ndarray::complex_of_t<T>>::uninitialized({a.shape()[0]});518
eigvals(out, a);519
return out;520
}521
/**522
* Eigen-decomposition of a symmetric matrix (Householder tridiagonalization + QL).523
*524
* Reduces @p a to tridiagonal form by Householder reflections, then diagonalizes it525
* with implicit-shift QL, returning real eigenvalues sorted descending with matching526
* eigenvector columns; it reads the full matrix and assumes symmetry rather than527
* checking it, so asymmetric input yields meaningless results. Throws on a non-square528
* matrix (or if the QL iteration fails to converge).529
* @param a square symmetric matrix.530
* @return @ref Eig (real input) or @ref EighC (complex Hermitian input): descending real values and matching eigenvector columns.531
* @complexity iterative O(n³).532
* @alloc allocates both members; the solver allocates its own O(n²) scratch (a complex533
* Hermitian input first embeds into a 2n×2n real matrix).534
* @test LinalgRoutines.SvdAndEigh535
* @test LinalgRoutines.ComplexHermitianEigh536
* @crtest LinalgCompileRun.Eigh537
* @crtest LinalgCompileRun.EighComplex538
* @systest StdlibE2E.Linalg539
* @systest StdlibE2E.LinalgComplex540
*/541
template <ndarray::Field T, template <typename> class Array>542
requires NumericArray<Array<T>> && ndarray::FloatingPoint<ndarray::real_base_t<T>>543
[[nodiscard]] eigh_result_t<T, Array> eigh(const Array<T>& a) { // symmetric / Hermitian544
if (a.ndim() != 2) throw std::runtime_error("linalg: expected a 2-D matrix");545
if (a.shape()[0] != a.shape()[1]) throw std::runtime_error("linalg: expected a square matrix");546
const std::size_t n = a.shape()[0];547
eigh_result_t<T, Array> out{Array<ndarray::real_base_t<T>>::uninitialized({n}),548
Array<T>::uninitialized({n, n})};549
eigh(out.values, out.vectors, a);550
return out;551
}552
/**553
* Eigenvalues of a symmetric matrix, descending (tridiagonal QL).554
*555
* Same tridiagonalization + QL as @ref eigh but **skips the eigenvector accumulation556
* entirely**, so it runs well under `eigh`'s cost at large n (see the linalg benchmark page); assumes557
* (does not verify) symmetry and throws on a non-square matrix.558
* ONE two-layer template collapsing the former real and complex (Hermitian) overloads: a real559
* element takes the symmetric path, a complex element the Hermitian path (`if constexpr`). The560
* spectrum is always REAL, returned as `Array<real_base_t<T>>`.561
* @tparam T the element type (`double` or `std::complex<double>`).562
* @tparam Array the container.563
* @param a square symmetric (real) / Hermitian (complex) matrix.564
* @return length-n vector of real eigenvalues.565
* @complexity iterative O(n³).566
* @alloc allocates a new result; the solver allocates its own O(n²) scratch (a complex567
* Hermitian input first embeds into a 2n×2n real matrix).568
* @test LinalgRoutines.EigvalshSymmetric569
* @test LinalgRoutines.ComplexHermitianEigh570
* @crtest LinalgCompileRun.Eigvalsh571
* @crtest LinalgCompileRun.EigvalshComplex572
* @systest StdlibE2E.Linalg573
* @systest StdlibE2E.LinalgComplex574
*/575
template <ndarray::Field T, template <typename> class Array>576
requires NumericArray<Array<T>> && ndarray::FloatingPoint<ndarray::real_base_t<T>>577
[[nodiscard]] Array<ndarray::real_base_t<T>> eigvalsh(const Array<T>& a) {578
if (a.ndim() != 2) throw std::runtime_error("linalg: expected a 2-D matrix");579
if (a.shape()[0] != a.shape()[1]) throw std::runtime_error("linalg: expected a square matrix");580
Array<ndarray::real_base_t<T>> out =581
Array<ndarray::real_base_t<T>>::uninitialized({a.shape()[0]});582
eigvalsh(out, a);583
return out;584
}585
// EighC (real values + complex vectors) and the unified two-layer `eigh` are declared above with586
// the other eig-family structs; the complex Hermitian eigh is that template at T = complex<double>,587
// returning EighC<NDArray, CNDArray> via the if-constexpr Hermitian branch — and its buffer-reuse588
// form is the SAME two-layer eigh kernel above at T = complex<double> (values NDArray&, vectors589
// CNDArray&). Likewise complex eigvalsh is the eigvalsh kernel at T = complex<double>.590
// ---- Norms and other numbers ----591
/**592
* Norm: L2 for vectors, Frobenius for matrices.593
*594
* Dispatches on rank: 1-D (or lower) inputs get the Euclidean L2 norm, 2-D595
* inputs the Frobenius norm; either way it is the square root of the sum of596
* squared entries. Two-layer over the element and container like every routine (the scalar-out597
* kernel `norm(out, a)` is the host/device seam; host `double` is the shipped instantiation).598
* @param a vector or matrix.599
* @return √Σ xᵢ².600
* @complexity O(n) for vectors / O(n²) for matrices.601
* @alloc none for a contiguous operand (summed in place); a non-contiguous view packs602
* once. Returns a double.603
* @test LinalgRoutines.NormAndRank604
* @crtest LinalgCompileRun.Norm605
* @systest StdlibE2E.Linalg606
*/607
template <ndarray::Field T, template <typename> class Array>608
requires NumericArray<Array<T>> && ndarray::FloatingPoint<T>609
[[nodiscard]] T norm(const Array<T>& a) { // default: Frobenius / L2610
T out;611
norm(out, a);612
return out;613
}614
/**615
* 2-norm condition number σ_max/σ_min (∞ if singular).616
*617
* Takes the ratio of largest to smallest singular value from a Golub–Reinsch SVD618
* (transposing internally for wide matrices); returns +infinity when the619
* smallest singular value is exactly zero (singular/rank-deficient).620
* @param a matrix.621
* @return condition number.622
* @complexity iterative O(n³) via SVD.623
* @alloc allocates scratch O(n²) for the factorization; returns a double.624
* @test LinalgRoutines.SlogdetAndCond625
* @crtest LinalgCompileRun.Cond626
* @systest StdlibE2E.Linalg627
*/628
template <ndarray::Field T, template <typename> class Array>629
requires NumericArray<Array<T>> && ndarray::FloatingPoint<T>630
[[nodiscard]] T cond(const Array<T>& a) {631
if (a.ndim() != 2) throw std::runtime_error("linalg: expected a 2-D matrix");632
T out;633
cond(out, a);634
return out;635
}636
/**637
* Determinant via LU with partial pivoting.638
*639
* Computes the product of the LU pivots times the permutation sign; requires a640
* square matrix (throws otherwise). A singular matrix yields a determinant of641
* (or extremely near) zero rather than an error.642
* @param a square matrix.643
* @return det(A).644
* @complexity O(n³).645
* @alloc allocates scratch O(n²) for the factorization; returns a double.646
* @test LinalgRoutines.SolveDetInv647
* @crtest LinalgCompileRun.Det648
* @systest StdlibE2E.Linalg649
*/650
template <ndarray::Field T, template <typename> class Array>651
requires NumericArray<Array<T>> && ndarray::FloatingPoint<T>652
[[nodiscard]] T det(const Array<T>& a) {653
if (a.ndim() != 2) throw std::runtime_error("linalg: expected a 2-D matrix");654
if (a.shape()[0] != a.shape()[1]) throw std::runtime_error("linalg: expected a square matrix");655
T out;656
det(out, a);657
return out;658
}659
/**660
* Numerical rank from SVD singular-value thresholding.661
*662
* Counts singular values above a tolerance scaled by the largest singular value663
* and the matrix size (the standard numpy-style threshold); accepts any shape,664
* transposing wide matrices internally.665
* @param a matrix.666
* @return rank.667
* @complexity iterative O(n³) via SVD.668
* @alloc allocates scratch O(n²) for the factorization.669
* @test LinalgRoutines.NormAndRank670
* @crtest LinalgCompileRun.MatrixRank671
* @systest StdlibE2E.Linalg672
*/673
template <ndarray::Field T, template <typename> class Array>674
requires NumericArray<Array<T>> && ndarray::FloatingPoint<T>675
[[nodiscard]] long long matrix_rank(const Array<T>& a) {676
if (a.ndim() != 2) throw std::runtime_error("linalg: expected a 2-D matrix");677
long long out = 0;678
matrix_rank(out, a);679
return out;680
}681
/** Result of slogdet(): det(A) = sign·exp(logabsdet). */682
struct SLogDet {683
double sign; ///< Sign of the determinant (−1 or +1; a singular matrix keeps ±1, see logabsdet).684
double logabsdet; ///< Natural log of |det(A)|, so det(A) = sign·exp(logabsdet).685
};686
/// @cond INTERNAL687
/// the scalar-out HOST kernel of the seam pattern (declared after @ref SLogDet,688
/// which its out-parameter needs; a device extension supplies its own DeviceArray overload).689
template <ndarray::Field T, template <typename> class Array>690
requires HostArray<Array<T>> && ndarray::FloatingPoint<T>691
void slogdet(SLogDet& out, const Array<T>& a);692
/// @endcond693
/**694
* Sign and log|det| via LU (overflow-safe determinant).695
*696
* Sums the logs of the absolute LU pivots (avoiding the over/underflow of a raw697
* product) and tracks the sign from the pivot signs and permutation parity;698
* requires a square matrix (throws otherwise). A singular matrix gives a hugely699
* negative logabsdet rather than −infinity, since a zero pivot is nudged to a700
* tiny value during factorization.701
* @param a square matrix.702
* @return @ref SLogDet.703
* @complexity O(n³).704
* @alloc allocates scratch O(n²) for the factorization (the struct members are plain doubles).705
* @test LinalgRoutines.SlogdetAndCond706
* @crtest LinalgCompileRun.Slogdet707
* @systest StdlibE2E.Linalg708
*/709
template <ndarray::Field T, template <typename> class Array>710
requires NumericArray<Array<T>> && ndarray::FloatingPoint<T>711
[[nodiscard]] SLogDet slogdet(const Array<T>& a) {712
if (a.ndim() != 2) throw std::runtime_error("linalg: expected a 2-D matrix");713
if (a.shape()[0] != a.shape()[1]) throw std::runtime_error("linalg: expected a square matrix");714
SLogDet out{};715
slogdet(out, a);716
return out;717
}718
// Trace — the allocating front `trace(a)` and the scalar-out kernel `trace(out, a)` are the719
// backend.hpp reduction pattern (host kernel here in routines.cpp; a device extension adds its720
// own DeviceArray overload, found by ADL).722
// ---- Solving equations and inverting matrices ----723
/**724
* Solve A·x = b via LU with partial pivoting.725
*726
* Factorizes @p a once then does forward/back substitution against @p b;727
* requires @p a square and @p b a vector of matching length (throws otherwise).728
* A singular @p a does not throw but yields a garbage/overflowing solution729
* (pivots are nudged off zero rather than detected).730
* @param a square coefficient matrix.731
* @param b right-hand-side vector.732
* @return solution x.733
* @complexity O(n³).734
* @alloc allocates a new NDArray result; the LU factorization allocates its own O(n²) scratch.735
* @test LinalgRoutines.SolveDetInv736
* @crtest LinalgCompileRun.Solve737
* @systest StdlibE2E.Linalg738
*/739
template <ndarray::Field T, template <typename> class Array>740
requires NumericArray<Array<T>> && ndarray::FloatingPoint<T>741
[[nodiscard]] Array<T> solve(const Array<T>& a, const Array<T>& b) { // A x = b742
if (a.ndim() != 2) throw std::runtime_error("linalg: expected a 2-D matrix");743
if (a.shape()[0] != a.shape()[1]) throw std::runtime_error("linalg: expected a square matrix");744
if (vector_len(b) != a.shape()[0]) throw std::runtime_error("linalg: solve dimension mismatch");745
Array<T> out = Array<T>::uninitialized({a.shape()[0]});746
solve(out, a, b);747
return out;748
}749
/**750
* Least-squares solution min‖A·x − b‖ (computed as @ref pinv (a)·b).751
*752
* Forms the Moore–Penrose pseudo-inverse via SVD and multiplies it by @p b, so753
* it handles over- and under-determined systems and returns the minimum-norm754
* solution for rank-deficient @p a; @p b must be conformable for the755
* @ref matmul step.756
* @param a m×n matrix.757
* @param b right-hand side.758
* @return minimizing x.759
* @complexity iterative O(n³) via SVD.760
* @alloc allocates a new NDArray result; plus the intermediate n×m pseudo-inverse and761
* its SVD scratch.762
* @test LinalgRoutines.Lstsq763
* @crtest LinalgCompileRun.Lstsq764
* @systest StdlibE2E.Linalg765
*/766
template <ndarray::Field T, template <typename> class Array>767
requires NumericArray<Array<T>> && ndarray::FloatingPoint<T>768
[[nodiscard]] Array<T> lstsq(const Array<T>& a, const Array<T>& b) { // least-squares solution769
if (a.ndim() != 2) throw std::runtime_error("linalg: expected a 2-D matrix");770
if (b.ndim() != 2) throw std::runtime_error("linalg: matmul expects 2-D matrices");771
if (a.shape()[0] != b.shape()[0])772
throw std::runtime_error("linalg: matmul inner dimension mismatch");773
Array<T> out = Array<T>::uninitialized({a.shape()[1], b.shape()[1]});774
lstsq(out, a, b);775
return out;776
}777
/**778
* Matrix inverse via LU with partial pivoting.779
*780
* Factorizes @p a once and back-solves against each identity column; requires a781
* square matrix (throws otherwise). A singular @p a does not throw but produces782
* garbage/overflowing entries since zero pivots are nudged rather than detected.783
* @param a square matrix.784
* @return A⁻¹.785
* @complexity O(n³).786
* @alloc allocates a new NDArray result; the LU factorization and the whole-identity787
* back-solve allocate their own O(n²) scratch.788
* @test LinalgRoutines.SolveDetInv789
* @crtest LinalgCompileRun.Inv790
* @systest StdlibE2E.Linalg791
*/792
template <ndarray::Field T, template <typename> class Array>793
requires NumericArray<Array<T>> && ndarray::FloatingPoint<T>794
[[nodiscard]] Array<T> inv(const Array<T>& a) {795
if (a.ndim() != 2) throw std::runtime_error("linalg: expected a 2-D matrix");796
if (a.shape()[0] != a.shape()[1]) throw std::runtime_error("linalg: expected a square matrix");797
Array<T> out = Array<T>::uninitialized({a.shape()[0], a.shape()[0]});798
inv(out, a);799
return out;800
}801
/**802
* Moore–Penrose pseudo-inverse via SVD (any shape).803
*804
* Computes V·diag(1/σ)·Uᵀ from a Golub–Reinsch SVD, transposing wide matrices805
* internally so any shape works; singular values at or below a size-scaled806
* tolerance are dropped (treated as zero) so it stays well-defined for807
* rank-deficient input.808
* @param a m×n matrix.809
* @return n×m pseudo-inverse.810
* @complexity iterative O(n³) via SVD.811
* @alloc allocates a new NDArray result; the SVD and the assembly allocate their own812
* O(m·n) scratch.813
* @test LinalgRoutines.PinvCondRankOnWideMatrix814
* @crtest LinalgCompileRun.Pinv815
* @systest StdlibE2E.Linalg816
*/817
template <ndarray::Field T, template <typename> class Array>818
requires NumericArray<Array<T>> && ndarray::FloatingPoint<T>819
[[nodiscard]] Array<T> pinv(const Array<T>& a) { // Moore–Penrose pseudo-inverse820
if (a.ndim() != 2) throw std::runtime_error("linalg: expected a 2-D matrix");821
Array<T> out = Array<T>::uninitialized({a.shape()[1], a.shape()[0]});822
pinv(out, a);823
return out;824
}826
} // namespace cheatah::linalg