rustebra 0.3.0

A hybrid no_std/alloc linear algebra crate for Rust, scaling from embedded targets to dynamic Krylov subspace solvers.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
# Changelog


All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/).

## [Released]


## [0.3.0] - 2026-06-28


### Added


- **Hardware examples workspace** (`firmware/`): A separate Cargo workspace for device-specific
  examples. First entry: `firmware/cortex-m3-lm3s6965evb/` demonstrates rustebra on ARM Cortex-M3
  via QEMU, with zero heap allocation, semihosting for debug I/O, and low-pass filtering on
  simulated sensor data.
- `matvec_csr` and `matvec_csc` in the `sparse` module: free functions (gated behind `alloc`)
  that multiply a `CsrMatrix<T>` or `CscMatrix<T>` by a dense `&[T]` slice, returning
  `Result<Vec<T>, DimensionMismatch>`. `matvec_csr` traverses row by row using the `row_ptr`
  array; `matvec_csc` traverses column by column using the `col_ptr` array. Both return
  `Err(DimensionMismatch)` when the vector length does not equal `m.cols()`.

- `scale_csr` and `scale_csc` in the `sparse` module: free functions (gated behind `alloc`)
  that multiply every stored value of a `CsrMatrix<T>` or `CscMatrix<T>` by a scalar,
  returning a new matrix with the same shape and sparsity pattern. The index and pointer
  arrays are left unchanged; only the values array is transformed.

- `add_csr` and `add_csc` in the `sparse` module: free functions (gated behind `alloc`)
  that compute the element-wise sum of two sparse matrices in CSR or CSC format respectively,
  returning `Result<CsrMatrix<T>, DimensionMismatch>` or `Result<CscMatrix<T>, DimensionMismatch>`
  when the operands have different shapes. Within each row (CSR) or column (CSC), the merge
  sorts and sums coincident entries so the output has at most one entry per position.

- `coo_to_csr`, `csr_to_coo`, `csr_to_csc`, and `csc_to_csr` in the `sparse` module: free functions
  (gated behind `alloc`) that convert between `CooMatrix<T>`, `CsrMatrix<T>`, and `CscMatrix<T>`.
  `coo_to_csr` sorts all triplets by `(row, col)` and sums any duplicate positions, returning a
  `SortedCsrMatrix<T>` with at most one entry per unique `(row, col)` and ascending column indices
  within each row. `csr_to_coo` expands the row-pointer array into explicit per-entry row indices,
  preserving the original column and value order. `csr_to_csc` and `csc_to_csr` transpose the
  sparse structure, converting between row-major and column-major layouts while sorting entries
  into the appropriate order.

- `CsrMatrix<T>` in the `sparse` module: a heap-allocated sparse matrix in compressed sparse
  row (CSR) format, gated behind the `alloc` feature. Stores non-zeros as three parallel
  arrays — `row_ptr` (length `rows + 1`), `col_indices`, and `values`. Construction validates
  that `col_indices` and `values` have equal length, that `row_ptr` has exactly `rows + 1`
  entries, starts at zero, is non-decreasing, and ends at `nnz`, and that every column index
  is in-bounds, returning `CsrError` rather than panicking. Exposes `rows`, `cols`, `nnz`,
  `row_ptr`, `col_indices`, `values`, and `row_range` accessors.

- `CscMatrix<T>` in the `sparse` module: a heap-allocated sparse matrix in compressed sparse
  column (CSC) format, gated behind the `alloc` feature. Stores non-zeros as three parallel
  arrays — `col_ptr` (length `cols + 1`), `row_indices`, and `values`. Construction validates
  that `row_indices` and `values` have equal length, that `col_ptr` has exactly `cols + 1`
  entries, starts at zero, is non-decreasing, and ends at `nnz`, and that every row index
  is in-bounds, returning `CscError` rather than panicking. Exposes `rows`, `cols`, `nnz`,
  `col_ptr`, `row_indices`, `values`, and `col_range` accessors.

- `CooMatrix<T>` in the new `sparse` module: a heap-allocated sparse matrix in coordinate
  (COO) format, gated behind the `alloc` feature. Stores `(row, col, value)` triplets in
  arbitrary order; duplicate positions are accepted and treated as logically summed by
  downstream operations. Construction validates that all three component vectors have the
  same length and that every index is in-bounds, returning `CooError` rather than panicking.
  Exposes `rows`, `cols`, `nnz`, `row_indices`, `col_indices`, and `values` accessors.

- `matmat_csr` and `matmat_csc` in the `sparse` module: free functions (gated behind `alloc`)
  that multiply a sparse `CsrMatrix<T>` or `CscMatrix<T>` by a dense row-major matrix stored as
  a flat slice, returning `Result<Vec<T>, DimensionMismatch>` with the output as a flat row-major
  vector. The sparse matrix is `m × k` and the dense matrix is `k × x_cols`; the output is
  `m × x_cols`.

- `spmm_csr` in the `sparse` module: a free function (gated behind `alloc`) that multiplies
  two `CsrMatrix<T>` sparse matrices using a dense row accumulator (Gustavson algorithm),
  returning `Result<SortedCsrMatrix<T>, DimensionMismatch>`. Column indices within each output
  row are in ascending order, which is why the result is a `SortedCsrMatrix<T>`.

