racah 0.1.1

Racah-Wigner calculus for compact Lie groups: exact SU(2) recoupling, and runtime Clebsch-Gordan / F- / R-coefficient generation for SU(N), SO(N), and Sp(2N)
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
480
481
482
483
484
# racah

Racah–Wigner calculus for compact Lie groups, in Rust: irreducible
representations, Clebsch–Gordan coefficients, and recoupling coefficients
(3j / 6j / F / R) for SU(2), SU(N), SO(N), and Sp(2N).

Coefficients for any admissible labels are computed on demand in exact or
verification-gated arithmetic — there is no precomputed table and no label
ceiling.

## Installation

Racah is available on crates.io. The default build provides the exact SU(2)
path; the optional `cgc-gen` feature uses the published Tenferro 0.3.0 line.
For a source checkout or unreleased development, use the git dependency:

```toml
[dependencies]
racah = "0.1.1"
# generated SU(N)/SO(N)/Sp(2N) families:
# racah = { version = "0.1.1", features = ["cgc-gen"] }
```

MSRV: latest stable Rust (CI pins no minimum version; it builds and tests on
`stable`).

## Feature flags

| Feature | Adds | Pulls in |
|---|---|---|
| *(default)* | Exact SU(2): 3j / 6j / Clebsch–Gordan / F / R, closed-form big-rational | `num-bigint`, `num-rational`, `num-traits` only |
| `cgc-gen` | Runtime CGC / F / R generation for SU(N) (Gelfand–Tsetlin) and SO(N)/Sp(2N) (generator bootstrap) | `tenferro-linalg` / `-cpu` / `-runtime` (the dense factorization + contraction backend) |

The `cgc-gen` dependencies use the published Tenferro 0.3.0 registry line. The
default build stays dependency-light and needs no linear-algebra stack.

## Quick start

One minimal example per layer. Each is a literal copy of a crate doctest, so it
compiles against the current API (`cargo test` / `cargo test --all-features`).

Exact SU(2) 6j (base, no features). Spins are doubled (`dj = 2j`), so `2` means
spin 1; `{1 1 1; 1 1 1} = 1/6`:

```rust
use racah::wigner_6j;

let sixj = wigner_6j(2, 2, 2, 2, 2, 2);
assert!((sixj.to_f64() - 1.0 / 6.0).abs() < 1e-14);
```

SU(N) F-symbol (`cgc-gen`). Irreps are built from Dynkin labels (length `N-1`);
this is the SU(3) sextet `1 ⊗ 3 ⊗ 3 → 6`, a `1×1×1×1` identity move:

```rust
use racah::sun::{f_symbol, Irrep};

let triv = Irrep::trivial(3).unwrap(); // SU(3) singlet
let three = Irrep::from_dynkin(&[1, 0]).unwrap(); // fundamental
let six = Irrep::from_dynkin(&[2, 0]).unwrap();

let block = f_symbol(&triv, &three, &three, &six, &three, &six).unwrap();
assert_eq!(block.dims(), [1, 1, 1, 1]);
assert!((block.at(0, 0, 0, 0) - 1.0).abs() < 1e-12);
```

SO(N)/Sp(2N) F-symbol (`cgc-gen`). Generation runs through a per-(series, rank)
`CanonicalCatalog` that caches the aligned CGC; this is an Sp(4) (`C_2`) block:

```rust
use racah::bcd::{f_symbol, CanonicalCatalog, Irrep, Series};

let mut cat = CanonicalCatalog::new(Series::C, 2).unwrap(); // Sp(4)
let triv = Irrep::trivial(Series::C, 2).unwrap();
let v = Irrep::from_dynkin(Series::C, &[0, 1]).unwrap(); // vector
let adj = Irrep::from_dynkin(Series::C, &[2, 0]).unwrap(); // in v ⊗ v

let block = f_symbol(&mut cat, &triv, &v, &v, &adj, &v, &adj).unwrap();
assert_eq!(block.dims(), [1, 1, 1, 1]);
assert!((block.at(0, 0, 0, 0) - 1.0).abs() < 1e-9);
```

## Why this crate exists

