Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,60 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed
- **Documented asymptotic cost of the 9j and Fano X corrected from
O(j⁴) to O(j³).** The old figure double-counted: the 9j term count
is O(j²) (an outer loop over O(j) values of the intermediate k, each
running Racah sums of O(j) terms) and each elementary bigint
operation costs O(j), which multiplies to O(j³), not O(j⁴). The
stated justification was wrong too: the per-k accumulators are
Θ(j)-bit, not "size-O(j²)", and the three big×big products per k are
O(j^1.58) under the Karatsuba path, so they never dominate the Racah
sums. Confirmed both from the loop structure in `src/wigner9j.c`
(k loops at the Pass-1/Pass-2 sites, inner term loop, per-k
combination) and by measurement: the log-log slope over j = 200 →
1000 is 2.98 for the 9j, 1.89 for the 3j and 1.85 for the 6j.
Corrected in `README.md`, `docs/reference.md`, the performance note
in `src/primes.h`, and the `wigner9j` / `fano_x` docstrings in
`src/python/wignernjmodule.c`. Documentation only; no code change,
so no behaviour or performance difference.
- **Bigint growth rate in the documentation corrected.** The claim
that intermediate bigints grow as "~j/ln 2 bits, since the LCM
denominator grows as the primorial" was wrong in both magnitude and
mechanism. The growth is linear in j but the measured rates are
≈ 9.5·j bits for the 6j tuple and ≈ 40·j bits for the 9j tuple (flat
to three significant figures over j = 32…2048), and it is driven by
the outer triangle coefficients, where Δ² = (j!)³/(3j+1)! cancels the
j ln j terms and leaves 3j log₂3 ≈ 4.75·j bits per Δ. The 6j Racah
sum contributes nothing at all, its `(z+1)!` numerator making every
term integral so the LCM denominator is exactly 1; the 3j sum has no
such numerator factorial, so its LCM survives at Θ(j ln j) bits and
the 3j is strictly O(j² log j) rather than O(j²).
- **Wall-clock horizon tables refreshed.** The table in
`docs/reference.md` and the horizons in `src/primes.h` followed from
the wrong exponent and were off by orders of magnitude at the top end
(3j/6j at j ~ 5000 listed as "minutes" against an actual few to tens
of milliseconds; 9j at j ~ 1000 listed as "~ hours" against an actual
~10 s). Replaced with figures extrapolated from the descriptor
paper's benchmark data using the corrected exponents, with a note
that the measured slopes below j ~ 200 are pre-asymptotic.