- `SortedCsrMatrix<T>` in the `sparse` module (gated behind `alloc`): a newtype wrapping
  `CsrMatrix<T>` that carries a compile-time guarantee that column indices within every row
  are in ascending order. Returned by `coo_to_csr` and `spmm_csr`, which produce sorted output
  by construction. `SortedCsrMatrix::from_csr` accepts any `CsrMatrix<T>` and sorts it in
  `O(nnz log nnz)`. Implements `Deref<Target = CsrMatrix<T>>` for transparent read access,
  `From<SortedCsrMatrix<T>> for CsrMatrix<T>` for ergonomic conversion, and cross-type
  `PartialEq` with `CsrMatrix<T>`.

- `SparseLinearOp<T>` in the `sparse` module (gated behind `alloc`): a trait providing a
  format-agnostic matrix-vector interface for use by Krylov solvers. Requires three methods:
  `rows() -> usize`, `cols() -> usize`, and `apply(&self, x: &[T]) -> Result<Vec<T>, DimensionMismatch>`.
  Implemented by `CsrMatrix<T>`, `CscMatrix<T>`, and `SortedCsrMatrix<T>`. Solver code can be
  generic over `impl SparseLinearOp<T>` with no heap allocation for the indirection.

- `prune_csr` in the `sparse` module (gated behind `alloc`): a free function that removes entries
  with absolute value at or below a caller-supplied tolerance from a `CsrMatrix<T>`, returning a
  new `CsrMatrix<T>` with the reduced entry set. Requires `T: Scalar + PartialOrd`; the tolerance
  is always caller-supplied (no auto-computed default).

- `DimensionMismatch` in the `sparse` module: a single, shared unit-struct error type for all
  sparse operations that can fail due to incompatible shapes. Replaces the per-module
  `ShapeMismatch` type that previously existed only in `sparse::matvec`. All sparse operations
  (`add_csr`, `add_csc`, `matvec_csr`, `matvec_csc`, `matmat_csr`, `matmat_csc`, `spmm_csr`,
  `SparseLinearOp::apply`) now return `Err(DimensionMismatch)` on shape failure.

- `lu`, `qr`, `cholesky`, `svd`, and `condition_number` methods on `StaticMatrix`/`DynamicMatrix`, wiring the existing
  `algorithm::matrix` decompositions up the same way `determinant`/`rank` already are, so callers don't need to work
  with `Storage`/`Scalar` generics or hand-size scratch buffers to use them. Only the general-user, auto-selecting
  entry point of each gets a method (matching `rank`'s/`determinant`'s existing precedent) — `lu_partial_pivot`,
  `qr_householder`/`qr_gram_schmidt`, `cholesky_decompose`, `svd_qr_iteration`, and `condition_number_svd` remain
  free-function-only for callers who need explicit control. `qr` returns a `Result` even on `StaticMatrix` (unlike
  `lu`/`cholesky`), since its `rows >= cols` precondition spans two different const generics that stable Rust has no
  way to bound against each other at the type level. `svd`/`condition_number` on `StaticMatrix` take their internal
  scratch workspace as a caller-supplied `&mut [T]` parameter rather than building it internally, for the same
  const-generic reason — `5*C*C + C + R` (`svd`) and `7*N*N + 3*N` (`condition_number`) aren't expressible as a
  fixed-size array length on stable Rust, since that requires combining (or even just squaring) const generic
  parameters, which only bare standalone parameters support; this keeps both usable without the `alloc` feature, at the
  cost of the caller needing to size the buffer by the documented formula instead of a zero-argument call.
  `DynamicMatrix`'s versions take no such parameter, since `Vec` sizing is ordinary runtime arithmetic with no such
  restriction.
- Extended `examples/matrix/static_matrix.rs`/`dynamic_matrix.rs` and `tests/matrix/static_matrix.rs`/
  `dynamic_matrix.rs` to demonstrate and exercise the new `lu`/`qr`/`cholesky`/`svd`/`condition_number` methods,
  matching the coverage every other matrix method already has.
- `FloatTolerance`, a trait separate from `Scalar` exposing machine epsilon, implemented for `f32`/`f64`. Backs
  automatically-computed default tolerances for non-expert callers, without forcing a numeric-conversion or epsilon
  concept onto every future `Scalar` implementor.
- `algorithm::matrix::rank_with_tolerance`, the explicit-tolerance counterpart to `rank`: the caller chooses the
  threshold below which a pivot candidate is treated as negligible, rather than getting an automatically-computed
  default.
- Edge-case tests demonstrating real floating-point rounding noise (not just exact-zero inputs) at the tolerance
  boundary for `rank`, `cholesky`, `svd`, and `condition_number`, including an extremely ill-conditioned matrix that
  exercises the precision floor of the fixed-iteration SVD algorithm.

### Fixed


- `determinant` (and `DynamicMatrix::determinant` / `StaticMatrix::determinant`) now return
  `Err(DeterminantError::MatrixTooLargeWithoutAlloc)` when the `alloc` feature is disabled
  and `rows > 4`. Previously, the `O(n!)` cofactor algorithm was silently used for any size
  matrix, with no signal to the caller that the call had become factorial-time.
  `DeterminantError` is a new enum with `DimensionMismatch` and `MatrixTooLargeWithoutAlloc`
  variants; `determinant`, `DynamicMatrix::determinant`, and `StaticMatrix::determinant` all
  now return `Result<T, DeterminantError>` instead of `Result<T, DimensionMismatch>` or `T`.