No library — in Rust, and essentially nowhere as a standalone component —
computes the *full* representation-theory coefficient set for the compact Lie
groups on demand with unbounded labels. By "full set" we mean, for a given
group and any admissible irreps: fusion multiplicities, dimensions, duals,
Frobenius–Schur indicators, Clebsch–Gordan coefficients, and the recoupling
data (3j / 6j and the F- and R-symbols). `racah` is that standalone library. It
covers SU(2), SU(N), SO(N), and Sp(2N); it is pure representation mathematics
with no tensor-network vocabulary and no dependency on any tensor engine; and it
is usable by anyone who needs these numbers — atomic and molecular spectroscopy,
nuclear and quantum-chemistry coupling, lattice and continuum models, symmetric
tensor networks, and more.

The existing supply of these coefficients stops short of that, in two ways:

- **Precomputed tables** (offline generation, checked-in data) are complete for
  *finite* symmetry sets — a fixed, small collection of irreps means a table is
  the whole truth. But a compact Lie group has infinitely many irreps, and
  taking tensor products only makes them larger, so any table has a cut that a
  large-enough calculation will exceed.
- **Single-group coefficient packages** solve one group at a fixed scope (for
  example exact SU(2) 3j/6j over a bounded label range) and do not extend to
  SU(N≥3), SO(N), or Sp(2N), where no closed-form expressions exist and the
  coefficients must be *constructed*.

`racah` removes both limits: coefficients for any admissible labels are computed
on demand, inside the process, in pure Rust, with no label ceiling. To do this
faithfully it consolidates the algorithms of three production references, one
per family (full provenance in [`docs/references.md`](docs/references.md)):

| Reference | What is taken from it |
|---|---|
| WignerSymbols.jl | the exact SU(2) model: big-rational Racah sums, prime-factorized factorials as the measured-need upgrade |
| SUNRepresentations.jl (Alex–Kalus–Huckleberry–von Delft, J. Math. Phys. 52, 023507 (2011)) | the SU(N) pipeline: Gelfand–Tsetlin patterns, exact ladder matrices, highest-weight nullspace, deterministic gauge canonicalization, weight-ladder descent |
| QSpace v4 (Weichselbaum) | the SO(N)/Sp(2N) pipeline: per-family defining-representation seeds feeding one family-generic decomposition loop; and the production discipline — abort on tolerance violation, per-representation error recording, precision tiers |

These are complementary, not competing: the Gelfand–Tsetlin construction is
fundamentally SU(N)-specific, while QSpace's generator-based decomposition is
the only production reference that generates SO(N) and Sp(2N). For the
representation-theory background behind these objects, see
[`docs/theory.md`](docs/theory.md).

## What it computes

- Irrep labels, dimensions, duals, Frobenius–Schur indicators.
- Product decomposition: fusion multiplicities $N^c_{ab}$ (exact combinatorics).
- Clebsch–Gordan coefficients $C^{ab\to c}$ (m-basis tensors, outer multiplicity
  as a trailing index).
- Recoupling coefficients: F-symbols (contraction of four CGC over all
  magnetic indices, leaving the multiplicity indices) and R-symbols
  (symmetric braiding phases). For SU(2) these reduce to the closed-form
  Racah/6j expressions and are computed exactly.
- Self-check functions: CGC orthogonality, F-unitarity, R-orthogonality,
  pentagon/hexagon identities — shipped as public API so they double as
  generation gates and as oracle harnesses for downstream users.

## What it deliberately is not

- **No fusion-category trait vocabulary.** No sector-identity types, no
  tensor-network concepts, no dependency on any tensor engine. `racah`
  answers "what are the correct numbers"; consumers translate them into
  their own categorical interfaces. (The category of representations of a
  compact group is one fusion category among many; a consumer's engine
  should not be able to tell whether an F-block came from this crate, a
  closed form, or a checked-in table.)
- **No pentagon solving for finite fusion categories.** Anyon models
  (Fibonacci, Ising, …) have complete exact F/R data published (e.g. the
  AnyonWiki classification, all multiplicity-free categories up to rank 7);
  those are a data-conversion problem for the consumer, not a computation
  problem for this crate.
- **No symbolic algebraic-number coefficients.** See the exactness contract
  below.

## Design

### Layering