### Changed
- **Citation updated to the published journal reference.** The
descriptor paper has appeared as S. Lehtola, *libwignernj: a reusable
C/C++/Fortran/Python library for exact Wigner symbols and related
coefficients*, Comput. Phys. Commun. **329**, 110342 (2026),
[doi:10.1016/j.cpc.2026.110342](https://doi.org/10.1016/j.cpc.2026.110342).
`CITATION.cff` (`preferred-citation`), the `README.md` citation
section and BibTeX snippet, `docs/reference.md`, and the citation
comment in every public header (`wignernj.h`, `wignernj.hpp`,
`wignernj_mpfr.h`, `wignernj_quadmath.h`, `wignernj_quadmath.hpp`),
the Fortran module, and the Python package docstring now give the
journal reference instead of the arXiv preprint. The arXiv
identifier is retained in `CITATION.cff` and in the README BibTeX
entry as a secondary pointer to the preprint version. A `Paper` URL
is added to `[project.urls]` in `pyproject.toml`.

## [0.8.0] – 2026-06-29

### Changed
Expand Down
9 changes: 6 additions & 3 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,13 @@ preferred-citation:
- given-names: Susi
family-names: Lehtola
orcid: "https://orcid.org/0000-0001-6296-8103"
journal: "Computer Physics Communications"
volume: 329
start: 110342
year: 2026
doi: 10.48550/arXiv.2605.06634
url: "https://arxiv.org/abs/2605.06634"
doi: 10.1016/j.cpc.2026.110342
url: "https://doi.org/10.1016/j.cpc.2026.110342"
identifiers:
- type: other
value: "arXiv:2605.06634"
description: arXiv identifier
description: arXiv identifier of the preprint version
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

**libwignernj** — exact evaluation of Wigner 3j, 6j, 9j symbols, Clebsch-Gordan coefficients, Racah W coefficients, Fano X-coefficients, and Gaunt coefficients in C99, following the prime-factorization technique introduced for the angular-momentum coefficients by Dodds & Wiechers (Comput. Phys. Commun. 4, 268, 1972; doi:10.1016/0010-4655(72)90019-7) and refined in subsequent work, combined with the multiword-integer Racah sum of Johansson & Forssén (SIAM J. Sci. Comput. 38(1), A376–A384, 2016; doi:10.1137/15M1021908). The key property: all intermediate arithmetic is exact integer arithmetic; floating-point conversion happens only at the final step. Results are accurate to the last bit of the chosen output precision.

The library itself is described in the descriptor paper S. Lehtola, *libwignernj: a reusable C/C++/Fortran/Python library for exact Wigner symbols and related coefficients*, arXiv:2605.06634 (2026; doi:10.48550/arXiv.2605.06634) — this is the citation downstream users should give for libwignernj, recorded canonically in `CITATION.cff`.
The library itself is described in the descriptor paper S. Lehtola, *libwignernj: a reusable C/C++/Fortran/Python library for exact Wigner symbols and related coefficients*, Comput. Phys. Commun. **329**, 110342 (2026; doi:10.1016/j.cpc.2026.110342; preprint arXiv:2605.06634) — this is the citation downstream users should give for libwignernj, recorded canonically in `CITATION.cff`.

Language interfaces: C (primary), C++ (header-only wrapper `wignernj.hpp` that links against `libwignernj`), Python (CPython extension `wignernj` — `pip install wignernj`, `import wignernj`), Fortran 90 (`module wignernj` from `libwignernj_f03`, via `iso_c_binding`). The `wignernj` name is used uniformly across every binding; the only `wigner`-prefixed identifiers that survive are the C math-symbol functions (`wigner3j`, `wigner6j`, `wigner9j`), which name the mathematical Wigner symbols rather than the library namespace.

Expand Down
20 changes: 13 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,8 +299,11 @@ rebuilding, at the cost of a proportionally larger compiled-in table.
`MAX_FACTORIAL_ARG` in the same header is derived from the sieve limit, so
contributors do not need to keep the two values in sync by hand.

The 9j is also O(j⁴) in computation time; evaluations with j > a few hundred
can be slow. See [docs/reference.md](docs/reference.md#limitations) for details.
The 9j is also O(j³) in computation time (6j is O(j²), 3j is O(j² log j)), so
evaluations with j > a few hundred can be slow: roughly 13 ms at j ~ 100 and
10 s at j ~ 1000 on a single modern core. See
[docs/reference.md](docs/reference.md#limitations) for the derivation and a
fuller timing table.

## Documentation

Expand All @@ -312,19 +315,22 @@ per-language examples: [docs/reference.md](docs/reference.md).
If libwignernj contributes to published work, please cite:

> S. Lehtola, *libwignernj: a reusable C/C++/Fortran/Python library for
> exact Wigner symbols and related coefficients*, arXiv:2605.06634
> (2026). doi:[10.48550/arXiv.2605.06634](https://doi.org/10.48550/arXiv.2605.06634).
> exact Wigner symbols and related coefficients*, Comput. Phys. Commun.
> **329**, 110342 (2026).
> doi:[10.1016/j.cpc.2026.110342](https://doi.org/10.1016/j.cpc.2026.110342).

```bibtex
@article{Lehtola2026libwignernj,
author = {Lehtola, Susi},
title = {{libwignernj}: a reusable {C}/{C}++/{F}ortran/{P}ython
library for exact {W}igner symbols and related coefficients},
journal = {arXiv preprint},
journal = {Comput. Phys. Commun.},
volume = {329},
pages = {110342},
year = {2026},
doi = {10.1016/j.cpc.2026.110342},
eprint = {2605.06634},
archivePrefix = {arXiv},
doi = {10.48550/arXiv.2605.06634}
archivePrefix = {arXiv}
}
```

Expand Down
64 changes: 47 additions & 17 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ A376–A384, 2016

If libwignernj contributes to published work, please cite S. Lehtola,
*libwignernj: a reusable C/C++/Fortran/Python library for exact Wigner
symbols and related coefficients*, arXiv:2605.06634 (2026),
[doi:10.48550/arXiv.2605.06634](https://doi.org/10.48550/arXiv.2605.06634).
symbols and related coefficients*, Comput. Phys. Commun. **329**,
110342 (2026),
[doi:10.1016/j.cpc.2026.110342](https://doi.org/10.1016/j.cpc.2026.110342).
A machine-readable `CITATION.cff` is provided at the repository root.

---
Expand Down Expand Up @@ -849,24 +850,53 @@ sieve limit so the two stay in sync automatically.

The Racah summation has O(j) terms for 3j and 6j; the 9j outer loop over the
intermediate quantum number k adds another factor of O(j), so the *number* of
sum terms is O(j) for 3j/6j and O(j²) for 9j. Intermediate bigints grow
proportional to j/ln 2 bits (the LCM denominator grows as the primorial), so
each elementary bigint operation costs O(j). The resulting end-to-end cost
per symbol is

- **3j, 6j, CG, Racah W, complex / real Gaunt:** O(j²)
- **9j, Fano X:** O(j⁴) (each k step contains three multiplications of size-O(j²) bigints; Fano X delegates to the 9j pipeline)

Approximate wall-clock times on a modern single core:
sum terms is O(j) for 3j/6j and O(j²) for 9j.

The intermediate bigints grow **linearly** in j, so each elementary bigint
operation (`bigint_mul_u64`, `bigint_div_u64_exact`, `bigint_add` on the
running accumulator) costs O(j). The measured growth rates are ≈ 9.5·j bits
for the 6j tuple and ≈ 40·j bits for the 9j tuple, both constant to three
significant figures across j = 32…2048. The linear growth comes from the
outer triangle coefficients, where Δ² = (j!)³/(3j+1)! makes the j ln j terms
cancel and leaves 3j log₂3 ≈ 4.75·j bits per Δ; in the 6j Racah sum the
`(z+1)!` numerator makes every term integral, so the LCM denominator is
exactly 1 and contributes nothing. The 3j sum has no such numerator
factorial, so its LCM denominator survives at Θ(j ln j) bits.

Combining the term count with the per-operation cost, the end-to-end cost per
symbol is

- **3j:** O(j² ln j), the log coming from the surviving LCM denominator; in
practice indistinguishable from O(j²) over the usable range
- **6j, CG, Racah W, complex / real Gaunt:** O(j²)
- **9j, Fano X:** O(j³) — O(j) values of k, each running three Racah sums of
O(j) terms on Θ(j)-bit accumulators. Fano X delegates to the 9j pipeline
and inherits its cost exactly.

The three big×big products combining the per-k partial sums are O(j)-bit ×
O(j)-bit, i.e. O(j^1.58) under the Karatsuba path, so they never dominate the
Racah sums; this is why the effective exponent approaches 3 from below rather
than sitting on it.

Approximate wall-clock times on a single modern core, extrapolated with the
exponents above from the benchmark data of the descriptor paper (which covers
j = 1…200):

| Symbol | j ~ 100 | j ~ 1000 | j ~ 5000 |
|---|---|---|---|
| 3j / 6j | < 1 ms | < 1 s | minutes |
| 9j | ~ 1 ms | ~ hours | impractical |

The 9j is the most expensive because it multiplies three large bigints at every
k step. For the highest 9j angular momenta supported by the prime table
(j ~ 5000), a single evaluation may take hours or more.
| 3j | ~ 2 µs | ~ 0.1 ms | ~ 3 ms |
| 6j | ~ 17 µs | ~ 1 ms | ~ 15 ms |
| 9j | ~ 13 ms | ~ 10 s | ~ 20 min |

The 9j is by far the most expensive, being a sum over O(j) intermediate k of
three 6j-shaped Racah sums. At the highest 9j angular momenta the prime table
supports (j ~ 5000) a single evaluation takes on the order of tens of minutes.

Note that the *measured* log-log slopes below j ~ 200 are well under these
asymptotic exponents (≈ 1.3 for 3j and 6j, ≈ 2.3 for 9j), because the bigints
are still only a handful of 64-bit words there and the O(j) per-operation
factor has not fully engaged. The asymptotic exponents are reached higher up:
measuring j = 200 → 1000 gives slopes of 1.89 (3j), 1.85 (6j) and 2.98 (9j).

### Argument type

Expand Down
4 changes: 2 additions & 2 deletions include/wignernj.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
*
* Citation: if libwignernj contributes to published work, please cite
* S. Lehtola, "libwignernj: a reusable C/C++/Fortran/Python library
* for exact Wigner symbols and related coefficients", arXiv:2605.06634
* (2026), doi:10.48550/arXiv.2605.06634.
* for exact Wigner symbols and related coefficients", Comput. Phys.
* Commun. 329, 110342 (2026), doi:10.1016/j.cpc.2026.110342.
*
* Every coupling-coefficient routine (3j, 6j, 9j, Clebsch-Gordan,
* Racah W, Fano X, Gaunt, real-Gaunt) takes its angular-momentum
Expand Down
4 changes: 2 additions & 2 deletions include/wignernj.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
//
// Citation: if libwignernj contributes to published work, please cite
// S. Lehtola, "libwignernj: a reusable C/C++/Fortran/Python library
// for exact Wigner symbols and related coefficients", arXiv:2605.06634
// (2026), doi:10.48550/arXiv.2605.06634.
// for exact Wigner symbols and related coefficients", Comput. Phys.
// Commun. 329, 110342 (2026), doi:10.1016/j.cpc.2026.110342.
//
// "Header-only" means the C++ side has no separate .cpp -- every template and
// overload below is inline. It does NOT mean stand-alone: each specialisation
Expand Down
4 changes: 2 additions & 2 deletions include/wignernj_mpfr.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
*
* Citation: if libwignernj contributes to published work, please cite
* S. Lehtola, "libwignernj: a reusable C/C++/Fortran/Python library
* for exact Wigner symbols and related coefficients", arXiv:2605.06634
* (2026), doi:10.48550/arXiv.2605.06634.
* for exact Wigner symbols and related coefficients", Comput. Phys.
* Commun. 329, 110342 (2026), doi:10.1016/j.cpc.2026.110342.
*
* Include this header (in addition to wignernj.h) when MPFR output is needed.
* The library must have been built with -DWIGNERNJ_BUILD_MPFR=ON.
Expand Down
4 changes: 2 additions & 2 deletions include/wignernj_quadmath.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
*
* Citation: if libwignernj contributes to published work, please cite
* S. Lehtola, "libwignernj: a reusable C/C++/Fortran/Python library
* for exact Wigner symbols and related coefficients", arXiv:2605.06634
* (2026), doi:10.48550/arXiv.2605.06634.
* for exact Wigner symbols and related coefficients", Comput. Phys.
* Commun. 329, 110342 (2026), doi:10.1016/j.cpc.2026.110342.
*
* Built only when configured with -DWIGNERNJ_BUILD_QUADMATH=ON, which requires a
* compiler that exposes the __float128 type (GCC, Clang, Intel ICC/ICX
Expand Down
4 changes: 2 additions & 2 deletions include/wignernj_quadmath.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
//
// Citation: if libwignernj contributes to published work, please cite
// S. Lehtola, "libwignernj: a reusable C/C++/Fortran/Python library
// for exact Wigner symbols and related coefficients", arXiv:2605.06634
// (2026), doi:10.48550/arXiv.2605.06634.
// for exact Wigner symbols and related coefficients", Comput. Phys.
// Commun. 329, 110342 (2026), doi:10.1016/j.cpc.2026.110342.
//
// Usage:
// #include "wignernj_quadmath.hpp" // pulls in wignernj.hpp + wignernj_quadmath.h
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ classifiers = [

[project.urls]
Homepage = "https://github.com/susilehtola/libwignernj"
Paper = "https://doi.org/10.1016/j.cpc.2026.110342"
Repository = "https://github.com/susilehtola/libwignernj"
Documentation = "https://github.com/susilehtola/libwignernj/blob/main/docs/reference.md"
Changelog = "https://github.com/susilehtola/libwignernj/blob/main/CHANGELOG.md"
Expand Down
4 changes: 2 additions & 2 deletions src/fortran/wignernj_f90.F90
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
!
! Citation: if libwignernj contributes to published work, please cite
! S. Lehtola, "libwignernj: a reusable C/C++/Fortran/Python library
! for exact Wigner symbols and related coefficients", arXiv:2605.06634
! (2026), doi:10.48550/arXiv.2605.06634.
! for exact Wigner symbols and related coefficients", Comput. Phys.
! Commun. 329, 110342 (2026), doi:10.1016/j.cpc.2026.110342.
!
! The module provides:
! 1. Raw C-interop interfaces using 2*j integer arguments for every
Expand Down
25 changes: 19 additions & 6 deletions src/primes.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,30 @@
* Performance note
* ----------------
* The Racah sum has O(j) terms for 3j/6j and O(j^2) total for 9j (outer k
* loop times inner Racah sums). Intermediate bigints grow as ~j/ln2 bits,
* so each elementary bigint operation already costs O(j). Combined, the
* loop times inner Racah sums). Intermediate bigints grow LINEARLY in j
* (measured: ~9.5*j bits for the 6j tuple, ~40*j bits for the 9j tuple,
* both flat to three significant figures over j = 32..2048), so each
* elementary bigint operation already costs O(j). Combined, the
* end-to-end asymptotic per-symbol cost is:
*
* 3j / 6j / CG / Racah W / Gaunt / Gaunt-real : O(j^2)
* 9j / Fano X : O(j^4)
* 6j / CG / Racah W / Gaunt / Gaunt-real : O(j^2)
* 3j : O(j^2 log j)
* 9j / Fano X : O(j^3)
*
* The 3j picks up the extra log because its Racah sum has no numerator
* factorial to cancel the LCM denominator, which therefore survives at
* Theta(j log j) bits; the 6j (z+1)! numerator makes every term integral
* so its LCM is exactly 1. The 9j exponent is O(j) values of k times
* three Racah sums of O(j) terms on Theta(j)-bit accumulators; the three
* big-by-big products per k are O(j^1.58) under Karatsuba and never
* dominate. Fano X delegates to the 9j pipeline and inherits its cost.
*
* Practical performance horizons (order of magnitude, modern hardware):
*
* 3j / 6j : j ~ 1000 in milliseconds; j ~ 6000 in seconds
* 9j / Fano X : j ~ 100 in milliseconds; j ~ 1000 in minutes
* 3j : j ~ 1000 in ~0.1 ms; j ~ 5000 in ~3 ms
* 6j : j ~ 1000 in ~1 ms; j ~ 5000 in ~15 ms
* 9j / Fano X : j ~ 100 in ~13 ms; j ~ 1000 in ~10 s;
* j ~ 5000 (the ceiling) in tens of minutes
*/

/* PRIME_SIEVE_LIMIT, MAX_PRIME_COUNT, MAX_FACTORIAL_ARG. All three are
Expand Down
4 changes: 2 additions & 2 deletions src/python/wignernjmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ static const char wigner9j_doc[] =
" Angular-momentum quantum numbers. Pass an int for integer j,\n"
" a float (e.g. ``0.5``) or ``Fraction(1, 2)`` for half-integer\n"
" j. Equal-j ceiling is j <= 5004 with the default-build prime\n"
" table; the per-symbol cost scales as O(j^4).\n"
" table; the per-symbol cost scales as O(j^3).\n"
"precision : {'float', 'double'}, optional\n"
" IEEE 754 binary precision of the returned value. Default\n"
" 'double'.\n"
Expand Down Expand Up @@ -430,7 +430,7 @@ static const char fano_x_doc[] =
" Angular-momentum quantum numbers in the same row-major order\n"
" as the underlying 9j. Pass an int for integer j, a float or\n"
" Fraction for half-integer j. Equal-j ceiling is j <= 5004\n"
" (delegates to the 9j pipeline; per-symbol cost O(j^4)).\n"
" (delegates to the 9j pipeline; per-symbol cost O(j^3)).\n"
"precision : {'float', 'double'}, optional\n"
" IEEE 754 binary precision of the returned value. Default\n"
" 'double'.\n"
Expand Down
4 changes: 2 additions & 2 deletions wignernj/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@
--------
If wignernj contributes to published work, please cite
S. Lehtola, "libwignernj: a reusable C/C++/Fortran/Python library
for exact Wigner symbols and related coefficients", arXiv:2605.06634
(2026), doi:10.48550/arXiv.2605.06634.
for exact Wigner symbols and related coefficients", Comput. Phys.
Commun. 329, 110342 (2026), doi:10.1016/j.cpc.2026.110342.

Example::

Expand Down
Loading