- `cholesky_decompose` (and `cholesky`) now return `Err(CholeskyError::NotSymmetric)` when
  any off-diagonal pair `a[i][j]` and `a[j][i]` differs by more than `tolerance` in absolute
  value. Previously, a non-symmetric matrix was silently accepted and the decomposition
  proceeded using only the lower triangle, producing an incorrect `L` with no error signal.
- 
### Changed

- `lu_partial_pivot` (and `rank`/`rank_with_tolerance`, which shares its elimination strategy) now selects the
  largest-magnitude candidate at or below the pivot row/column, instead of merely the first nonzero one — the standard
  partial-pivoting guarantee for bounding rounding-error growth, which the previous "first nonzero" selection didn't
  actually provide despite being named and documented as partial pivoting.
- `cholesky_decompose`, `svd_qr_iteration`, and `condition_number_svd` now take an explicit `tolerance` parameter for
  deciding when a value is negligibly close to zero (a positive-semi-definiteness residue, a negligible singular
  value), replacing exact float equality, which silently mis-classified any input whose arithmetic didn't land on an
  exact representable `0.0`. `cholesky`, `svd`, and `condition_number` remain the high-level entry points, now
  computing a sensible default tolerance automatically (via `FloatTolerance`) rather than taking the parameter
  directly. `lu_partial_pivot`/`qr_householder`/`qr_gram_schmidt` keep exact-zero checks deliberately: those decide an
  algebraic fact about the input (is this pivot/column exactly singular), not a negligibility judgment, so a tolerance
  there would mask rather than fix the real defect (see the partial-pivoting fix above).

## [0.2.1] - 2026-06-25


### Added


- `examples/algorithm/vector.rs`, touring `algorithm::vector`'s functions (`add`, `sub`, `scale`, `dot`, `norm`)
  directly via `Storage`, the same style `examples/algorithm/matrix/*.rs` already use; wired into
  `examples/algorithm/main.rs` alongside `matrix`.
- `examples/storage/` (`main.rs`, `static.rs`, `dynamic.rs`), the same `main.rs`-dispatches-`mod`s structure as
  `examples/matrix/`/`examples/vector/`: `static.rs` constructs a `StaticStorage` and inspects it through `Storage`;
  `dynamic.rs`, gated behind `#[cfg(feature = "alloc")]`, does the same for `DynamicStorage`.
- `examples/scalar/sqrt.rs`, one file per elementary `Scalar` function, calling it on both `f32` and `f64`;
  `examples/scalar/f32.rs`/`f64.rs` are trimmed to just the basic arithmetic identities
  (`zero`/`one`/`add`/`sub`/`mul`/`div`) to avoid demonstrating `sqrt` twice.
- A GitHub Actions workflow that publishes to crates.io on `v*` tag push, after checking the tag matches `Cargo.toml`'s
  version.

### Changed


- `Scalar::sin`/`Scalar::cos`'s near-identical bodies in `f32.rs` and `f64.rs` (computing `-x^2`, `2`, `3` and calling
  `taylor_series` inline) are now a single pair of generic functions in `trigonometry.rs` (`sin`/`cos`, taking
  `zero`/`one` as parameters rather than depending on `Scalar`, the same reason `sqrt.rs`'s `newton_raphson` already
  takes `zero`/`two` explicitly); `f32.rs`/`f64.rs` each reduce to a one-line call, matching how `sqrt` already
  delegates to `newton_raphson`.
