# Dense Linear Algebra

`densematrix.h`/`densematrix.cpp` provide `VectorView`, `DenseMatrixView`, and `DenseMatrix` - a small dense linear algebra layer used where a circuit-sized sparse solve (see [KLU Matrix](klumatrix.md)) is not the right tool: small per-block computations such as harmonic-balance mixing, Jacobian condition/inversion diagnostics, and other fixed-size dense algebra. All three are templates over an element type `T` (in practice `double`, `Complex`, or, for storage-only uses, `Int`); the double/`Complex` instantiations dispatch their inner loops to BLAS/LAPACK (see [BLAS/LAPACK Raw Prototypes](blaslapack.md)) wherever the routine's storage requirements are met, falling back to a plain C++ loop otherwise (used unconditionally for any other `T`, e.g. `Int`).

## `VectorView<T>`

A non-owning, strided view into a contiguous buffer (another vector, or a row/column of a matrix). `start_`/`n_`/`stride_` are all it holds, so a `VectorView` can address a matrix row, a matrix column, or an arbitrary strided sub-vector uniformly - `at(i)`/`operator[](i)` both compute `*(start_+i*stride_)`.

### Construction

Besides the plain `VectorView(T* start, size_t n, size_t stride)` constructor, several convenience overloads build a view directly from a `std::vector<T>&`: the whole vector (offset 0, stride 1), a contiguous prefix of it (`VectorView(v, length)`), or an arbitrary offset/length/stride slice. Deduction guides let callers write `VectorView(v)`/`VectorView(v, offset, length, stride)`/`VectorView(start, n, stride)` without spelling out `<T>`. `subVector(offset, n, stride=1)` derives a further, shorter view from an existing one, composing the new offset/stride against the parent's own `stride_` rather than requiring the caller to do that arithmetic by hand.

### Methods

Its methods split cleanly by dispatch:

