# KLU Block-Sparse Matrix

`klubsmatrix.h`/`klubsmatrix.cpp` provide `KluBlockSparseMatrixCore`, a matrix whose sparsity pattern is a grid of `n x n` dense `nbRow x nbCol` blocks rather than individual scalars. It reuses [KLU Matrix](klumatrix.md)'s `KluMatrixCore` for storage and factorization (each block simply expands into `nbRow*nbCol` scalar entries of the underlying compressed-column matrix) and adds block-level addressing on top. It is used by harmonic-balance analysis (the `anhb.h`/`anhbac.h`/`corehb.h`/`corehbnr.h`/`corehbac.h` cores) to store, per pair of coupled unknowns, a dense frequency- or time-domain sub-Jacobian.

## Relationship to `KluMatrixCore`

`KluBlockSparseMatrixCore<IndexType, ValueType>` derives from `KluMatrixCore<IndexType, ValueType>` and separately implements `MatrixAccess<IndexType>` (rather than going through `KluAtomicMatrix`, which is reserved for the one-scalar-per-position case; see [KLU Matrix](klumatrix.md)). It pulls in the protected members it needs (`smap`, `nnz_`, `AN`, `AP`, `AI`, `Ax`, `lastError`, `common`, `symbolic`, `bucket_`) via `using` declarations and drives them directly, building its own `AP`/`AI` from the block structure instead of from `SparsityMap` positions one-to-one.

Everything `KluMatrixCore` already provides - `factor()`/`refactor()`, `solve()`/`solveBlock()`, `product()`, `isFinite()`, accounting integration, `formatError()` - works unchanged on a block-sparse matrix once `rebuild()` (below) has expanded the block pattern into scalar `AP`/`AI`/`Ax`, because from KLU's point of view a block matrix is just a larger scalar matrix with a denser fill pattern.

## Block layout

Each nonzero entry of the underlying `SparsityMap` (keyed by block position, 1-based, `(row, col)`) expands to an `nbRow x nbCol` dense sub-block, stored column-major (required because KLU's own `AI`/`Ax` are column-major). The layout bookkeeping is three parallel arrays over the `n` block columns:

- **`denseColumnBegin`** (size `n+1`): the index, into the sparsity map's sorted `positions()`, of the first block in each column of blocks (and, at `[n]`, the total block count) - built by walking `positions()` once, since positions are already sorted column-major.
- **`blockColumnOrigin`** (size `n`): the scalar (`Ax`) offset of the topmost block's origin in each block column.
- **`blockColumnStride`** (size `n`): how many scalars to skip to move from one block-column's row to the same row in the next block-column of the same dense block-column group - `(blocks in this column) * nbRow`. This is what makes `column(i)` addressing within a block possible without every block column occupying contiguous memory: blocks in the same column of blocks are contiguous with each other, but different sub-columns of one dense block are separated by this stride, not by `nbRow` alone (because other blocks in the same block-column sit between them).

**`elementIndex(mep, blockMep)`** turns a block position (and, optionally, a 0-based in-block position) into a scalar offset into `Ax`, using `denseColumnBegin` to find the block's rank within its column and `blockColumnOrigin`/`blockColumnStride` to locate it; `elementPtr()` builds on it to return a `double*` to a component the same way `KluMatrixCore::elementPtr()` does, falling back to `blockBucket_` for a missing block instead of `KluMatrixCore`'s scalar `bucket_`.

## `rebuild(m, n, nbRow, nbCol, storageOnly=false)`

Builds the block layout from a `SparsityMap` of block positions (`n` block rows/columns) and expands it into `AN = n*nbCol` scalar columns and `nnz_ = m.size()*nbRow*nbCol` scalar nonzeros:

1. Computes `denseColumnBegin` from the sparsity map's sorted block positions.
2. Walks block-columns in order; within each, walks the `nbCol` scalar sub-columns and, within those, every block present in that block-column, writing each block's `nbRow` consecutive row indices into `AI` (and each sub-column's starting offset into `AP`) - this is what turns the block grid into a valid KLU compressed-column pattern.
3. Allocates `Ax` and zeroes it; allocates `blockBucket_` (see below); calls `klu_defaults()` and, unless `storageOnly`, `klu_analyze()` to get a symbolic factorization, exactly as `KluMatrixCore::rebuild()` does.

**`storageOnly`**: when true, `AP`/`AI` are left empty and `symbolic` is left null - the matrix becomes pure block-addressable storage (`block()`/`elementPtr()` still work) with no scalar KLU structure and therefore no `factor()`/`solve()` capability. HB analysis uses this for `jacColoc`/`jacSpec`, the per-timepoint/per-instance derivative blocks assembled before conversion to a solvable frequency- or time-domain system, while the actual solved matrix (`bsjac`/`acMatrix` in the HB cores) is built with `storageOnly=false`. Rectangular blocks (`nbRow != nbCol`) are explicitly supported for the storage-only case - such a matrix cannot be LU-factored, but is still useful as block-structured storage.

## `block(mep)`

Returns a `DenseMatrixView<ValueType>` (see [Dense Linear Algebra](densematrix.md)) over one block - `row stride = 1`, `column stride = blockColumnStride[col]` - plus a `found` flag. A view of `blockBucket_` (with strides 0) is returned instead when the block is absent, so callers can still write through it without a branch, exactly like `MatrixAccess::valuePtr()`'s scalar bucket convention. Returning a `DenseMatrixView` rather than a raw pointer lets callers use the full dense-algebra API of [Dense Linear Algebra](densematrix.md) (`operator=`, `addScaled()`, `multiply()`, ...) directly on one block, which is how HB analysis mixes a block's per-harmonic or per-timepoint contributions.

## Bucket sizing: `largeBucket`

The constructor takes a `largeBucket` flag (default `true`). When `true`, `blockBucket_` points into `bucketStorage_`, a full `nbRow*nbCol`-element scratch block, allocated at `rebuild()` time; when `false`, it points at the single-scalar `bucket_` inherited from `KluMatrixCore`. The large bucket exists because Jacobian loading can address a block element by adding a byte/element offset to a base bucket pointer (offset-based loading, used when a device writes several entries relative to one resolved base address) - a single scalar bucket would make such an offset read or write out of bounds even though the position is legitimately "not present, write goes nowhere real."

## `MatrixAccess` implementation

`valueArray()`/`cxValueArray()` forward to the inherited `KluMatrixCore::data()` (one is `nullptr` depending on `ValueType`, as in `KluAtomicMatrix`). `valueIndex()` forwards to `elementIndex()`, and `valuePtr()`/`cxValuePtr()` forward to `elementIndex()`/`elementPtr()` - all four honor `blockMep`, unlike `KluAtomicMatrix`'s versions, which ignore it because atomic matrices have no in-block position to speak of. When `blockMep` is omitted, all of them resolve to the block's origin element `(0,0)`.

## Diagnostics

`dumpBlockSparsity()` prints the block-level pattern (`x`/`.` per block position, one line per block row) by probing `block()` for every `(row, col)` pair - the block-grid analogue of `KluMatrixCore::dumpSparsity()`.

## Typedefs

`KluBlockSparseRealMatrix`/`KluBlockSparseComplexMatrix` are the project's `MatrixEntryIndex`-indexed instantiations, matching `KluRealMatrix`/`KluComplexMatrix` from [KLU Matrix](klumatrix.md).