```
racah
├─ base (minimal dependencies)
│   └─ SU(2): exact 3j/6j/CGC — closed-form big-rational Racah sums,
│      canonical Regge keys, bounded publication cache, no doubled-spin
│      ceiling; a single final rounding to floating point
└─ feature "cgc-gen"
    ├─ SU(N):  GT-pattern basis → exact Rational ladder matrices →
    │          highest-weight nullspace → gauge canonicalization
    │          (positive-diagonal QR ∘ column-pivoted reduced echelon,
    │          pivot rules part of the specification) → ladder descent
    ├─ SO(N)/Sp(2N): per-family defining-rep seeds (simple-root raising
    │          operators + Cartan generators) → shared decomposition loop
    │          (raising-operator seed → Gram–Schmidt sweep → column QR)
    ├─ CGC → F/R contraction (m-indices contracted, multiplicity indices
    │          [μ,ν,κ,λ] remain)
    ├─ verification gates (orthogonality, unitarity, pentagon/hexagon)
    └─ bounded provider-internal coefficient caches
```

The feature boundary is mathematical, not organizational: SU(2) has closed
forms and needs no matrix computation; every other family must be generated
numerically. Consumers that only need abelian or SU(2) symmetries never pull
a linear-algebra stack.

### Why each family gets its algorithm

The construction per family is forced by the group's branching structure, not
chosen for convenience (the full argument is in
[`docs/theory.md`](docs/theory.md) §5):

- **SU(2)** — closed forms exist (Racah), so the 3j/6j/CGC/F/R are evaluated
  directly in exact big-rational arithmetic with a single final rounding; there
  is nothing to generate.
- **SU(N)** — the unitary chain $U(N) \supset U(N-1) \supset \dots \supset U(1)$ has multiplicity-free
  branching (the intermediate U(1) charge at each step separates copies that the
  SU chain alone would repeat), so basis states of an SU(N) irrep are labelled
  uniquely by Gelfand–Tsetlin patterns and the ladder operators have exact
  closed-form matrix elements (Alex–Kalus–Huckleberry–von Delft). That closed
  form is what makes the direct GT construction possible, and it is SU(N)-specific.
- **SO(N) / Sp(2N)** — the symplectic chain $Sp(2r) \supset Sp(2r-2)$ has branching
  multiplicities, so no GT-type basis with practical closed-form matrix elements
  exists (the SO chains are multiplicity-free, and explicit GT-type matrix
  elements for them do exist — Gelfand–Tsetlin 1950; Molev — but they are
  substantially more involved and no production implementation exists). So these
  families use the generator bootstrap —
  defining-representation seeds (writable explicitly per series), tensor
  products, numeric highest-weight decomposition, harvest, recurse — which needs
  almost no family-specific structure. Its price, a gauge fixed by procedural
  determinism rather than a formula, is what
  [`docs/gauge_soN.md`]docs/gauge_soN.md pins down.

### Kernel routing

All dense numerical work behind `cgc-gen` — the nullspace/QR/least-squares
factorizations and the CGC contractions producing F/R — routes through a
selectable dense backend. `racah` contains no hand-rolled numeric kernels:
the backend a consumer selects for its tensor computations is the backend
used for coefficient generation. An extended-precision tier (the QSpace
model: compute in ~128-bit precision, tighten tolerances, store f64) is a
future backend capability with an explicit unsupported boundary until
implemented, not a private arithmetic stack inside this crate.

### Exactness contract

Coefficient *values* are floating point — as in every production reference
(the Julia SU(N) stack is Float64 end-to-end after the ladder matrices;
QSpace is double or MPFR-128; exact algebraic-number coefficients exist only
in research-scale tools). What is exact is the *computation*:

1. **Combinatorial structure is exact.** Pattern enumeration, fusion
   multiplicities, weight systems, and multiplicity dimensions use
   integer/rational arithmetic only.
2. **Discrete data is exact.** Duals, Frobenius–Schur phases, signs, and
   basis ordering are combinatorial facts, never numerical results.
3. **Gauge fixing is deterministic.** The canonicalization is a specified,
   deterministic function of the nullspace subspace (pivot rules and sign
   conventions included); a discrete gauge flip across runs, builds, or
   backends is a defect, not a tolerance event.