- Added scope of the version 0.2.1
- Reorganized `examples/` to mirror `tests/`'s `algorithm`/`matrix`/`scalar`/`vector` grouping: each top-level group is
  one example (`cargo run --example algorithm`, etc.) with a `main.rs`/`mod.rs` holding only `mod` declarations and
  dispatch, the same role `tests/algorithm/main.rs`/`tests/matrix/main.rs` play for the test binaries; the actual
  demonstration code lives one file per `src` item, named after it (e.g. `examples/algorithm/matrix/cholesky.rs` for
  `algorithm::matrix::cholesky`, `examples/scalar/f32.rs` for `Scalar`'s `f32` impl), rather than one flat file per
  type (`examples/static_matrix.rs`, `examples/algorithm_matrix.rs`). `examples/algorithm/matrix/` now has one file per
  `algorithm::matrix` source file (`arithmetic`, `determinant`, `rank`, `lu`, `qr`, `cholesky`, `svd`, `condition`),
  splitting what was previously a single `algorithm_matrix.rs`; `examples/scalar/` gained an `f32.rs` alongside the
  existing `f64.rs` coverage, matching `tests/scalar/`'s per-type split. `dynamic_matrix`/`dynamic_vector` are no
  longer separate examples gated by `required-features` in `Cargo.toml` — they're `#[cfg(feature = "alloc")]`
  submodules of `matrix`/`vector` invoked conditionally from `main.rs`, the same pattern
  `tests/matrix/main.rs`/`tests/vector/main.rs` already use, so `Cargo.toml` no longer needs explicit `[[example]]`
  entries at all (auto-discovery picks up every `examples/*/main.rs`).
- Migrated the docs site from Jekyll to mdbook, with a custom theme.

## [0.2.0] - 2026-06-23


### Added

- `algorithm::matrix::determinant`, computed via recursive cofactor expansion along the first row; only defined for
  square matrices, returning `Result<T, DimensionMismatch>` instead of panicking on non-square input, per ADR 0004.
  Each minor is a zero-copy `Storage` view (a private `Minor` type) rather than a materialized submatrix, since
  submatrix sizes are only known at runtime in this `no_std`-first crate; `Minor` holds its source as
  `&dyn Storage<Item = T>` rather than a generic parameter, since a generic parameter would make every recursion level
  its own type (`Minor<Minor<Minor<...>>>`) with no compile-time bound on recursion depth — the one exception in this
  module to preferring generics over `dyn Trait`.
- Unit tests for `algorithm::matrix::determinant` (known 2x2 and 3x3 values, a singular matrix with a zero row, and
  non-square input as an error case).
- `StaticMatrix<T, N, N>::determinant`, wiring `algorithm::matrix::determinant` to an infallible method (`R == C` is
  guaranteed by the type, so the dimension mismatch is unreachable), and `DynamicMatrix<T>::determinant`, returning
  `Result<T, DimensionMismatch>` for real since a `DynamicMatrix`'s shape isn't known at compile time.
- Unit tests for both, including the non-square error case for `DynamicMatrix`.
- Extended `tests/matrix.rs` and the `examples/static_matrix.rs`/`examples/dynamic_matrix.rs` end-to-end demonstrations
  to cover `determinant`.
- `algorithm::matrix::rank`, computed by reducing the `rows x cols` matrix `a` to row echelon form via Gaussian
  elimination with partial pivoting, then counting the rows that aren't entirely zero; unlike `determinant`, works for
  non-square matrices. Since `Storage` is read-only, elimination runs in a caller-provided `scratch` buffer rather than
  mutating `a` in place; returns `Result<usize, DimensionMismatch>` if `a` or `scratch` doesn't have exactly
  `rows * cols` elements, rather than panicking.
- Unit tests for `algorithm::matrix::rank` (a full-rank case, a rank-deficient case with one row a multiple of another,
  a non-square case, and a mismatched-scratch-length error case).
- `StaticMatrix<T, R, C>::rank` and `DynamicMatrix<T>::rank`, wiring `algorithm::matrix::rank` to an infallible method
  (the internal scratch buffer always matches `self`'s shape, so the dimension mismatch is unreachable) that works for
  non-square shapes too, unlike `determinant`.
- Unit tests for both.
- Extended `tests/matrix.rs` and the `examples/static_matrix.rs`/`examples/dynamic_matrix.rs` end-to-end demonstrations
  to cover `rank`.
- `Scalar::sin` and `Scalar::cos`, public elementary functions on `Scalar`, computed via fixed-iteration Taylor series
  expansion around zero (`sin(x) = x - x^3/3! + x^5/5! - ...`, `cos(x) = 1 - x^2/2! + x^4/4! - ...`), each via a
  recurrence that avoids recomputing factorials and powers from scratch per term. No range reduction is performed, so
  precision degrades for inputs far from zero, the same trade-off `Scalar::sqrt` already documents for its own
  fixed-iteration approach.
- Unit tests for `Scalar::sin`/`Scalar::cos` on `f32` and `f64` (known angles: `0`, `pi/2`, `pi`).
- `tests/scalar.rs`, a black-box integration test exercising `Scalar`'s public API
  (`zero`/`one`/`add`/`sub`/`mul`/`div`/`sqrt`/`sin`/`cos`) on `f32` and `f64` using only `pub` items.
- `examples/scalar.rs`, a runnable end-to-end demonstration of every `Scalar` operation on `f64`.
- `tests/no_alloc.rs`, a `harness = false` integration test that registers a `#[global_allocator]` panicking on any
  `alloc`/`dealloc` call, then runs `StaticVector`/`StaticMatrix` `add`, `dot`, and `norm` against known values; a
  standard libtest harness allocates before running any test body, so it would trip the panicking allocator regardless
  of whether the code under test allocates, hence the custom `main`. Gated to run under `--no-default-features` only,
  matching the `alloc` feature's absence by default.
- `algorithm::matrix::lu_partial_pivot`, LU decomposition via Gaussian elimination with partial pivoting: at each step,
  the first row at or below the pivot with a nonzero entry in the pivot column is swapped into place (the same pivoting
  strategy `algorithm::matrix::rank` already uses), letting decomposition succeed on inputs plain Gaussian elimination
  would fail on (a zero already on the diagonal). Returns the row swap count rather than materializing a permutation
  matrix, since `l * u == p * a` only needs `p`'s sign (via swap parity) for the determinant and solve functions
  expected to consume this later. A column with no nonzero entry at or below the pivot (singular along that column) is
  left with a zero pivot in `u` rather than erroring, since that's a property of the input, not a malformed call. Only
  defined for square matrices, returning `Result<usize, DimensionMismatch>` instead of panicking on non-square input or
  mismatched output-buffer lengths, per ADR 0004.
- `algorithm::matrix::lu`, the high-level entry point that delegates to `lu_partial_pivot`.
- Unit tests for `algorithm::matrix::lu`/`lu_partial_pivot` (a known case requiring no pivoting, a case with a zero
  already on the diagonal that only succeeds because of pivoting, a singular matrix with an entire zero column, and the
  non-square/mismatched-buffer-length error cases), each verifying `l * u == p * a`.
- `algorithm::matrix::qr_householder`, QR decomposition via Householder reflections: for each column, builds a
  reflector `h = i - 2 * v * vᵗ / (vᵗ * v)` (`v = x + sign(x₁) * ‖x‖ * e₁`, sign chosen to match `x₁` to avoid
  catastrophic cancellation) that zeroes out the entries below the diagonal, applying it directly to `r`'s columns and
  `q`'s rows via the reflection formula rather than materializing each `h` as its own matrix. Only defined for
  `rows >= cols` (`q` is always square, so `r` couldn't otherwise stay upper triangular), returning
  `Result<(), DimensionMismatch>` instead of panicking on that or on mismatched buffer/scratch lengths.
- `algorithm::matrix::qr_gram_schmidt`, QR decomposition via modified Gram-Schmidt orthogonalization: each column is
  projected against every previously orthogonalized column in turn (subtracting from the running vector rather than the
  original column, which is what keeps rounding error from compounding as badly as classical Gram-Schmidt) and
  normalized into `q`. Produces a `rows x cols` `q` rather than `qr_householder`'s `rows x rows`, since Gram-Schmidt
  only ever produces as many orthonormal columns as `a` has. A column linearly dependent on the earlier ones leaves a
  zero column in `q` instead of dividing by a zero norm, the same choice `lu_partial_pivot` makes for a zero pivot.
- `algorithm::matrix::qr`, the high-level entry point that delegates to `qr_householder`, since Householder reflections
  are more numerically stable than Gram-Schmidt for general use.
- Unit tests for `algorithm::matrix::qr_householder`/`qr_gram_schmidt` (orthogonality of `q` and reconstruction of `a`
  from `q * r`, each for a square and a non-square case, plus the `rows < cols` and mismatched-scratch-length error
  cases), and for `algorithm::matrix::qr` confirming it matches `qr_householder`'s output.
- `algorithm::matrix::cholesky_decompose`, Cholesky decomposition of a symmetric positive-definite matrix into
  `l * lᵗ`: for each column `j`, `l[j][j] = sqrt(a[j][j] - sum_{k<j} l[j][k]^2)` and
  `l[i][j] = (a[i][j] - sum_{k<j} l[i][k] * l[j][k]) / l[j][j]` for `i > j`. Introduces `CholeskyError`, since this
  algorithm has a failure mode beyond a dimension mismatch: a negative value under the square root means `a` isn't
  positive-definite, reported as `Err(CholeskyError::NotPositiveDefinite)` rather than computing a nonsensical result.
  A value of exactly zero under the square root (positive *semi*-definite) leaves that column of `l` at zero instead of
  dividing by it, the same "leave a zero instead of erroring" choice `lu_partial_pivot` makes for a zero pivot.
- `algorithm::matrix::cholesky`, the high-level entry point that delegates to `cholesky_decompose`, the only Cholesky
  algorithm currently implemented.
- Unit tests for `algorithm::matrix::cholesky`/`cholesky_decompose` (reconstruction of `a` from `l * lᵗ` for a 2x2 and
  a 3x3 positive-definite matrix, a non-positive-definite case returning `NotPositiveDefinite`, the
  non-square/mismatched-buffer-length cases returning `DimensionMismatch`, and a case confirming `cholesky` matches
  `cholesky_decompose`'s output).
- Reorganized `algorithm::matrix` from a single file into a folder split by algorithm (`arithmetic`, `determinant`,
  `rank`, `lu`, `qr`, `cholesky`), each with its own colocated unit tests; `mod.rs` re-exports every item so existing
  call sites are unaffected.
- `algorithm::matrix::svd_qr_iteration`, singular value decomposition of any `rows x cols` matrix `a` by
  eigendecomposing the symmetric positive-semi-definite `aᵗ * a` via unshifted QR iteration (fixed at 100 iterations
  rather than convergence-checked, the same predictable-work trade-off `Scalar::sqrt`/`sin`/`cos` make for their own
  fixed-iteration approaches), accumulating the eigenvector matrix into `v` and reading the eigenvalues off the
  resulting diagonal. Singular values are `sqrt` of the eigenvalues, sorted descending via a selection sort performed
  directly on the diagonal and `v`'s columns (no separate index buffer, since only `T`-typed scratch is available).
  Each left singular vector is `a * v[:, i] / sigma_i`, left at zero instead of dividing by a zero singular value, the
  same choice `lu_partial_pivot` makes for a zero pivot. Takes a single caller-provided `scratch` buffer sized
  `5 * cols * cols + cols + rows`, partitioned internally into the working matrices the iteration needs, rather than
  growing the signature by one parameter per matrix.
- `algorithm::matrix::svd`, the high-level entry point that delegates to `svd_qr_iteration`, the only SVD algorithm
  currently implemented.
- Unit tests for `algorithm::matrix::svd`/`svd_qr_iteration` (reconstruction of `a` from `u * diag(sigma) * vᵗ` for a
  square and a non-square case, descending-sorted non-negative singular values for a diagonal matrix with out-of-order
  entries, singular-value count matching the known rank of a rank-deficient matrix, the mismatched-buffer-length error
  cases, and a case confirming `svd` matches `svd_qr_iteration`'s output).
- `algorithm::matrix::condition_number_svd`, the condition number `kappa(a) = sigma_max / sigma_min` of a square
  matrix, computed by reusing `algorithm::matrix::svd` and reading off the first and last entries of its
  already-descending-sorted `sigma` output. Introduces `ConditionNumberError`, since this has a failure mode beyond a
  dimension mismatch: a singular matrix (`sigma_min == 0`) has no well-defined condition number, reported as
  `Err(ConditionNumberError::Singular)` rather than dividing by that zero. Takes a single caller-provided `scratch`
  buffer sized `7 * rows * rows + 3 * rows`, partitioned internally into `svd`'s `u`/`sigma`/`v` outputs plus `svd`'s
  own scratch requirement, the same single-buffer approach `svd_qr_iteration` uses for its own working matrices.
- `tests/decompositions.rs`, black-box tests for `lu`, `qr`, `cholesky`, `svd`, and `condition_number` exercising only
  `pub` items on inputs distinct from the ones already covered by their colocated unit tests. Each test checks a
  mathematical property the result is required to satisfy (triangularity, orthogonality, reconstruction) or
  cross-validates against an independently-computed quantity from elsewhere in the public API (e.g. `determinant`'s
  cofactor expansion against an LU- or QR-based determinant, `svd`'s nonzero singular value count against `rank`,
  condition number under transpose), rather than asserting specific algorithm output.
- `examples/algorithm_matrix.rs`, a runnable end-to-end tour of every `algorithm::matrix` function operating directly
  on `Storage` and caller-provided buffers, since LU, QR, Cholesky, SVD, and condition number aren't wired to
  `StaticMatrix`/`DynamicMatrix` methods yet. Each operation with both a high-level entry point and one or more
  explicit algorithms runs both, naming the explicit function alongside the dispatcher.
- `algorithm::matrix::condition_number`, the high-level entry point that delegates to `condition_number_svd`, the only
  way this crate currently computes a condition number.
- Unit tests for `algorithm::matrix::condition_number`/`condition_number_svd` (the identity matrix giving `kappa = 1`,
  a diagonal matrix with a known ratio giving an exact `kappa`, a singular matrix returning `Singular`, the
  non-square/mismatched-scratch-length cases returning `DimensionMismatch`, and a case confirming `condition_number`
  matches `condition_number_svd`'s output).
- `algorithm::matrix::determinant_cofactor`, the existing recursive cofactor expansion, split out of `determinant`
  under its own name now that `determinant` dispatches between two algorithms.
- `algorithm::matrix::determinant_lu`, computing the determinant from an LU decomposition instead:
  `det(a) = (-1)^s * u[0][0] * ... * u[n-1][n-1]`, where `s` is the row swap count `lu_partial_pivot` performed.
  `O(n^3)` rather than cofactor expansion's `O(n!)`, at the cost of a caller-provided `scratch` buffer sized
  `2 * rows * cols` to hold the `l`/`u` factors.
- `.github/workflows/ci.yml`: a `test` job running `cargo build`/`cargo test` under both `--no-default-features` and
  `--features alloc`, plus `cargo clippy -- -D warnings` and `cargo fmt --check`; and a `docs` job (gated to pushes on
  `main`, depending on `test`) that runs `cargo doc --no-deps --features alloc` and deploys the generated API docs
  alongside a Jekyll-built site to GitHub Pages.
- A `Build (no_std, bare-metal cross-compile)` step in the `test` job:
  `cargo build --no-default-features --target thumbv6m-none-eabi`, cross-compiling the crate without the `alloc`
  feature for a Cortex-M0/M0+ target that has no operating system, no `std`, and no heap. Proves `no_std` compatibility
  at the compiler level — catches accidental dependence on `std`-only items that would otherwise compile silently on a
  hosted (`x86_64`) target — rather than relying on a runtime check. Requires the `thumbv6m-none-eabi` target
  component, installed via `dtolnay/rust-toolchain`'s `targets` input.
- A GitHub Pages site under `docs/`, built with Jekyll via the `pages-themes/cayman` remote theme (`docs/config.yml`):
  an ADR index page (`docs/adr/index.md`), a project banner (`docs/assets/banner.png`), and a customized `README.md`;
  published by the `docs` job above.
- `.github/PULL_REQUEST_TEMPLATE.md` and `.all-contributorsrc`, standard GitHub project scaffolding.

### Changed

- `Cargo.toml`: bumped `version` to `0.2.0`; added `keywords` (`linear-algebra`, `matrix`, `vector`, `no-std`,
  `numerical`) and `categories` (`algorithms`, `mathematics`, `no-std`).
- `algorithm::matrix::determinant` now dispatches by size instead of always using cofactor expansion:
  `determinant_cofactor` for `rows <= 4`, where cofactor expansion's overhead is negligible, and `determinant_lu` for
  larger matrices, where `O(n!)` becomes prohibitive. Widens `determinant`'s bound from `T: Scalar` to
  `T: Scalar + PartialEq`, since the LU path needs it; `StaticMatrix::determinant`/`DynamicMatrix::determinant` pick up
  the same bound as a per-method `where` clause rather than on their enclosing `impl` blocks, the same way `rank`
  already does. The LU path is only reachable behind the `alloc` feature, since `determinant`'s signature has no
  parameter for the `O(n^2)` scratch buffer it needs; without `alloc`, `determinant` always falls back to
  `determinant_cofactor`.
- Added a unit test confirming `determinant`'s output for a 5x5 matrix matches `determinant_lu` called directly.
- Reorganized `tests/` to mirror `src/`'s folder structure, one file per item under test, named after it (e.g.
  `tests/algorithm/matrix/cholesky.rs` for `algorithm::matrix::cholesky`, `tests/matrix/static_matrix.rs` for
  `StaticMatrix`), rather than one flat file per top-level area (`tests/decompositions.rs`, `tests/matrix.rs`,
  `tests/scalar.rs`, `tests/vector.rs`). Each directory's entry point (`main.rs`, or `mod.rs` one level down) now holds
  only `mod` declarations, the same role `src/algorithm/mod.rs` plays for the library itself — test logic always lives
  in a file named after what it tests, never in the entry point. `tests/algorithm/matrix/mod.rs` keeps the assertion
  helpers (`assert_all_close`, `assert_upper_triangular`, etc.) the split-out `lu`/`qr`/`cholesky`/`svd`/`condition`
  files all share. The `decompositions` test binary is renamed `algorithm` as a result (it now lives at
  `tests/algorithm/main.rs`); the others keep their names.

### Removed

- `tests/no_alloc.rs` and its `[[test]]` entry in `Cargo.toml`. Its approach — a `#[global_allocator]` that panics on
  any `alloc`/`dealloc` call, then running `StaticVector`/`StaticMatrix` operations against it — turned out to be an
  unreliable correctness signal: any panic in that binary, genuine allocation or plain value mismatch alike, needs to
  heap-allocate to box its own unwind payload, which re-enters the panicking allocator and aborts the process before
  the real failure ever prints. The `thumbv6m-none-eabi` cross-compile step above already proves no `alloc`-feature
  code is reachable without the feature enabled, the property this test existed to check, without that failure mode.

## [0.1.0] - 2026-06-21


### Added

- `Scalar` trait (zero, one, add, sub, mul, div) defining the scalar layer.
- `Scalar` implementation for `f32`.
- `Scalar` implementation for `f64`.
- Unit tests for the `f32` and `f64` `Scalar` implementations.
- `Storage` trait (element access, length) defining the storage layer.
- `StaticStorage<T, const N: usize>`, a stack-allocated `Storage` backed by a const-generic array.
- `alloc` feature flag (per ADR 0001), and `DynamicStorage<T>`, a heap-allocated `Storage` backed by `Vec<T>`, gated
  behind it.
- Unit tests for the `StaticStorage` and `DynamicStorage` implementations.
- Verified the crate builds, lints, and tests clean with `--no-default-features` (no `alloc`).
- `Scalar::sqrt`, computed via fixed-iteration Newton-Raphson (Babylonian) iteration, since `no_std` has no `f32`/`f64`
  `sqrt`; returns `0` for `self <= 0` (no real square root, and no `NaN` sentinel since `Scalar` must stay infallible
  for future non-float implementors).
- Unit tests for `Scalar::sqrt` on `f32` and `f64` (exact perfect squares, tolerance-bounded irrational results,
  negative-input behavior).
- `algorithm::vector::add`, the first algorithm-layer function: element-wise vector addition generic over `Storage` +
  `Scalar`, writing into a caller-provided output slice (since `Storage` has no way to construct a new instance) and
  returning `Result<(), LengthMismatch>` instead of panicking on mismatched lengths, per ADR 0004.
- Unit tests for `algorithm::vector::add` (matching-length addition, mismatched operand lengths, mismatched output
  length).
- `algorithm::vector::sub`, element-wise vector subtraction, following the same `Storage` + `Scalar` generic,
  output-slice, `Result<(), LengthMismatch>` pattern as `add`.
- Unit tests for `algorithm::vector::sub` (matching-length subtraction, mismatched operand lengths, mismatched output
  length).
- `algorithm::vector::scale`, element-wise scalar multiplication; still returns `Result<(), LengthMismatch>` since the
  output buffer's length can disagree with the input's even with only one `Storage` operand.
- Unit tests for `algorithm::vector::scale` (known factor, scaling by zero, mismatched output length).
- `algorithm::vector::dot`, the inner product, returning `Result<T, LengthMismatch>` instead of panicking on mismatched
  lengths; documented as the building block for the upcoming `norm` (`‖v‖ = sqrt(dot(v, v))`).
- Unit tests for `algorithm::vector::dot` (known vectors, mismatched lengths).
- `algorithm::vector::norm`, the Euclidean (L2) norm, implemented as `dot(a, a).sqrt()`; infallible (returns `T`
  directly), since comparing `a` against itself can never produce a length mismatch.
- Unit tests for `algorithm::vector::norm` (exact known case, tolerance-bounded irrational case).
- `StaticVector<T, const N: usize>`, the first public API layer type: wires `StaticStorage` together with
  `algorithm::vector`'s `add`/`sub`/`scale`/`dot`/`norm` into ergonomic methods (`v1.add(&v2)`, etc.), constructed from
  a fixed-size array via `StaticVector::new`.
- `PartialEq` and `Debug` implementations for `StaticVector`, to support equality assertions and printing in tests
  without exposing raw element access.
- Unit tests for `StaticVector` construction and each operation (`add`, `sub`, `scale`, `dot`, `norm`), confirming the
  wiring to the algorithm layer.
- `DynamicVector<T>`, the heap-allocated counterpart to `StaticVector`, gated behind the `alloc` feature: wires
  `DynamicStorage` together with `algorithm::vector`'s functions the same way, but since two `DynamicVector`s aren't
  guaranteed by the type system to share a length, `add`/`sub`/`dot` return `Result<_, LengthMismatch>` for real (not
  just defensively) rather than panicking, per ADR 0004.
- `PartialEq` and `Debug` implementations for `DynamicVector`, accounting for operands of different lengths.
- Unit tests for `DynamicVector` construction and each operation, including mismatched-length cases for `add`, `sub`,
  and `dot`.
- `examples/static_vector.rs` and `examples/dynamic_vector.rs`, runnable end-to-end demonstrations of `StaticVector`
  and `DynamicVector` (the latter gated via `required-features = ["alloc"]` in `Cargo.toml`, so
  `cargo build`/`run --examples` skip it cleanly without the feature).
- `tests/vector.rs`, a black-box integration test exercising the public `StaticVector`/`DynamicVector` API end-to-end
  (construction plus `add`/`sub`/`scale`/`dot`/`norm`) using only `pub` items, with the `DynamicVector` case gated
  behind `alloc`.
- `algorithm::matrix::add`, the first matrix-layer function: element-wise matrix addition generic over `Storage` +
  `Scalar`. Matrices are stored row-major in a flat `Storage` (which has no concept of rows/columns itself, per ADR
  0003), so `rows`/`cols` are passed explicitly alongside each operand; returns `Result<(), DimensionMismatch>` instead
  of panicking on a row, column, or flat-length disagreement, per ADR 0004.
- Unit tests for `algorithm::matrix::add` (matching dimensions, mismatched rows, mismatched columns, mismatched output
  length).
- `algorithm::matrix::sub`, element-wise matrix subtraction, following the same row-major, explicit-shape,
  `Result<(), DimensionMismatch>` pattern as `add`.
- `algorithm::matrix::mul_scalar`, element-wise scalar multiplication of a matrix.
- `algorithm::matrix::mul_vector`, the matrix-vector product: each output element is `dot(row_i, v)`, reusing
  `algorithm::vector::dot` via a private zero-copy `Row` `Storage` view instead of re-deriving the summation.
- `algorithm::matrix::mul_matrix`, the matrix-matrix product: each output element is `dot(row_i of a, col_j of b)`,
  reusing `algorithm::vector::dot` via the `Row` view and a new private `Column` `Storage` view; validates `a`'s column
  count against `b`'s row count (the multiplication's inner dimension).
- `algorithm::matrix::transpose`, pure reindexing with no `Scalar` arithmetic, so it only requires `T: Copy` rather
  than `T: Scalar`.
- Unit tests for `sub`, `mul_scalar`, `mul_vector`, `mul_matrix`, and `transpose` (known-value cases plus
  dimension-mismatch error cases).
- `Storage` implementations for `StaticVector` and `DynamicVector` (delegating to their internal storage), so they can
  be passed directly to generic `Storage`-based functions — needed for `StaticMatrix`/`DynamicMatrix` to multiply
  against them.
- `StaticMatrix<T, const R: usize, const C: usize>`, the first public-API matrix type: stored as `[[T; C]; R]` (since
  `R * C` can't be a single const-generic array length on stable Rust, unlike `StaticVector`, it implements `Storage`
  directly over that field rather than wrapping `StaticStorage`). Wires
  `add`/`sub`/`mul_scalar`/`mul_vector`/`mul_matrix`/`transpose` to `algorithm::matrix`; `mul_matrix<const C2: usize>`
  changes shape (`R x C` times `C x C2` gives `R x C2`), with the inner-dimension check statically guaranteed (and thus
  unreachable) by the type system, same as the other same-shape operations.
- Unit tests for `StaticMatrix` construction and each operation, confirming the wiring to the algorithm layer.
- `DynamicMatrix<T>`, the heap-allocated counterpart to `StaticMatrix`, gated behind the `alloc` feature: shape
  (`rows`/`cols`) lives in its fields rather than its type, so `add`/`sub`/`mul_vector`/`mul_matrix` return
  `Result<_, DimensionMismatch>` for real (not just defensively) rather than panicking, per ADR 0004;
  `mul_scalar`/`transpose` stay infallible, sized from `self` alone. Constructed via
  `DynamicMatrix::new(rows, cols, Vec<T>)`, itself fallible since the flat data's length can disagree with the claimed
  shape.
- `PartialEq` and `Debug` implementations for `DynamicMatrix`, accounting for operands of different shapes.
- Unit tests for `DynamicMatrix` construction and each operation, including mismatched-shape/inner-dimension error
  cases for `add`, `sub`, `mul_vector`, and `mul_matrix`.
- `examples/static_matrix.rs` and `examples/dynamic_matrix.rs`, runnable end-to-end demonstrations of `StaticMatrix`
  and `DynamicMatrix` (the latter gated via `required-features = ["alloc"]` in `Cargo.toml`, matching the vector
  examples' pattern).
- `tests/matrix.rs`, a black-box integration test exercising the public `StaticMatrix`/`DynamicMatrix` API end-to-end
  (construction plus `add`/`sub`/`mul_scalar`/`mul_vector`/`mul_matrix`/`transpose`) using only `pub` items, with the
  `DynamicMatrix` case gated behind `alloc`.