- **BLAS-backed for `double`/`Complex`**: `operator=(VectorView)` (copy, via `dcopy_`/`zcopy_`), `operator=(T)` (broadcast, via `dlaset_`/`zlaset_` with `m=1`), `norm2()`/`norm()` (via `dnrm2_`/`dznrm2_`, squared by hand for `norm2()`), `dot()` (via `ddot_`/`zdotc_` - the complex form conjugates `other`, matching `zdotc_`'s "conjugate the first vector" convention), `orthogonalize()` (via `daxpy_`/`zaxpy_`, on top of `dot()`/`norm2()`), `swap()` (via `dswap_`/`zswap_`), `scale()` (via `dscal_`/`zscal_`), `addScaled()` (`v += other*factor`, via `daxpy_`/`zaxpy_`), `vectorPlusScaledVector()` (`v = v0 + other*factor`, via `dcopy_`+`daxpy_`/`zcopy_`+`zaxpy_`), `add()` (`v += other`, `daxpy_`/`zaxpy_` with `factor=1`), `scaledVector()` (`v = other*factor`, via `dcopy_`+`dscal_`/`zcopy_`+`zscal_`), and `maxAbs()` for `double` (via `idamax_`).
- A **`Complex`-only overload of `scale(double factor)`** (SFINAE-gated via `enable_if_t`, coexisting with the general `scale(T factor)` above) scales a complex vector by a real factor through `zdscal_` instead of promoting `factor` to `Complex` and going through `zscal_`'s complex-complex multiply - see [BLAS/LAPACK Raw Prototypes](blaslapack.md).
- **Plain-loop fallback** for any other `T`, and unconditionally for `apply()` (elementwise function application, no BLAS equivalent) and `swap(i, j)` (swap two elements by index within the same view, too small to dispatch to BLAS).
- **`maxAbs()` for `Complex`** is deliberately *not* BLAS-backed: LAPACK's `izamax_` picks the maximal element by `|Re|+|Im|` rather than true modulus, which can select a different element (off by up to a factor of sqrt(2)) than a `std::abs()`-based comparison. Since no live call site needs the `Complex` case today, the code keeps the exact-modulus loop rather than switching to the cheaper but subtly different LAPACK semantics.

## `DenseMatrixView<T>`

A non-owning view over a 2D region: `start_`, `nRow_`/`nCol_`, and independent `rowStride_`/`colStride_`, so the same class represents both row-major and column-major layouts (and arbitrary sub-block views) by choosing which stride is 1. `row(i)`/`column(i)` return a `VectorView<T>` over that row/column - the mechanism by which all the BLAS dispatch above becomes available for matrix rows and columns too. `diagonal()` similarly returns a `VectorView<T>` of `min(nRow_,nCol_)` elements with stride `rowStride_+colStride_` (one step down and one step across per element), so `M.diagonal() = val` fills just the diagonal via the same `VectorView::operator=(T)` broadcast. `ptrOf(row, col)` returns a raw pointer to an element, for building a truncated sub-row/column `VectorView` by hand where the `row()`/`column()` accessors don't fit.

### Element-wise and algebraic operations

`operator=` (view-to-view and scalar broadcast), `zero()`, `identity()`, `multiply()`/`multiplyAdd()` (matrix-vector), `multiply()` (matrix-matrix, built from `row(i).dot(column(j))` - no BLAS GEMM call), `scaledMatrix()`/`addScaledMatrix()`/`addMatrix()`/`subtractMatrix()`, `scaledRows()`/`addScaledRows()`/`scaledColumns()`/`addScaledColumns()` (row-/column-wise scale by a per-row/per-column factor from a `VectorView`), `apply()`, and `maxAbs()` round out the view's algebra. The matrix-matrix product and the row/column-scaling family are built out of per-row/column `VectorView` operations (`dot()`, `scaledVector()`, `addScaled()`), which is where the BLAS dispatch actually happens; three operations dispatch to BLAS/LAPACK directly instead:

- **`operator=(DenseMatrixView)`** (view-to-view copy) calls `dlacpy_`/`zlacpy_` for `T=double`/`Complex` whenever both views are contiguous in the *same* direction (both native column-major, i.e. `rowStride_==1` on both sides, or both row-major read as transposed column-major, i.e. `colStride_==1` on both sides) - a mismatched-major pair (this row-major, other column-major or vice versa) falls through to the per-row loop, since a positional element copy isn't transpose-symmetric the way a uniform fill is.
- **`identity()`**/**scalar `operator=`** go through a private `laset()` helper that calls `dlaset_`/`zlaset_` whenever the view is contiguous in either direction (`rowStride_==1` or `colStride_==1`; row-major is handled by reinterpreting the view as its transpose in LAPACK's column-major terms, valid here since uniform/diagonal fill is symmetric under transpose), falling back to a per-element or per-row loop otherwise.
- **`maxAbs()`** calls `dlange_`/`zlange_` (`norm='M'`, true complex modulus, unlike `izamax_`) under the same contiguity condition as `laset()`, falling back to a row-wise max of `VectorView::maxAbs()` otherwise.

All three LAPACK fast paths clamp their computed leading dimension to `max(lda, m, 1)`, since LAPACK requires `lda>=max(1,m)` and a degenerate `nRow_==1`/`nCol_==1`/`nRow_==0`/`nCol_==0` view (e.g. mid-`addRow()`-growth, or a genuinely single-row/column block) can otherwise report a smaller or zero value.

### Factorization and solving

`DenseMatrixView` performs LU factorization and triangular solves LAPACK-style, with an explicit fallback for cases LAPACK's calling convention cannot handle.

- **`factor(rowPerm)`** - in-place LU decomposition with partial pivoting (`A := P*L*U`, unit-diagonal `L` below the diagonal, `U` on and above it, both stored back into the matrix). `rowPerm` receives the pivot sequence in LAPACK's own 1-based `ipiv` convention (row `i` was interchanged with row `ipiv[i]` at step `i`), so it doubles as scratch for `dgetrf_`/`zgetrf_` when the LAPACK fast path is taken.
- **`luSolve(rhs, rowPerm)`** - solves `Ax=rhs` given a factorization already produced by `factor()`, permuting `rhs` in place and then doing forward/backward substitution (or calling `dgetrs_`/`zgetrs_`).
- **`factorAndLuSolve(rhs, rowPerm=nullptr)`** - factor-then-solve in one call, for both a single right-hand-side vector and a right-hand-side matrix (multiple columns solved together). `rowPerm` is optional scratch; without it there is nowhere to store `ipiv`, so even an otherwise LAPACK-compatible matrix falls back to the manual path.
- **`factorAndInvert(result, rowPerm=nullptr)`** - destructive inversion, implemented as `result.identity(); factorAndLuSolve(result, rowPerm)` (i.e. solving `A*X=I`) rather than duplicating the factor/solve dispatch logic.

**LAPACK dispatch condition**: the LAPACK fast path is only taken for `T=double`/`T=Complex` *and* when every buffer involved is contiguous in the dimension LAPACK requires - `rowStride_==1` for the matrix (LAPACK is column-major and has no per-element increment parameter, only a leading dimension between columns), and likewise `stride()==1` for `rowPerm` and for the right-hand side. When any of those strides is not 1 (e.g. a view into a row-major sub-block), the code falls back to `luSolveCore()`, a hand-written Gaussian elimination with partial pivoting that mirrors LAPACK's `ipiv` convention exactly (so `rowPerm`, once filled, is interchangeable between the two code paths) but works through arbitrary `VectorView`/`DenseMatrixView` strides instead of requiring contiguity. `luSolveCore()` is a single template shared by the vector-rhs and matrix-rhs cases (dispatched via `if constexpr` on `RhsType`) and by the strideless `factor()`-only case (`rhs==nullptr`).

All of `factor()`/`luSolve()`/`factorAndLuSolve()` guard their size preconditions (non-square matrix, wrong-length `rhs`/`rowPerm`) with `DBGCHECK` (see [Namespace and Types](common.md)) rather than an unconditional throw - a `std::runtime_error` on mismatch in debug builds, compiled out entirely in release, since these are caller bugs the class assumes are already excluded by construction. `std::invalid_argument` is still thrown unconditionally if LAPACK itself reports an illegal argument (`info<0`) - that failure mode can't be checked for in advance the way a length mismatch can. A singular matrix (`info>0` from `dgetrf_`/`zgetrf_`, or a zero pivot in the manual path) is reported through the `bool` return value instead, `false` meaning "factorization/solve did not succeed." This same `DBGCHECK`-for-preconditions pattern applies throughout `VectorView`/`DenseMatrixView` (length/shape mismatches on `operator=`, `dot()`, `swap()`, `addScaled()`, `multiply()`, and the rest), not just the factorization methods - it used to be an unconditional `std::out_of_range` throw everywhere.

## `DenseMatrix<T>`

`DenseMatrix<T>` is the owning matrix: it holds its own `std::vector<T> data_` and derives from `DenseMatrixView<T>`, keeping the view's `start_`/`nRow_`/`nCol_`/strides pointed at that buffer.

- **`Major`** (`Row` or `Column`) selects storage order at construction/resize time; `setStride()` sets `rowStride_`/`colStride_` accordingly (`rowStride_=nCol_, colStride_=1` for row-major; `rowStride_=1, colStride_=nRow_` for column-major). Column-major is what the LAPACK fast paths above require, since `rowStride_==1` is their entry condition.
- Copy/move construction and assignment all re-point `start_` at the (copied or moved-from) `data_` and recompute strides - a `DenseMatrix` cannot be memcpy'd or trivially relocated because of this internal self-reference.
- Construction from an existing `std::vector<T>` (`DenseMatrix(std::vector<T>&& from, nRow, nCol, major)` and the `const&` copy-construct overload) validates `nRow*nCol == from.size()` via `DBGCHECK` (debug-only, see "Factorization and solving" above).
- **`resize(nRow, nCol, major)`** reallocates and re-derives strides but does **not** preserve/reorder existing content - it is a fresh-start resize, not a growth operation.
- **`addRow()`** grows the matrix by one row and returns a `VectorView` over the new row. For a row-major matrix this is a cheap tail-resize (`data_.resize()`). For a column-major matrix, every existing column has to be restrided, so it reshapes into a fresh buffer instead, copying each old `nRow_`-element column block into the corresponding `(nRow_+1)`-element block of the new one (the new row's slot in each block is left value-initialized, i.e. zero).
- **`indexOf(row, col)`** exposes the flat index into `data()` for a given `(row, col)`, matching whichever `Major` is currently in effect - useful for code that wants to hand `data()` straight to an external routine (e.g. as LAPACK's `A` argument) alongside its own indexing.
- **`view()`** returns a `DenseMatrixView<T>` over the same data (same `start_`/strides), for call sites that want the base class's plain view-to-view `operator=(const DenseMatrixView<T>&)` (copy values only) instead of `DenseMatrix`'s own copy assignment (which also overwrites `major_`). Spelling `DenseMatrixView<T>(mat) = other;` directly instead would most-vexing-parse as a variable declaration when `mat` already names a variable in scope, hence the named method instead of an inline cast.

## Self-test

`DenseMatrix<T>::test()` (instantiated for `double`, `Complex`, and `Int`; a no-op returning `true` for any other `T` via `if constexpr`) exercises small- and generic-matrix inversion, matrix-matrix multiplication, and their consistency (`A * inverse(A) == I`) against hand-computed and diagonal matrices; `identity()` on non-square matrices and on padded/strided sub-block views (both the native-column-major and row-major-as-transpose `laset()` branches, sentinel-filled beforehand so any out-of-block spill is caught); and the matrix-level helpers `view()`, `diagonal()`, `scaledMatrix()`, `addScaledMatrix()`, and `addMatrix()`/`subtractMatrix()` (round-tripped against each other). It prints intermediate matrices and reports pass/fail via its `bool` return value - a manually-invoked sanity check, not part of an automated test harness reachable from this header alone.