4. **Versioned values.** The generation algorithm and gauge are part of this
   crate's semantic-versioning contract: any change that can alter
   coefficient values is a breaking change, so consumers can key caches and
   persisted data on the crate version.
5. **Verification-gated floating point.** Orthogonality, unitarity, and
   pentagon/hexagon checks run at generation time; a tolerance violation is
   a typed error, never a silently degraded coefficient.

This generalizes the exact-SU(2) tradition (compute in rationals, round
once): for generated families the single rounding point moves earlier — into
the nullspace solve — while structure, gauge, and verification stay at the
same standard.

For the base SU(2) provider this convention set is exposed as an opaque
fingerprint that changes only on the value-affecting breaking release of point 4
above; see [Provider contract](#provider-contract) below.

### Gauge continuity

The SU(N) pipeline reproduces the gauge of its reference implementation by
construction: the canonical gauge is a deterministic function of the GT basis
order and the nullspace subspace, so a faithful port reproduces
reference-generated coefficient tables to numerical tolerance. Existing
table-based deployments can therefore demote their tables from authority to
oracle fixtures. SO(N)/Sp(2N) carry their own gauge tag; cross-checks against
QSpace numbers go through an explicit gauge-transformation harness.

## Verification strategy

Oracles are independent of the code under test:

- exhaustive agreement with the existing exact SU(2) crate over its label
  domain, plus reference-generated fixtures beyond it;
- regeneration diffs against reference-generated SU(N) tables (gauge
  continuity makes this a direct comparison);
- Regge/tetrahedral symmetries, pentagon/hexagon identities, and
  orthogonality as internal consistency gates;
- QSpace numbers for SO(N)/Sp(2N) after gauge alignment.

## Provider contract

The base SU(2) provider (default build, no features) exposes a small, stable
contract so a consumer can use it as one coefficient authority without
duplicating convention identity, representation validation, or cache accounting.
Every item below is base-SU(2)-only and pulls no linear-algebra stack.

### Authority fingerprint

`su2_authority_fingerprint() -> &'static [u8]` returns opaque bytes that
identify the *convention set* every returned SU(2) coefficient (3j, 6j,
Clebsch–Gordan, F, R, Frobenius–Schur) is computed in.

- **What it is** — an identifier for the value-fixing conventions, not a
  document. Treat it as opaque: compare by equality only, never parse it.
- **When it changes** — not on a rebuild, dependency bump, or additive release;
  it is derived from none of the crate version, source, docs, or process state.
  It changes only on a value-affecting *breaking* release — a change that can
  alter a returned coefficient, its normalization, or its canonical convention,
  the same event class point 4 of the exactness contract declares breaking. So
  "fingerprint changed ⇔ value-affecting breaking release" is one reviewable
  invariant, pinned by the compatibility-policy test `tests/su2_fingerprint.rs`.
- **How a consumer uses it** — persist the bytes next to anything derived from
  these coefficients (a cache, a serialized table); on load, compare for
  equality and reject the derived data on mismatch.

### Checked SU(2) representation surface

The `su2` module adds a typed, checked layer over the infallible closed-form
functions. `Su2Irrep` labels an irrep by its doubled spin (`dj = 2j`); every
`u32` is valid, so `Su2Irrep::new` is infallible and `dj` / `dim` / `dual`
cannot fail. `Su2Irrep::fusion` returns a non-allocating `Su2Fusion` iterator
over the coupled irreps, or `Err(Su2Error::LabelOverflow)` when `dj1 + dj2`
exceeds `u32`.

The checked coefficient functions — `wigner_3j_checked`, `wigner_6j_checked`,
`clebsch_gordan_checked`, `su2_f_symbol_checked`, `su2_r_symbol_checked` —
return `Err(Su2Error::NotAdmissible(_))` for a structurally forbidden tuple
(triangle / parity / m-range violation, named by `AdmissibilityViolation`) and
`Ok(value)` otherwise. That `Ok` / `Err` split is the point: an admissible 6j
can still be *accidentally* zero, so `Ok(0)` (a real zero of an admissible
coupling) and `Err(NotAdmissible)` (a forbidden coupling) are finally
distinguishable — where the infallible functions return the same exact zero for
both. The checked layer is purely additive: the infallible `wigner_6j` &c. keep
their zero convention, and both paths share one admissibility predicate, so they
can never disagree.

### Cache resource contract

The three base coefficient tiers (3j, 6j, derived-F) are each bounded
independently by a per-tier entry and conservative retained-charge cap. The documented aggregate cap
`BASE_CACHE_MAX_BYTES` (192 MiB = 3 × 64 MiB) is their sum — a **static
partition, not a dynamic shared pool** — and holds as a corollary of the
per-tier charged-entry caps, tied to the per-tier cap by a `const` assertion so the two
cannot drift.

The charge covers entries currently owned by the cache. It excludes container
retained capacity and scaffolding, allocator metadata and RSS, transient or
external clones, and values returned through public APIs.

`base_cache_stats() -> BaseCacheStats` exposes per-tier `TierStats` (`entries`,
`bytes`, `hits`, `misses`, `evictions`) for `three_j`, `six_j`, `derived_f`,
plus a field-wise `total()`. Each per-tier snapshot is consistent under its tier
lock; the total is a sum of per-tier snapshots, not a global atomic snapshot, so
under concurrent fills it is only eventually consistent (no global lock spans the
tiers).

`reset()` returns every tier's entries, bytes, and hit/miss/eviction counters to
zero. It acts on process-global `static` state, so reset ownership is
**single-owner**: exactly one component in a consuming process owns reset policy;
a library must not call it.

#### Process-local budget policy

Before any coefficient-cache operation or policy observation, an application
may call `configure_cache_budgets(CoefficientCacheBudgets::default()
.with_limit(CoefficientCacheTier::SixJ, 1 << 20))`. The policy is one-shot and
shrink-only: the compiled defaults are also the maximum accepted caps. A zero
tier cap evaluates normally but retains no entry; `CoefficientCacheBudgets::disabled()`
sets every compiled tier to zero. `cache_budgets()` reports the effective policy
and fixes the default if configuration did not win the first-use race. `reset()`
clears entries and counters without changing this policy. There are no presets,
environment variables, shared LRU, or runtime reconfiguration.

#### Per-tier cache trim

`trim_to(CoefficientCacheTier::SixJ, target_charged_bytes)` is the
single-owner, process-global lifecycle operation for releasing one tier's
oldest FIFO entries. It returns `CacheTrimReport` with the removed and remaining
cache-owned charged entries at its tier-lock linearization point; a concurrent
miss may publish after the call. Zero removes that tier's entries, while hits
and misses remain and evictions increase by the entries removed. Trimming never
changes the one-shot budget and never promises to release external `Arc`s,
container capacity, allocator metadata, or RSS.

### Generated families (cgc-gen)

The `cgc-gen` generated SU(N) and SO(N)/Sp(2N) providers add a parallel surface
alongside the base one. It is **unstable**:

> Unstable: shape may change while the generated-provider contract is negotiated.

Cargo features cannot express instability tiers, so this label plus issue
[#47](https://github.com/Ryo-wtnb11/racah/issues/47) are the ledger.

#### Authority fingerprints

`racah::sun::sun_authority_fingerprint()` and
`racah::bcd::bcd_authority_fingerprint()` return opaque `&'static [u8]` that
identify the convention set, generation pipeline, and verification/tolerance
policy of each generated family. Unlike the exact SU(2) fingerprint, their
contract is deliberately weaker:

> Equal fingerprints identify the same convention, generation pipeline, and
> tolerance policy. They do not imply byte-identical values or independently
> prove numerical agreement.

Numerical agreement is established by the generation-time verification gates and
the independent oracle suites (`docs/gauge.md`, `docs/gauge_soN.md`), never by
the fingerprint. Backend identity is deliberately excluded: per-backend ULP
differences are inside the disclaimed tolerance class, and a discrete gauge flip
across backends is a defect, not a tolerance event.

Each family's `epoch` tag is **per-family and independent**: an SU(N) gauge
change bumps only the SU(N) epoch and never invalidates SU(2)- or B/C/D-derived
consumer state (and vice versa). Compare the bytes by equality only; never parse
them.

#### Generated cache aggregate

The five generated tiers (SU(N) product / CGC / F, B/C/D CGC / F) are each bounded
by a per-tier entry and conservative retained-charge cap.
`racah::cache::GENERATED_CACHE_MAX_BYTES` (640 MiB + 128 KiB) is their documented sum,
tied to the per-tier caps by a `const` assertion so the two cannot drift. The
cache story is **two-layer**: base = `BASE_CACHE_MAX_BYTES`, generated =
`GENERATED_CACHE_MAX_BYTES`, and the documented whole-process retained-entry
charge cap is their sum. Generated-tier `TierStats::bytes` uses the same charge
and exclusions as the base tiers: it covers entries currently owned by the
cache, not container retained capacity/scaffolding, allocator metadata or RSS,
transient or external clones, or values returned through public APIs. There is
deliberately no single cross-feature constant — one number spanning
feature-gated tiers would change meaning with the `cgc-gen` flag.
`racah::cache::generated_cache_stats() -> GeneratedCacheStats` reports the five
tiers per-tier plus a field-wise `total()`; `reset()` clears them alongside the
base tiers.

The generated CGC value cache is keyed by the complete `(s1, s2, s3)` irrep
labels and is independent of which caller-owned `CanonicalCatalog` instance
produced an entry. Dropping or rebuilding a catalog does not invalidate cached
values:

> Cached values remain valid because catalog instances implement the same
> canonical convention and tolerance contract, and the complete
> family/rank/irrep labels determine the key.

## Status

Feature-complete for its v0 scope; all three families are implemented and
oracle-checked:

- **SU(2)** (base): exact 3j / 6j / Clebsch–Gordan / F / R in big-rational
  arithmetic.
- **SU(N)** (`cgc-gen`): the full Gelfand–Tsetlin pipeline — CGC, F, R, with
  outer-multiplicity indices.
- **SO(N) / Sp(2N)** (`cgc-gen`): the generator-bootstrap pipeline (B/C/D
  Cartan series) — CGC, F, R.

Verification (every claim below is backed by a merged test; the crate ships its
self-checks — orthogonality, F-unitarity, pentagon, hexagon — as public API):

- **SU(2)**: exhaustive agreement with `wigner-symbols` 0.5.1 over its label
  domain, plus reference fixtures beyond it.
- **SU(N)**: signed element-wise table regeneration against
  SUNRepresentations.jl v0.4.0 — a dim ≤ 8 slice on every `cargo test`, and a
  full dim ≤ 27 sweep (76,853 F blocks) run explicitly. Products and
  multiplicities are cross-checked against GroupMath 1.1.3 fixtures.
- **SO(N) / Sp(2N)**: the QSpace v4 CGC projector battery — **33** of the
  fixture's rank-2/3 B/C/D channels are projector-tested against QSpace to
  round-off (via verified factor-basis dictionaries), **0** remain
  structural-only, and **9** higher-rank rows (SO(7)/Sp(6)/SO(8)) are out of the
  rank-2/3 anchor's scope and skipped. See
  `src/bcd/qspace_oracle_tests.rs` for the full coverage note.

The base SU(2) provider's stable public surface — authority fingerprint,
checked representation layer, and cache resource contract — is described under
[Provider contract](#provider-contract).

Not published to crates.io yet (blocked on the `tenferro-rs` publish); the git
dependency above is the supported path. See [Installation](#installation) and
[Feature flags](#feature-flags).

## More

- Theory primer (the objects the API computes): [`docs/theory.md`]docs/theory.md.
- Porting provenance and bibliography: [`docs/references.md`]docs/references.md.
- Gauge conventions: [`docs/gauge.md`]docs/gauge.md (SU(N)),
  [`docs/gauge_soN.md`]docs/gauge_soN.md (SO(N)/Sp(2N)).
- Fixture provenance and the oracle matrix: [`tools/README.md`]tools/README.md.
- Guard discipline (every port PR carries a guard inventory): issue
  [#15]https://github.com/Ryo-wtnb11/racah/issues/15.
- Build the docs locally with the same KaTeX math rendering as docs.rs:
  `RUSTDOCFLAGS="--html-in-header doc/katex-header.html" cargo doc --no-deps --all-features --open`.

## License

MIT OR Apache-2.0