Skip to content

Part 4 sessions 4-5: symbolic algebra, primes, and number theory - #4

Merged
Magic-Man-us merged 61 commits into
mainfrom
part-4-symbolic
Aug 26, 2026
Merged

Part 4 sessions 4-5: symbolic algebra, primes, and number theory#4
Magic-Man-us merged 61 commits into
mainfrom
part-4-symbolic

Conversation

@Magic-Man-us

Copy link
Copy Markdown
Owner

Completes Phase A and opens Phase B of docs/ROADMAP_PART4.md.

Module Content
exact/symbolic expression trees: precedence-climbing parser, Display/LaTeX, exact differentiation, simplification, expansion, polynomial extraction, Taylor series, stack-machine compiler, table-driven integration, limits, root finding, gradients and Hessians
discrete/primes three cross-checking sieves, deterministic Miller-Rabin over u64, BPSW for BigInt, Pollard rho and p−1, Fermat, factorization, prime counting
discrete/number_theory CRT for general moduli, multiplicative functions, primitive roots, discrete logarithms, Legendre/Jacobi, Tonelli-Shanks, Carmichael, Gaussian integers, Frobenius, Egyptian fractions, Zeckendorf, Diophantine solving

A rare branch that no test reached

After the primes tests passed, I replaced the entire strong Lucas test — the second half of BPSW — with return true. The whole suite stayed green. is_prime_bigint short-circuits below 2⁶² and otherwise runs Miller-Rabin on random bases first, so essentially every composite is rejected before Lucas is reached.

It is now tested directly: over the odd numbers below 20,000 it must accept every prime and exactly the strong Lucas pseudoprimes for Selfridge's parameters. The implementation reproduces that set — 5459, 5777, 10877, 16109, 18971 — computed independently rather than read off the code. The test also pins the property BPSW rests on: none of the six base-2 strong pseudoprimes below 20,000 (2047, 3277, 4033, 4681, 8321, 15841) is a Lucas pseudoprime, so each test catches what the other misses. The mutation now fails.

This is the third rare-branch gap found this way, after Knuth's add-back correction and the Lentz convergence threshold.

Other findings

Two missing simplifier rules. split_coeff didn't recognise negation, so its −1 stayed an opaque factor and x − x hashed under two keys instead of cancelling. And products of exponentials didn't combine, so exp(x)·exp(−x) couldn't reach 1.

Dual was missing atan, sinh, cosh. The roadmap asks that symbolic derivatives be cross-checked against forward-mode automatic differentiation; those functions made it impossible. Backfilled with tanh, tested against closed forms and cosh² − sinh² = 1.

Notes

π(10⁹) = 50,847,534 in 87 ms in a debug build, via the Lucy_Hedgehog recurrence over the distinct values of n/i — O(√n) state, so the roadmap's headline value is an ordinary test rather than something skipped for cost.

simplify(diff(sin²+cos²)) == 0 falls out of ordinary term collection, not a hard-coded trig rewrite: the product rule yields +2·cos·sin and −2·cos·sin, and they collect.

Three specification readings are documented in the commits: quadratic_diophantine_solve as a definite form, frobenius_number with a unit coin, and stern_brocot_nth indexed breadth-first.

Verification

2,981 lib tests, 107 property tests, cargo clippy --all-targets -- -D warnings clean. Each commit was verified by extracting its staged tree into a clean checkout and confirming the committed tree hash matched what was tested there.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi


Generated by Claude Code

claude added 3 commits August 24, 2026 16:58
Add exact/symbolic.rs: expression trees with a precedence-climbing
parser, Display and LaTeX rendering, evaluation, exact symbolic
differentiation, simplification, expansion, substitution, polynomial
extraction, Taylor series, a stack-machine compiler, table-driven
integration, numeric limits, root finding and critical points, gradients
and Hessians.

Simplification is a normaliser, not a prover. It folds constants,
flattens nested sums and products, collects like terms by their
non-numeric part, groups repeated bases into powers, and applies the
power, exponential and logarithm identities. That is enough for the
roadmap's headline property to fall out of arithmetic rather than a
special case: differentiating sin(x)^2 + cos(x)^2 produces +2*cos*sin and
-2*cos*sin, and the two collect to exactly zero.

Two rules were missing until the tests found them. split_coeff did not
recognise a negation, so its -1 stayed an opaque factor and x - x hashed
under two different keys instead of cancelling. And a product of
exponentials did not combine its arguments, so exp(x)*exp(-x) could not
reach 1; exponential factors are now gathered into a single argument sum,
which is the companion to the ln(exp(x)) rule that was already there.

Sums and products need opposite operand orders. Constants sort last in a
sum so a polynomial reads x^2 - 1, and first in a product so a term reads
5*x.

Also backfills atan, sinh, cosh and tanh on core::dual::Dual. They were
absent, which blocked cross-checking the symbolic derivative against
forward-mode automatic differentiation for those functions -- the
roadmap's stated property for this session. The new rules are tested
against their closed forms and against cosh^2 - sinh^2 = 1.

Verified by extracting the staged tree into a clean checkout: 2935 lib
tests, 107 property tests, and clippy --all-targets -D warnings pass
there, and the committed tree hash matches the one tested.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Add discrete/primes.rs: three sieves that cross-check each other,
deterministic Miller-Rabin for every u64, BPSW for BigInt, Pollard rho
and p-1, Fermat's method, complete factorization, and prime counting.

prime_count_meissel uses the Lucy_Hedgehog recurrence over the distinct
values of n/i rather than Meissel's own formula. The state holds O(sqrt n)
partial counts and each prime up to sqrt(n) sieves all of them at once,
which is what the name promises -- pi(n) without a sieve to n. pi(10^9) is
50847534 in 87 ms in a debug build, so the roadmap's headline value is an
ordinary test rather than something skipped for cost.

The Lucas half of BPSW needed testing on its own terms. is_prime_bigint
short-circuits below 2^62 and otherwise runs Miller-Rabin on random bases
first, which rejects essentially every composite before Lucas is reached:
replacing the whole strong Lucas test with `return true` left the entire
suite green. It is now tested directly against the odd numbers below
20000, where it must accept every prime and exactly the five strong Lucas
pseudoprimes for Selfridge's parameters. The implementation reproduces
that set, 5459, 5777, 10877, 16109 and 18971, which is a much stronger
statement than not crashing. The test also pins the complementarity BPSW
rests on: none of the six base-2 strong pseudoprimes below 20000 is a
Lucas pseudoprime, so each test catches what the other misses.

Factorization is checked by reconstruction on 2000 random values up to
10^12 plus the shapes that defeat a single method: a semiprime of two
near-equal factors, prime powers, and a prime beyond trial division.

number_theory.rs is committed empty; it lands in its own commit.

Verified by extracting the staged tree into a clean checkout: 2942 lib
tests, 107 property tests, and clippy --all-targets -D warnings pass
there, and the committed tree hash matches the one tested.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Add discrete/number_theory.rs: modular arithmetic and the Chinese
remainder theorem for general moduli, the multiplicative functions and
their Dirichlet relations, multiplicative order and primitive roots,
discrete logarithms by baby-step giant-step and Pohlig-Hellman, Legendre
and Jacobi symbols, Tonelli-Shanks, the Carmichael function, digit and
Collatz utilities, sums of two and four squares, primitive Pythagorean
triples by the Berggren tree, Gaussian integer factorization, the
Frobenius number, Egyptian fractions, Zeckendorf representations, Lucas
sequences, Diophantine solving, and Stern-Brocot and Farey navigation.

The tests assert the defining identities rather than sampled values:
sum of phi over the divisors of n is n, sum of mobius is one exactly at
n = 1, and mobius and phi are a Dirichlet inverse pair. Carmichael
numbers below 10^4 are exactly the known seven and each is verified
composite yet Fermat-pseudoprime to every coprime base. Tonelli-Shanks
is checked by squaring the root back, and None is checked to mean a
genuine non-residue rather than a failure to find one. The two discrete
logarithm routines agree with each other and with brute force.

Three notes on the specification. quadratic_diophantine_solve is read as
a x^2 + b y^2 = c, so an indefinite form returns an empty vector rather
than enumerating an infinite Pell family. frobenius_number returns zero
when a unit coin is present and None when the coins share a factor.
stern_brocot_nth indexes the tree breadth-first from the root 1/1.

Verified by extracting the staged tree into a clean checkout: 2981 lib
tests, 107 property tests, and clippy --all-targets -D warnings pass
there, and the committed tree hash matches the one tested.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Copilot AI lite review requested due to automatic review settings August 24, 2026 17:23

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

There are correctness/contract issues in the new discrete-math code paths (notably overflow in segmented sieving and factorization failure handling) that should be addressed before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR completes Phase A and starts Phase B of docs/ROADMAP_PART4.md by adding new discrete-math functionality (primes + number theory) and a new symbolic algebra subsystem, with tests that directly exercise previously untested rare branches (notably the Lucas half of BPSW).

Changes:

  • Add exact::symbolic expression trees with parsing/printing, simplification, differentiation, Taylor series, compilation, and numeric limit/solve helpers.
  • Add discrete::primes (sieves, deterministic u64 primality, BPSW for BigInt, factorization, prime counting) plus discrete::number_theory (CRT, arithmetic functions, discrete logs, residues, Diophantine/digit problems, etc.).
  • Extend core::Dual with atan/sinh/cosh/tanh to enable AD cross-checks for the new symbolic derivatives.
File summaries
File Description
src/lib.rs Exposes the new discrete module at the crate root.
src/discrete/mod.rs Adds the discrete-math module namespace.
src/discrete/primes.rs Implements sieves, primality (u64 + BigInt/BPSW), factorization, and prime counting with dedicated Lucas-branch tests.
src/discrete/number_theory.rs Implements CRT, arithmetic functions, discrete logs, residue tests/roots, and related number theory utilities with extensive tests.
src/exact/mod.rs Exposes the new symbolic module and re-exports Expr.
src/exact/symbolic.rs Adds the symbolic expression system (parse/print/simplify/diff/compile/etc.) and tests including AD cross-checks.
src/core/dual.rs Adds missing transcendental/hyperbolic Dual operations plus derivative/identity tests.
Review details

Suppressed comments (1)

src/discrete/primes.rs:536

  • factorize_bigint is documented as a complete factorization, but on pollard_rho_bigint failure it pushes m into the output even though it is known composite (it already failed is_prime_bigint). This makes the API contract unclear and can also make tests/callers flaky if rho occasionally fails to split a composite.
/// The complete factorization of a `BigInt`.
///
/// # Panics
/// Panics if `n` is not positive.
#[must_use]
pub fn factorize_bigint(n: &BigInt, rng: &mut Rng) -> Vec<(BigInt, u32)> {
    assert!(!n.is_negative() && !n.is_zero(), "factorization needs a positive integer");
  • Files reviewed: 7/7 changed files
  • Comments generated: 5
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/discrete/primes.rs
Comment on lines +48 to +54
// First multiple of p at or above lo, never below p^2.
let start = (lo.div_ceil(p) * p).max(p * p);
let mut m = start;
while m < hi {
is_p[(m - lo) as usize] = false;
m += p;
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed. lo.div_ceil(p) * p overflows at lo = u64::MAX for p = 7, 11 and 13, which in release wraps below lo and then underflows the m - lo index. It now rounds up by the remainder with checked_add, so every intermediate stays at or below the answer and a first multiple that does not fit means the window holds no multiple at all.

One note on testing it: the overflow is not reachable through sieve_segmented itself, because a window that high needs a base sieve to sqrt(u64::MAX) — four billion booleans. So the regression test pins the arithmetic directly, against the naive form wherever the naive form is valid and against the top of the range where it is not.


Generated by Claude Code

Comment thread src/discrete/primes.rs Outdated
Comment on lines +510 to +516
match pollard_rho(m) {
Some(d) => {
stack.push(d);
stack.push(m / d);
}
None => found.push(m),
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed for factorize, and the u64 contract is now provable: trial division to the square root is added as a guaranteed-terminating fallback when Pollard rho gives up. (Rho splits every composite below two million, so nothing observed reaches it, but the path existed.)

factorize_bigint at line 530 is different and I have not made it unconditional. Splitting a large composite has no cheap certain method, so there is no BigInt analogue of the trial-division fallback. It now tries rho three times — forty-eight independent polynomials — and the doc says plainly that an unsplit cofactor comes back as a single entry despite being known composite, and that a caller needing certainty should test each returned base with is_prime_bigint. Claiming completeness there would be false, so the contract was narrowed rather than the code made to promise something it cannot.


Generated by Claude Code

Comment on lines +1357 to +1365
if q == 1 {
return Rational::from_i64(p * order + 1, order);
}
let inv = mod_inverse_u64(p.rem_euclid(q) as u64, q as u64)
.expect("a reduced fraction has coprime parts");
let s0 = ((q as u64 - inv % q as u64) % q as u64) as i64;
let s = s0 + q * ((order - s0) / q);
let r = (1 + p * s) / q;
Rational::from_i64(r, s)
Comment thread src/exact/symbolic.rs
Comment on lines +161 to +176
pub fn variables(&self) -> Vec<String> {
let mut out = Vec::new();
fn walk(e: &Expr, out: &mut Vec<String>) {
if let Expr::Var(n) = e {
if !out.contains(n) {
out.push(n.clone());
}
}
for c in e.children() {
walk(c, out);
}
}
walk(self, &mut out);
out.sort();
out
}
Comment thread src/discrete/primes.rs Outdated
claude added 24 commits August 24, 2026 17:31
The Verify workflow has never run. Every one of its 18 runs since it was
added failed instantly with zero jobs, which is what a workflow that fails
to parse looks like from the API.

Two defects:

1. The Miri step's command was unquoted:

       run: cargo miri test --lib -- core:: linalg:: spatial::

   `core:: linalg::` is a colon followed by a space, so YAML parses it as a
   nested mapping and rejects the whole file. Locally:

       mapping values are not allowed here
         in ".github/workflows/verify.yml", line 50, column 44

   Every `run:` in the file is now quoted so a command containing a colon
   cannot recur as a parse error.

2. The clippy step combined `-D warnings` with `-W clippy::float_cmp` and
   three other float-accuracy lints, intending them as advisory. `-D
   warnings` denies the whole warning level, so naming an allow-by-default
   lint with `-W` promotes it to an error rather than softening it. That
   combination produces 3429 errors, mostly suboptimal_flops. Rewriting
   those expressions as mul_add changes rounding, so they need review one
   at a time rather than a blanket gate; the flags are removed and the
   reasoning recorded in the file.

Miri is also narrowed from three module filters to `core::` (27 tests).
Miri interprets at roughly a hundredth of native speed and the crate has no
`unsafe`, so it is a backstop, not the primary check.

Verified locally: the file parses, `cargo test --release --test properties`
passes 107 tests, and `cargo clippy --all-targets -- -D warnings` finishes
clean. The kani and miri jobs remain unverified — neither tool is installed
here, so CI will be their first real execution.

An earlier commit message claimed the strict lint job was "green rather
than red on arrival". The lint command did pass locally, but the workflow
containing it never parsed, so no job ever ran.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Add discrete/combinatorics.rs, partitions.rs, sequences.rs and
disjoint_set.rs.

combinatorics.rs covers binomials and multinomials, permutation and
combination enumeration, the permutation group, the classical counting
numbers, Burnside and Polya, and the named puzzles. Most tests compare a
formula against exhaustive enumeration of the objects it counts rather
than against a published table: Eulerian numbers against permutations
sorted by ascent count, Narayana numbers against Dyck paths sorted by
peak count, ballot numbers against every vote sequence, necklaces and
bracelets against brute-force orbits under rotation and reflection, and
all twelve entries of the twelvefold way against enumerated maps
quotiented four ways. The Hanoi move list is simulated on three real
pegs, the shuffle order is checked by shuffling until the deck returns,
and the magic squares are checked on every row, column and diagonal in
all three residue classes.

partitions.rs covers the pentagonal recurrence, Young diagrams and hook
lengths, and RSK. The hook length formula is checked against a direct
fill of the diagram; RSK is checked to be injective on S_n, to satisfy
Schuetzenberger's theorem that inverting the permutation swaps the two
tableaux, and to satisfy Schensted's, that the first row is the longest
increasing subsequence and the row count the longest decreasing one.

sequences.rs covers Taylor coefficients by Cauchy's integral, linear
recurrences by iteration and by matrix power, Berlekamp-Massey over Q
and over GF(2), and the named integer sequences. The recovered
recurrences are checked by regenerating the input rather than by
comparing coefficients, and the OGF error is checked against the exact
aliasing term r^N/(1-r^N) rather than a tolerance.

Four defects the tests found while writing them.

nth_permutation read the factoradic digits from the wrong end: it
divided by the position radix and used the remainder, which walks the
positions in reverse. It now divides by (n-1-j)! at position j.

The restricted growth string successor never reset the suffix, so
set_partitions_iter emitted one string per n rather than Bell(n).

binomial_u64 reported overflow for results that fit. The running product
before the division is C(n,k+1)*(k+1), up to k times the answer, so a
u64 accumulator overflows first. It accumulates in u128 and tests the
coefficient itself against u64::MAX, which makes None mean exactly "does
not fit".

random_permutation drew its swap index with next_u64() % (i+1). The
generator is an LCG modulo 2^64, where bit b has period 2^(b+1), so a
small modulus reads the shortest-period bits: the lowest merely
alternates. Shuffling six elements that way cycles through a handful of
arrangements instead of sampling the 720. It now takes the high half of
a widening multiply. The test that caught this counts how often each
symbol lands in each position over 20000 draws, which a validity-only
check would have passed.

Also fixes seven clippy errors that only the current stable raises, none
of which had ever been seen: verify.yml has never had a passing run, and
this environment's toolchain was four releases behind. Substantive
rather than suppressed -- an explicit counter loop becomes a range, a
comparator becomes sort_by_key, a manual checked division becomes
checked_div, chunks_exact(2) becomes as_chunks, a loop becomes while
let, and two no-op .max(0) calls on unsigned values are dropped. One of
those two was hiding a real fault: additive_evolving computed
(env.len() - 1) on an envelope it never checked was non-empty, which
underflows. Empty envelopes now contribute nothing.

Verified by extracting the staged tree into a clean checkout: 3065 lib
tests, 117 property tests, and clippy --all-targets -D warnings pass
there under rustc 1.98, the same version CI uses.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
The Verify workflow ran for the first time on the previous commit. Two of
its four jobs still failed; this addresses Miri.

Miri reported five failures in core::dual and core::interval, all of them
exact-value float assertions:

    assertion `left == right` failed: p' at -3
      left: -70.99999999999997
     right: -71.0

None of them is a defect. Miri does not call the host's sin, exp or powi;
it evaluates them itself, and is deliberately non-deterministic within the
slack the language allows for those operations. A test asserting an exact
double therefore fails under Miri whatever the code does. The five are
marked #[cfg_attr(miri, ignore)] with that reason recorded at each site.
They are unaffected everywhere else -- core:: still runs 27 tests, none
ignored, under a normal cargo test.

Also bounds the two slow jobs. Kani had been running for over fifty
minutes and was still going when this was written: the harnesses quantify
over whole f64 domains and several take a square root, which the solver
bit-blasts. With no bound a single slow harness holds a runner until the
six-hour job default. 90 minutes for Kani and 45 for Miri turn that into a
failure that names the problem instead. Miri's own 27 tests took twelve
minutes, which is recorded in the file so the scope choice is legible.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Add graph/core.rs and graph/paths.rs.

core.rs holds the representation and the structural queries: connected
and strongly connected components by an iterative Tarjan, condensation,
bipartiteness, topological order, bridges and articulation points,
Eulerian circuits and paths by Hierholzer, Hamiltonian paths by bitmask
dynamic programming, girth, the distance metrics, clustering and
transitivity, k-cores, the named and random generators, line and product
graphs, canonical forms and isomorphism, graph6, and the matrix-tree
theorem over the integers.

paths.rs holds Dijkstra, Bellman-Ford, Floyd-Warshall, Johnson, A*,
bidirectional search, Yen's k shortest paths, widest and minimax paths,
the DAG routines, three minimum spanning tree algorithms, the second-best
spanning tree, Dreyfus-Wagner Steiner trees, Held-Karp, the tour
heuristics, Christofides, and the Chinese postman.

The tests compare each algorithm against the definition it implements
rather than against stored answers. Strongly connected components are
checked against mutual reachability computed by transitive closure, one
pair at a time. Bridges and articulation points are checked by actually
removing each edge or vertex and recounting the components. The girth is
checked against exhaustive cycle search, the k-core against direct
peeling, and Hamiltonian path existence against every permutation.
Eulerian walks are checked to consume each edge exactly once with a
tally, not merely to have the right length. Held-Karp is checked against
every tour, Christofides against Held-Karp for its 1.5 bound, and the
Steiner tree against a brute force over every subset of Steiner points.
Cayley's formula is checked to n = 14, where n^(n-2) is past 2^53 and an
f64 determinant could not be exact.

One defect the tests found. The heap key ordered `f64` with
`partial_cmp(..).unwrap_or(Equal)`, which is the obvious thing to write
and is wrong: it makes a NaN key compare equal to every other key, so the
ordering is not transitive, `Ord`'s contract is broken, and the heap can
return items out of order. A NaN pushed among 1, 2 and 3 came back
second. `total_cmp` is a genuine total order and puts a positive NaN
above infinity, so a NaN weight now settles last instead of corrupting
the search. Every weight comparison in the module uses it.

Also corrects the Chinese postman's documented contract: an edgeless
graph is disconnected but has nothing to cross, so it returns the empty
route rather than failing. Only edges spanning more than one component
make the problem unsolvable.

Verified by extracting the staged tree into a clean checkout: 3109 lib
tests, 125 property tests, and clippy --all-targets -D warnings pass
there under rustc 1.98, and the committed tree hash matches the one
tested.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Five findings from the automated review on PR #4. Four are real; the
fifth is a contract that could not be made unconditional, and is now
documented accurately instead.

sieve_segmented computed the first multiple of p at or above lo as
`lo.div_ceil(p) * p`. That rounds lo up past u64::MAX when lo is within p
of the top, which wraps to a value below lo and then underflows the
`m - lo` index. Confirmed directly: it overflows at lo = u64::MAX for
p = 7, 11 and 13. It is not reachable through sieve_segmented itself,
because a window that high needs a base sieve to sqrt(u64::MAX), four
billion booleans, so no test can drive it; the arithmetic is pinned
directly instead. Rounding up by the remainder keeps every intermediate
at or below the answer, and a first multiple that still does not fit
means the window holds no multiple at all. The `p * p >= hi` guard is
also written with checked_mul now -- it cannot overflow for any prime the
base sieve produces, and saying so beats leaving the reader to check.

factorize is documented as a complete prime factorization but pushed the
cofactor into the result when Pollard rho gave up, even though it had
already failed is_prime_u64 and was therefore known composite. Rho splits
every composite below two million, so nothing observed reaches it, but
the path existed. Trial division to the square root is added as a
guaranteed-terminating fallback, which makes the guarantee provable
rather than overwhelmingly likely.

factorize_bigint has the same shape and no such fallback: splitting a
large composite has no cheap certain method. It now tries rho three
times, forty-eight independent polynomials in total, and the doc says
plainly that an unsplit cofactor comes back as a single entry and that a
caller needing certainty should test each base with is_prime_bigint.
Claiming completeness there would be false.

farey_next formed `p * order` and `p * s` as i64 products, which wrap
silently in release. They are formed in i128 and converted back with a
checked cast, so an out-of-range successor panics with a message rather
than returning a wrong fraction.

Expr::variables deduplicated by scanning a growing vector, which is
O(v^2) string comparisons in the number of distinct variables. It
collects into a BTreeSet, which also supplies the sort.

Also removes a dead `a = 0; let _ = a;` at the end of jacobi_bigint and
the `mut` it existed to justify.

Each fix carries a regression test that pins the behaviour: the
first-multiple arithmetic against the naive form wherever the naive form
is valid and against the top of the range where it is not, factorize
against primality of every returned base on prime powers and near-equal
semiprimes, farey_next against the neighbour identity r*q - p*s = 1
across F_1 to F_40 plus a should_panic for the overflow, and variables
against a 300-variable expression repeated twice.

3114 lib tests and 125 property tests pass, and clippy --all-targets
-D warnings is clean under rustc 1.98.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
lu_decompose_3x3_finite_or_err carried #[kani::unwind(6)] on a loop that
runs nine times, so the unwinding assertion could never discharge:

    Check 655: verification::linalg::lu_decompose_3x3_finite_or_err.unwind.0
      - Status: FAILURE
      - Description: "unwinding assertion loop 0"

That is not a property violation. Kani reports it when the bound is too
small to cover the loop, and the bound must exceed the trip count, so nine
iterations need ten. At ten the harness verifies: 655 checks, none failed,
68 seconds.

The harness has been wrong since it was written. Nobody could have seen
it, because verify.yml never parsed and so the Kani job never ran once.
This was found by installing Kani locally and timing every harness rather
than pushing another guess at CI.

That timing sweep also shows three harnesses exceeding a five-minute
budget -- bisection_result_is_inside_bracket, interval_mul_contains_corner
_products and mat3_inverse_never_divides_by_zero -- against 25 to 32
seconds for the ones that finish. Those are handled separately once the
full table is in; they quantify over whole f64 domains with symbolic
multiplication, division, or a 34-iteration loop, which CBMC has to
bit-blast.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Add graph/flow.rs and graph/matching.rs.

flow.rs holds Dinic and push-relabel, minimum cut, the global minimum cut
by Stoer-Wagner, minimum-cost maximum flow, circulations with demands and
lower bounds, both forms of Menger's theorem, Gomory-Hu trees by
Gusfield's construction, and the maximum-weight closure with its project
selection specialisation. matching.rs holds Hopcroft-Karp, the Hungarian
algorithm, the auction algorithm, Edmonds' blossom algorithm,
Gale-Shapley, Irving's stable roommates, Konig vertex covers and Hall's
condition.

The tests check each result against the definition rather than against a
stored answer. Max-flow is compared with min-cut and with a second flow
algorithm that never holds a valid flow until it finishes, and the flow
itself is checked for conservation at every interior vertex and capacity
on every arc. Stoer-Wagner is compared with the best over all C(n,2) s-t
cuts. Menger's theorem is checked by removing edges and vertices and
recounting. The Gomory-Hu tree is checked to encode every pairwise
minimum cut as the lightest edge on its tree path. Blossom matching is
checked by Berge's lemma -- that no augmenting path remains -- which is a
different statement from the algorithm's own search.

Four defects the tests found.

blossom_max_matching contracted blossoms but never rewired the parent
pointers through them. That rewiring is the lifting step and is the whole
difficulty of Edmonds' algorithm: without it an augmenting path traces
back out of the tree by the wrong edge, and the result was an asymmetric
pairing with m[1] = 4 but m[4] != 1. Rewritten with a proper lowest
common ancestor over blossom bases and a path-marking pass that rewires
parents along the odd cycle.

stable_roommates never terminated. Irving's rotation elimination has
y_i reject x_{i+1} and everyone below, but x_{i+1} is *defined* as the
last entry of y_i's list, so a non-strict comparison rejects nobody: no
list shrinks, no rotation is consumed, and the loop spins forever. The
test hung rather than failed. With the strict comparison the same test
finishes in 0.01 seconds.

circulation_with_demands had the super-source and super-sink inverted. A
vertex that must receive was wired to the source rather than the sink, so
every feasible instance came back None.

vertex_disjoint_paths gave each original edge capacity n. Two vertex-
disjoint paths cannot share an edge either, since sharing one means
sharing both its endpoints, so a single edge between adjacent vertices
reported two paths instead of one.

The auction algorithm was correct but reset its prices between epsilon
rounds, which discards the entire mechanism of epsilon scaling: each
round became a fresh auction and the last one, at the smallest epsilon,
ran the slowest variant there is, on the order of n^2 (cost range) / eps
bids. Its test took minutes. Carrying the prices over brings it to 0.04
seconds.

Also drops three n == 0 guards that were dead code. Matrix::zeros asserts
positive dimensions, so an empty cost matrix cannot be constructed.

Verified by extracting the staged tree into a clean checkout: 3133 lib
tests, 132 property tests, and clippy --all-targets -D warnings pass
there under rustc 1.98, and the committed tree hash matches the one
tested.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
The Kani job had never finished. It ran for over 100 minutes on one PR
and was still going. Rather than guess at it again from CI, Kani was
installed locally and all twenty harnesses were timed individually
against a five-minute budget. The result splits cleanly along what each
harness asserts:

    24s  normalize_angle_never_panics
    25s  displacement_is_finite_on_bounded_inputs
    25s  mat3_identity_inverse_is_identity
    25s  mean_panics_on_empty
    25s  projectile_range_panics_on_nonpositive_g
    25s  vec3_dot_with_self_nonnegative
    27s  kinetic_energy_nonnegative_for_nonnegative_mass
    28s  factorial_is_monotone_and_finite_below_171
    28s  projectile_range_never_panics_with_positive_g
    32s  escape_velocity_finite_nonnegative
    34s  shannon_entropy_never_panics_on_nonempty
    52s  vec3_normalized_never_produces_nan
    68s  lu_decompose_3x3_finite_or_err
    ---
   >300s bisection_result_is_inside_bracket
   >300s interval_mul_contains_corner_products
   >300s mat3_inverse_never_divides_by_zero
   >300s mean_of_bounded_slice_is_bounded
   >300s orbital_velocity_below_escape_velocity
   >300s ray_aabb_interval_ordered
   >300s variance_is_nonnegative

Everything above the line asserts panic-freedom, finiteness or a sign.
Everything below asserts a numeric relation between symbolic float
expressions. CBMC decides floating point by bit-blasting it into SAT:
proving a result is finite constrains few bits, while proving that one
symbolic product or quotient bounds another constrains the whole 53-bit
mantissa of every intermediate, and the instance stops being tractable.

That is the distinction the module's own documentation already drew --
transcendentals are modelled as unconstrained finite values, so those
harnesses prove panic-freedom rather than numeric bounds. The slow seven
ask for numeric bounds anyway.

They are kept rather than deleted: each states something true, and a
future Kani or solver may decide them. They now sit behind a `kani-slow`
cargo feature, off by default, and are runnable with

    cargo kani --features kani-slow --harness <name>

`cargo kani list` reports 13 harnesses by default and 20 with the
feature. The full default run was executed locally on Kani 0.67, the
version the action installs: 13 successfully verified, 0 failures, 67
seconds wall-clock with -j. The workflow now passes -j and its timeout
drops from 90 minutes to 45.

The measured table is recorded in src/verification/mod.rs so the split is
legible and revisitable rather than folklore.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Part 4 session 9, first half: src/graph/spectral.rs.

Laplacians (combinatorial and normalized) and their spectra, algebraic
connectivity and the Fiedler vector, spectral bisection and k-way
spectral clustering, Kirchhoff's spanning-tree count, PageRank, HITS,
eigenvector and Katz centrality, Brandes betweenness, closeness and
harmonic centrality, effective resistance and commute time, random-walk
stationary distributions and mixing time, Cheeger bounds and expander
testing, graph energy, the Estrada index, cospectrality, modularity,
Louvain, and label propagation.

Twenty tests, each against a closed form, an independent algorithm, or
the definition: the Laplacian's zero-eigenvalue multiplicity against a
component count, the spectra of K_n, C_n, P_n, the star and the
hypercube against their formulas, the normalized spectrum reaching two
exactly on a bipartite component, the matrix-tree count against exact
enumeration, betweenness of a star centre against (n-1)(n-2)/2, Foster's
theorem, commute time against 2m R, Cheeger's inequality against
brute-forced conductance, the K_{1,4} / C_4+K_1 cospectral pair, and
planted-partition recovery.

Three defects the tests found, all in the implementation:

  - eigenvector_centrality did not converge on a bipartite graph. The
    adjacency spectrum is symmetric about zero there, so the extreme
    eigenvalues tie in magnitude and power iteration flips between their
    eigenvectors forever, returning whichever phase the loop bound
    happened to stop in. Iterating on A + cI for a Gershgorin bound c
    breaks the tie and moves no eigenvector.

  - mixing_time_estimate filtered every transition eigenvalue of
    magnitude one out of its gap computation, which discarded exactly
    the evidence that the walk does not converge. It reported a finite
    mixing time for bipartite and disconnected graphs. It now drops one
    zero -- the Perron eigenvalue -- and takes the largest magnitude
    among the rest.

  - modularity counted degrees with weighted_degrees, which drops
    self-loops because the Laplacian does. Modularity does not: a loop
    adds two to a vertex's degree with nothing to cancel it. Louvain's
    contraction turns each community's internal edges into exactly such
    a loop, so the total edge weight shrank at every level of the
    recursion and the gain formula compared against the wrong 2m. On
    four cliques joined in a path it merged everything into one
    community and scored modularity zero. Now pinned by a test that
    contracts a random partition and requires modularity to be
    unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Part 4 session 9, second half: src/graph/coloring.rs.

Greedy colouring in four orders (natural, largest-first, degeneracy,
DSATUR), Welsh-Powell and its bound, the exact chromatic number by
bracket-and-search, the chromatic polynomial by memoised
deletion-contraction, Vizing edge colouring by the Misra-Gries
construction, a time-limited constraint search for k-colourability,
optimal interval-graph colouring, map colouring from adjacency lists,
Bron-Kerbosch with pivoting for maximal and maximum cliques, greedy and
exact independent sets, the vertex-cover two-approximation and its exact
counterpart, a greedy dominating set, and the Eades-Lin-Smyth feedback
arc set.

Eleven tests, each against a closed form, an independent algorithm, a
theorem, or exhaustive search:

  - Welsh-Powell's colour-class sweep is asserted equal to largest-first
    greedy, which is the theorem that the two procedures are one.
  - The chromatic polynomial is checked to count what it claims: its
    value at every k from zero to five equals the exhaustively counted
    proper k-colourings, on random graphs, plus the closed forms for
    cycles, trees and complete graphs, P(C5, 3) = 30, degree n, monic,
    and an x^(n-1) coefficient of minus the edge count.
  - Gallai's identity alpha + tau = n, the Caro-Wei bound on the greedy
    independent set, the factor of two on the cover approximation, and
    set cover's ln(n) + 1 on the dominating set.
  - Vizing's bound both ways: never more than Delta + 1, never fewer
    than Delta, and exactly Delta + 1 on every odd cycle. Separately
    stressed over twenty thousand random graphs of up to thirty-one
    vertices.
  - The crown graph, where the natural order takes four colours on a
    two-chromatic graph and DSATUR takes two, so the order really is the
    algorithm.

Three defects the tests found:

  - The Misra-Gries rotation chose its fan vertex without rechecking the
    fan. Inverting the alternating path recolours an edge at the hub and
    can occupy, at a fan vertex, the colour the fan property needed free
    there, so a prefix that was a fan before the inversion need not be
    one after. The prefix is now re-established first.

  - The path inversion wrote each edge back into the incidence table as
    it went. Two consecutive path edges meet at a vertex and exchange
    colours there, so the second erased the entry the first had just
    made and the table drifted out of step with the colouring, which
    then went silently improper. Clearing the whole path before writing
    any of it back fixes it.

  - The interval sweep spent a colour on an empty interval. A half-open
    interval whose ends coincide meets nothing, so charging it a colour
    of its own pushed the total past the maximum overlap, which is the
    one guarantee the sweep exists to make.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Part 4 session 9, third half: src/graph/layout.rs. Completes roadmap
item 6c.

Metric layouts (Kamada-Kawai, stress majorization by SMACOF from a
classical-scaling start, Fruchterman-Reingold, spectral), structural
layouts (circular, shell, Reingold-Tilford, Sugiyama), the stress
functional itself in two dimensions and in n, straight-line crossing
counting, biconnected decomposition, and planarity by Demoucron's path
addition -- which returns the faces, so the embedding comes with the
answer.

Thirteen tests. What each of them pins:

  - Stress majorization is required to be monotone at every round on
    every random graph, which is the only property that distinguishes
    majorization from gradient descent on the same objective. Kamada-
    Kawai is held to the weaker statement it can actually make: never
    worse than the drawing it started from. That is not free either --
    Newton on a non-convex energy steps uphill happily, so the step is
    accepted only when the energy falls.
  - A path laid out in one dimension must come out with consecutive
    vertices exactly one apart, and a nine-cycle in two dimensions must
    come out with every edge the same length.
  - The spectral layout's two coordinates are checked to satisfy L x =
    lambda x entry by entry for the right two eigenvalues, to be
    centred, and to be orthogonal to each other.
  - Reingold-Tilford is held to all three of its defining properties at
    once -- depth is the height, nothing at a depth overlaps, every
    parent is centred over its outermost children -- plus zero crossings
    and, on a complete binary tree, exact symmetry and leaves packed at
    exactly the separation.
  - Crossing counts against the closed form: a complete graph drawn in
    convex position has one crossing per four vertices, so K_n gives
    n choose 4, checked for n from three to nine.
  - Planarity against Kuratowski's graphs and their subdivisions, the
    planar families, and the sharp boundary that K5 and K3,3 become
    planar on the removal of any single edge. Then against an
    independent algorithm: enumerate every rotation system, trace its
    faces, and read off the genus. Demoucron and the genus computation
    agree on every graph small enough to enumerate.
  - The returned embedding is checked to be one: Euler's formula holds,
    every face is a closed walk in the graph, and every edge borders
    exactly two faces.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Five properties that hold between graph/spectral, graph/coloring,
graph/layout and graph/matching, which no one module's own tests can
state:

  - Koenig's theorem, with the matching from the blossom algorithm and
    the cover from maximum independent sets, on bipartite graphs; plus
    the odd cycles that show why the theorem needs bipartiteness, where
    the cover is exactly one larger than the matching.
  - Hoffman below and Wilf above: the adjacency spectrum brackets the
    chromatic number, computed by Jacobi rotations on one side and an
    exhaustive colouring search on the other, with both bounds shown
    tight on complete graphs.
  - The four colour theorem against the planarity test: every graph
    called planar must be four-colourable, must obey Euler's bound, and
    must have degeneracy at most five, so the smallest-last order never
    opens a sixth colour. Also asserts that some drawn planar graph
    actually needed four, so the property is not passing vacuously.
  - The chromatic number squeezed by the clique number below and by
    chi times alpha at least n above.
  - Every straight-line drawing of a graph the planarity test rejects
    must cross, checked across four layout algorithms -- a planarity
    test that wrongly said no would pass its own module's tests and fail
    here -- and the tree layout draws every random tree with no
    crossings at all.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Part 4 session 10, first half: src/codes/checksum.rs, a new codes/
module.

Parity, Fletcher-16 and -32, Adler-32, a parametric CRC covering every
named variant, CRC-32/CRC-16-CCITT/CRC-8 as named instances, the
reflected table form, Luhn, ISBN-10 and -13, Verhoeff, Damm, and Hamming
distance.

Ten tests. The point of a checksum is which errors it catches, so that
is what they assert rather than that the bytes come out the same twice:

  - Eight named CRCs against their published check values, and the
    table-driven CRC-32 against the bit-at-a-time one on random input.
  - Every burst of w bits or fewer is detected by a CRC of width w, on
    five parameter sets. This needed the bits numbered in the order the
    CRC actually consumes them -- most-significant first within a byte,
    or least-significant first when the CRC reflects its input -- since
    numbering them the other way scatters a window across up to twice
    its span and the theorem stops applying.
  - A zero-seeded CRC is linear over GF(2), which is the fact that makes
    the burst statement a statement about error patterns at all.
  - A generator with an even number of terms is divisible by x + 1 and
    so detects every odd number of bit errors. CRC-16/CCITT and
    CRC-8/SMBUS qualify; the test records that CRC-32 has fifteen terms
    and does not, rather than claiming a guarantee it lacks.
  - Fletcher and Adler against transposition. Both weight byte m by the
    number of bytes after it, so a swap moves the checksum by
    (d_i - d_j)(i - j); the test asserts Fletcher-16 catches the swap
    exactly when that survives its modulus of 255, and that Adler-32,
    whose modulus is the prime 65521, can never be reached by a product
    that small -- so it never misses one.
  - Luhn catches every single-digit error and every adjacent
    transposition except 09 against 90, which the test requires to
    occur rather than working around.
  - Verhoeff and Damm catch every single-digit error and every adjacent
    transposition, with no exception, over thousands of cases.
  - ISBN-10's prime modulus catches every transposition; ISBN-13's
    composite one misses exactly those of digits differing by five, and
    the test asserts both halves of that.
  - Hamming distance satisfies the metric axioms and is translation
    invariant, which is why a linear code's minimum distance is its
    minimum non-zero weight.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Part 4 session 10, second half: src/codes/block.rs. Completes roadmap
item 7a.

A bit-packed GF(2) matrix with elimination, rank, solving and kernel
bases; LinearCode carrying both a generator and a parity check matrix;
the Hamming, extended Hamming, repetition, single parity check,
Golay(23), Golay(24) and Reed-Muller families; syndrome decoding and the
explicit standard array; weight enumerators over exact integers; duals;
Hamming(7,4) in its classical bit layout; the Singleton, sphere-packing,
Gilbert-Varshamov and Plotkin bounds; Gallager's regular LDPC
construction with belief-propagation and bit-flipping decoders.

Nine tests:

  - GF(2) linear algebra against its definitions: the product entry by
    entry, transposition reversing products, rank-nullity, kernel
    vectors actually in the kernel and independent, rref idempotent
    with cleared pivot columns, and solve returning None exactly when
    the augmented rank exceeds the rank, which is Rouche-Capelli.
  - Hamming(7,4) exhaustively: all sixteen nibbles by all seven single
    errors corrected, all sixteen codewords pairwise at distance three
    or more, and every one of the 336 double errors flagged and
    miscorrected rather than passing silently.
  - Fourteen named codes against their stated length, dimension and
    distance, with G H' zero and H full rank in each.
  - The perfect codes: Hamming, Golay(23) and the odd repetition codes
    meet the sphere-packing bound with equality and nothing else does,
    every coset leader is within the correction radius, and the
    extended Golay code's covering radius is one past its correction
    radius.
  - Syndrome decoding corrects every error up to the radius and, on a
    perfect code, provably lands on a different codeword one past it,
    since there is nowhere else to land. The incremental search and the
    standard array agree.
  - MacWilliams's identity: the dual's weight enumerator computed by
    enumerating the dual equals the Krawtchouk transform of the
    primal's, exactly, for every code and every weight. Plus duality as
    an involution, Golay(24) self-dual, and the dual of repetition
    being the single parity check code.
  - The Golay(24) weight distribution against the classical
    1, 759, 2576, 759, 1.
  - The four bounds, with repetition meeting Singleton and Plotkin at
    once and the perfect codes meeting sphere-packing.
  - LDPC regularity, and belief propagation against bit flipping on a
    binary symmetric channel at two crossover probabilities chosen to
    straddle bit flipping's threshold, so the soft decoder's advantage
    shows as a difference in kind.

Two defects the tests found:

  - hamming_74_encode used the wrong coverage masks for two of the
    three parity bits: 0b0110011 and 0b0001111 rather than 0b1100110
    and 0b1111000. The syndrome then named the wrong position and a
    single error was "corrected" into a different nibble.

  - ldpc_decode_bitflip flipped every bit tied for the most unsatisfied
    checks. When the maximum is one, a large fraction of the block ties
    for it, so the decoder flipped them all and oscillated: on a
    length-240 code at five per cent crossover it ended with more
    errors than it started, 684 against 665. It now keeps the iterate
    with the fewest unsatisfied checks and returns that, which turns
    the oscillation into a plateau: 217 of 495 on the same channel, and
    4 of 181 at two per cent.

LinearCode::hamming and extended_hamming now carry their known distance
rather than searching for it, so the family reaches r = 8 instead of
stopping where enumerating 2^k codewords becomes impossible.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Part 4 session 11: src/codes/reed_solomon.rs and
src/codes/convolutional.rs. Completes roadmap item 7b.

reed_solomon.rs: GF(256) by logarithm tables, a prime field, a general
GF(2^m) with trace and minimal polynomials, systematic Reed-Solomon with
syndrome decoding through Berlekamp-Massey, Chien search and Forney's
formula, erasure decoding, the CCSDS, QR and disc parameter sets, binary
BCH codes, and an enumeration of the cyclic codes of a given length.

convolutional.rs: rate-1/n convolutional codes with hard and soft
Viterbi, free-distance search, puncturing and depuncturing, block,
random and quadratic-permutation interleavers, recursive systematic
encoders with trellis termination, max-log-MAP BCJR, turbo codes with
iterative decoding, Gaussian and binary symmetric channels, a bit error
rate sweep, and the capacity functions the whole subject is measured
against.

Twenty-five tests. The ones that carry weight:

  - GF(256) exhaustively: the powers of the primitive element hit every
    non-zero element exactly once, every element has the inverse it
    should, and multiplication is commutative across all 65536 pairs.
    GF(2^m) for m from two to eight likewise, with the trace shown to
    land in GF(2), to be additive, and to split the field exactly in
    half -- which is what being a surjective linear map onto GF(2)
    means -- and every minimal polynomial shown to vanish at its own
    root with a degree dividing m.
  - Reed-Solomon corrects every error pattern up to (n-k)/2 on six
    parameter sets, including the roadmap's RS(255, 223) against
    sixteen random byte errors.
  - A burst of 128 flipped bits confined to sixteen bytes is corrected
    exactly, which is the property the code is deployed for and which
    no bit-level code of that rate could match.
  - Erasures cost half what errors do: n-k erasures are recovered,
    which is the maximum distance separable property stated
    operationally, and n-k+1 is refused.
  - Past its capacity the decoder never returns a non-codeword: it
    corrects to something valid or reports failure.
  - BCH parameters against the classical table for nine (m, t) pairs,
    with each generator verified to divide x^n - 1.
  - The cyclic codes of length n are exactly the divisors of x^n - 1,
    so the count must be two to the power of the number of cyclotomic
    cosets. It is, for seven lengths.
  - Free distances against the published generator tables: 5, 6, 7, 8
    and 10 for constraint lengths three to seven.
  - A terminated convolutional code is a block code of minimum distance
    dfree, so Viterbi corrects any (dfree-1)/2 errors wherever they
    fall. Checked for every count up to that on six codes.
  - Soft decisions beat hard ones by the two decibels they are supposed
    to be worth.
  - Turbo iteration more than halves the error count against a single
    pass, at a signal-to-noise chosen to sit in the waterfall.
  - The Shannon limit for binary signalling against the values every
    coding paper quotes: 0.187 dB at rate 1/2, -0.495 at 1/3, 1.059 at
    2/3, and the -1.59 dB floor as the rate falls. The capacity behind
    those is integrated numerically, so the agreement is a real check
    on the integration.

One defect the tests found: the Reed-Solomon decoder had its position
convention backwards. Symbol zero is the leading coefficient of the
codeword polynomial, so position j carries x^(n-1-j) and its locator
value is alpha^(n-1-j); the Chien search instead read a root at
alpha^-i as naming position i, and Forney's formula carried a spurious
factor of alpha^i that belongs only to a generator whose roots start
elsewhere. The algebra verified against itself and corrected the wrong
symbols, so nothing short of an end-to-end decode would have caught it.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Part 4 session 12: src/codes/compression.rs and
src/codes/crypto_math.rs. Completes roadmap item 7c and section 7.

compression.rs: bit-level readers and writers, Huffman with canonical
codes, Shannon-Fano, arithmetic coding, LZ77, LZW, PackBits run lengths,
suffix and longest-common-prefix arrays, the longest repeated substring,
the Burrows-Wheeler transform and its inverse, move-to-front, delta
coding, byte entropy, and the normalized compression distance.

crypto_math.rs: RSA with Chinese-remainder decryption, Diffie-Hellman,
short Weierstrass curves over a prime field with the full group law and
the secp256k1 and P-256 constants, elliptic-curve Diffie-Hellman,
Shamir's secret sharing, the one-time pad, shift registers with the
Berlekamp-Massey attack on them, an avalanche measurement, the birthday
bound, and the classical cipher analyses. The module documents at length
that none of it is safe to deploy: every routine branches and indexes on
secrets, so the timing and the memory trace leak them.

Twenty tests. The ones that carry weight:

  - Huffman is checked to be optimal, not merely valid: for alphabets up
    to five, every length assignment satisfying Kraft is enumerated and
    none beats it. Plus Kraft with equality, prefix-freeness by
    construction, and Shannon's bound on both sides.
  - Arithmetic coding is required to land within two bytes of the
    message's own information content, and to beat Huffman where a whole
    bit per symbol is too coarse a unit.
  - The suffix array is checked against a naive sort of the suffixes and
    the LCP array against naive comparison, on a three-letter alphabet
    where ties are everywhere.
  - The Burrows-Wheeler transform is required to preserve the histogram,
    to halve the run count on repeated text, and to invert on periodic
    strings, where the rotations tie and the sort order is ambiguous.
  - The elliptic curve group law is checked to be a group -- identity,
    inverses, closure, commutativity across every pair, and associativity
    on a sample -- for four curves, and scalar multiplication against
    repeated addition, with Lagrange and Hasse both holding.
  - The published secp256k1 and P-256 generators are verified to lie on
    their curves, to have the stated prime order, and to make scalar
    multiplication a homomorphism at full 256-bit size.
  - Shamir: every k-subset of the shares reconstructs and no (k-1)-subset
    does, for every k and n up to five and seven.
  - Berlekamp-Massey is required not just to report the right register
    length but to predict the rest of the keystream, which is the actual
    attack.
  - Caesar is broken for all 26 shifts and Vigenere for four keys, with
    Kasiski's suggestions required to include a multiple of the true
    length.

Two defects the tests found:

  - shamir_reconstruct dropped the minus sign in the Lagrange numerator,
    computing the product of x_j where it needed the product of -x_j.
    The two agree when the threshold is odd, so a three-of-n split
    worked and a two-of-n split reconstructed the negation of the
    secret.

  - The shift register's step map is a bijection only when bit zero is
    tapped: without it the outgoing bit does not reach the feedback, two
    states share an image, and the register enters a cycle it never
    started on. lfsr_period returned zero there with nothing saying why.
    Both functions now document the requirement, and the test asserts
    the zero rather than treating it as a short period.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Part 4 session 13, first half: src/stochastic/markov.rs, a new
stochastic/ module.

MarkovChain: construction with validation, estimation from counts or
from an observed sequence, stationary distributions by linear solve,
n-step transitions, simulation, reachability and state classification,
absorption through the fundamental matrix, hitting times and
probabilities, Kac return times, the mean first passage matrix, total
variation distance, mixing time, the spectral gap through the additive
reversibilisation, detailed balance, entropy rate, exact sampling by
coupling from the past, and the PageRank chain of a graph.

Mcmc: Metropolis-Hastings, an adaptive variant, Gibbs, Hamiltonian Monte
Carlo, the no-U-turn sampler, slice sampling, parallel tempering,
autocorrelation time, effective sample size, the Gelman-Rubin statistic,
and simulated annealing.

Eleven tests:

  - The stationary distribution is checked to satisfy pi P = pi entry by
    entry and to be the limit of the matrix powers, and separately that
    a periodic chain still has one -- which is why it is solved rather
    than iterated.
  - Absorption against the gambler's ruin in closed form, for three
    board sizes and four win probabilities, both the ruin probability
    and the expected duration, with the hitting-time and
    hitting-probability routines required to agree by their own separate
    routes.
  - Kac's formula, the mean-first-passage recurrence, and the entropy
    rate bounded above by the stationary marginal's entropy.
  - A random walk on a graph is shown reversible with stationary
    distribution proportional to degree, and a biased directed cycle
    shown stationary but not reversible.
  - Coupling from the past is checked against the stationary
    distribution over thirty thousand exact draws, and the PageRank
    chain's stationary distribution against graph::spectral::pagerank.
  - Metropolis-Hastings recovers a Gaussian's mean within four standard
    errors, where the standard error is built from the effective sample
    size rather than the run length.
  - Hamiltonian and no-U-turn samplers are held to the mean, variance
    and correlation of a target with correlation 0.9, and required to
    achieve a higher effective sample size per draw than the
    random-walk proposal does.
  - Parallel tempering is required to cross between two modes separated
    by twelve nats, at least ten times as often as a single cold chain
    at the same proposal width, averaged over five starts.
  - The diagnostics are checked to separate the two cases they exist
    for: independent draws give an autocorrelation time near one and a
    Gelman-Rubin statistic within 0.02 of one, while a correlated walk
    gives a time above ten and chains started ten apart give a statistic
    above two.
  - Annealing escapes a local minimum in at least eighteen of twenty
    runs, and a frozen schedule provably does not.

One defect the tests found: the first version of the no-U-turn sampler
ran a trajectory until it turned back and took the last point. That is
not reversible -- the trajectory length depends on the state in a way
the acceptance rule does not account for -- and it sampled a
distribution with variance 242 where the target's was one. Replaced with
Hoffman and Gelman's doubling scheme, where the trajectory grows
forwards or backwards at random, every sub-trajectory is checked for the
turn, and the next state is drawn uniformly from what the slice variable
admits.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Part 4 session 13, second half: src/stochastic/hmm.rs. Completes roadmap
item 8.

Hmm with scaled forward and backward recursions, Viterbi, posteriors and
posterior decoding, Baum-Welch over multiple sequences, and simulation.
GaussianHmm with the same interface for continuous emissions. A Kalman
filter sequence runner, the Rauch-Tung-Striebel smoother, the lag-one
smoothed cross-covariances, expectation-maximisation for the noise
covariances, and a bootstrap particle filter with systematic resampling.

Eight tests:

  - The forward recursion is checked against literal enumeration of every
    state path, for sequence lengths one to nine, and Viterbi against the
    maximum over the same set -- so the recursions are verified against
    the definitions they are shortcuts for, not against each other. The
    posteriors are checked the same way, path by path.
  - Scaling is checked to do its job: a five-thousand-symbol sequence
    still gives a finite log-likelihood, which an unscaled recursion
    would not.
  - Viterbi recovers the exact state path on a chain whose emissions name
    the state, and decodes the occasionally-dishonest casino correctly
    more than seven times in ten.
  - Baum-Welch is checked round by round, not end to end, for the
    monotonicity that is its only guarantee; and required to find the
    loaded die's loaded face from a random start.
  - The Gaussian model recovers well-separated states more than
    ninety-five per cent of the time and learns means of -2 and 3 from a
    start at -0.5 and 0.5.
  - The smoother's variance is required to be no larger than the
    filter's at every step and every component, its total error smaller,
    and its final estimate identical to the filter's -- since there is no
    future for the last step to borrow from.
  - EM recovers a measurement noise of 0.8 from a start of 0.01, keeps
    both covariances symmetric and positive semidefinite, and leaves the
    truth alone when started there.
  - The particle filter is required to track the Kalman filter to within
    0.1 over sixty steps on a linear Gaussian model, where Kalman is
    exactly optimal, with the effective particle count restored by each
    resampling.

One defect the tests found: the expectation-maximisation step estimated
the noise covariances from the residuals of the smoothed states alone,
with no covariance terms. The smoothed states are shrunk towards each
other, so their residuals understate the process noise, and the
measurement residuals ignore the smoother's own uncertainty; from a
deliberately wrong start it converged to a measurement noise of 3.7
where the truth was 0.8. Replaced with the closed-form maximisation in
the three second-moment sums, which needs the lag-one smoothed
cross-covariances -- so those are now computed and exposed rather than
implicitly assumed to be zero.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Part 4 session 14: src/stochastic/sde.rs and
src/stochastic/point_process.rs. Completes roadmap item 9.

sde.rs: Brownian motion, bridges and their higher-dimensional forms;
geometric Brownian motion and Ornstein-Uhlenbeck stepped exactly;
Euler-Maruyama in one dimension and in n; Milstein; the Stratonovich
Heun scheme; an order-1.5 scheme for additive noise; measured
convergence orders; Cox-Ingersoll-Ross and Heston by full truncation;
Merton jump diffusion; stable sampling by Chambers-Mallows-Stuck;
fractional Brownian motion by Davies-Harte with a Cholesky fallback;
rescaled-range and detrended-fluctuation Hurst estimators; first passage
by simulation and in closed form, both density and distribution;
Feynman-Kac against Black-Scholes; Ito's isometry; BAOAB Langevin
dynamics; the Chang-Cooper Fokker-Planck solver and the stationary
density it should reach; Kramers' escape rate; stochastic resonance.

point_process.rs: Poisson processes in time, space and with a varying
rate; compound Poisson; Hawkes with intensity, likelihood, simulation
and fitting; renewal and Cox processes; Matern and Thomas cluster
processes; Ripley's K, Besag's L and the pair correlation with edge
correction; the Clark-Evans index; quadrat and Kolmogorov-Smirnov tests;
Galton-Watson branching with its extinction probability; Yule and
birth-death processes.

Twenty-two tests. The ones that carry weight:

  - Euler-Maruyama is measured at strong order one half and Milstein at
    one, path by path against the exact solution driven by the same
    noise. That is the only statement distinguishing a correct Milstein
    from an Euler step with a small extra term.
  - Heun and Euler-Maruyama are shown to disagree about the same
    equation in exactly the way Ito and Stratonovich do: for dX = X dW
    the Ito solution is a martingale with mean one and the Stratonovich
    one has mean exp(1/2). Both are measured.
  - The Fokker-Planck solver is required to conserve probability to
    within a part in a billion and to relax pointwise onto the closed-
    form stationary density, which is separately checked against the
    Gaussian it should be.
  - Langevin dynamics is held to equipartition in the velocity and the
    Boltzmann distribution in the position, both to within six per cent.
  - First passage times are compared to the exact distribution over the
    whole sample rather than bin by bin, and the two closed forms --
    density and distribution -- are checked against each other by finite
    difference. A downward drift is required to reach the barrier with
    probability exp(2 mu b / sigma squared) and no more.
  - Poisson counts are checked against the mass function term by term
    for thirty values of k, not merely on their mean and variance.
  - Clustered patterns are required to be detected as clustered by all
    four spatial diagnostics, and a random one by none of them.
  - Hawkes: the intensity against its definition, the stationary rate
    against mu over one minus the branching ratio, the likelihood shown
    to fall when any parameter is displaced, and the fit recovering the
    branching ratio to fifteen per cent.
  - Galton-Watson extinction against the generating function's smallest
    fixed point, including the critical case where a population that
    replaces itself on average still dies out with probability one.

Four defects the tests found:

  - cir_process clipped its state at zero, which is reflection rather
    than full truncation. Every reflection injects probability mass the
    exact process does not have, and the bias grew as the step was
    refined -- 0.03 at dt = 1e-3 and 0.10 at 5e-5 against a true mean of
    0.02. Full truncation keeps the internal state signed and truncates
    only inside the coefficients and on output. Heston's variance leg
    had the same fault.

  - levy_stable_sample skewed the opposite way from the convention it
    documented: a positive beta stretched the lower tail. Now pinned by
    a test in both directions.

  - hawkes_process resummed the whole history at every proposal, so a
    run with n events cost n squared and an eight-thousand-unit horizon
    did not finish. The excitation is now carried forward, which is one
    exponential per proposal.

  - first_passage_time_sim checked for crossings only at grid points and
    so missed the excursions that cross and return within one step,
    biasing the times upward. Conditional on the two endpoints the
    probability that the bridge between them touched the barrier has a
    closed form, so those crossings are now counted.

hurst_dfa now skips windows below sixteen points and documents that it
wants increments rather than an integrated series: removing a straight
line from eight points takes real fluctuation with it and biases the
exponent up by enough to make white noise look persistent.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
The Miri job has been hitting its 45-minute cap and being cancelled, so
the check has not reported since the workflow was repaired.

The build was never the problem -- the cached sysroot and crate finish in
under twenty seconds. The scope was. A libtest filter is a substring
match rather than a path prefix, so `core::` selected `graph::core::` and
`verification::core::` alongside the intended module. Those two arrived
after the job was written and quietly doubled it from 27 tests to 54,
adding brute-force combinatorial tests that Miri, interpreting at roughly
a hundredth of native speed, cannot finish: girth_matches_brute_force
alone ran for nine and a half minutes and
hamiltonian_path_matches_brute_force for five before the cancel landed.

Skipping `::core::` anchors the filter to the top level. A top-level path
starts with `core::` and so has nothing preceding it to match, while
every nested one does, which keeps the anchor working for any module
named `core` added later. That restores the intended 27 tests, which the
cancelled run showed completing in nine and a half minutes -- comfortably
inside the existing budget, so the timeout stays as it is.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
queueing.rs covers the birth-death queues and their closed forms -- M/M/1,
M/M/c, the finite-capacity and infinite-server variants, Erlang B and C,
Pollaczek-Khinchine and Kingman -- along with Jackson networks, an
event-driven simulator, and continuous-time Markov chains with transient
solutions by uniformization.

The tests lean on two theorems that hold across the whole module rather
than checking each formula in isolation. Little's law is a statement about
areas under a sample path, so L = lambda W has to hold for every model at
every admissible parameter, and the simulator measures its time averages
by integrating a merged event list rather than by invoking the law, which
makes the comparison evidence instead of arithmetic. Summing n p_n against
the reported mean ties each distribution to the means derived separately
from Erlang C. The models also have to nest: M/M/c at c = 1 is M/M/1,
M/M/1/K converges to M/M/1 as the buffer grows, and P-K with an
exponential second moment reproduces M/M/1 while P-K with a deterministic
one halves it. The simulator is checked against both, so M/D/1 separates
the general formula from the exponential special case. Priorities are
checked against Cobham's formula and against Kleinrock's conservation law,
which fixes sum rho_k Wq_k however the queue is ordered.

Three defects surfaced in writing those tests. Forming p_n as a^n / n!
overflows both halves independently and returns NaN for a tail the
recursion handles without trouble; every such expression is now a running
product. Uniformization at a long horizon failed in both directions at
once -- past Lt = 745 the leading Poisson weight underflows to zero, and
substituting the smallest subnormal made the recurrence climb by roughly
e^Lt on its way to the mode and overflow to infinity -- so the weights are
now carried as logarithms and materialised only over the window where they
are representable. The transient solver's state-space bound followed the
free drift even for a stable queue, which is positive-recurrent and stays
near its stationary law however long it runs.

timeseries.rs covers correlation structure, the two opposed stationarity
tests, ARMA/ARIMA/SARIMA, exponential smoothing, GARCH, Granger causality,
vector autoregressions, cointegration, seasonal decomposition, changepoint
detection, the entropy measures, IAAFT surrogates, and the local level
model.

The p-values for the Dickey-Fuller and KPSS statistics come from tabulated
quantiles of their own non-standard null distributions rather than from a
t or a chi-squared, which would be wrong rather than approximate; the
Engle-Granger table is kept separate from the plain Dickey-Fuller one
because the residual being tested is fitted rather than observed. The two
tests take opposite nulls, so the pair is checked on the same data in both
directions. Elsewhere the tests assert identities: the spectral density
integrates to the variance the impulse-response weights give, the partial
autocorrelation cuts off exactly past the autoregressive order, the
forecast band converges to the process standard deviation for a stationary
model and grows without bound for an integrated one, EWMA is GARCH at zero
intercept and unit persistence step for step, and every fit is checked by
whether the residuals it leaves are white.

Holt's method was seeded half a step ahead of where its recursion expects
the state, leaving a transient on data it should reproduce exactly -- an
exact straight line. Seeding one step before the data, with the seasonal
factors taken against the trend line rather than a flat mean, makes both
Holt and Holt-Winters exact from the first prediction on a noiseless
trend-plus-season. The Hannan-Rissanen pilot order was clamped to a range
that inverts on a short series, panicking where the function documents an
error.

The shared regression kernel solves the normal equations by Cholesky
rather than taking a QR of the design. The crate's Householder QR
accumulates an explicit n-by-n orthogonal factor, which is O(n^2 k) and
reached a billion operations for a long pilot autoregression; the normal
equations are O(n k^2) and every regression here has far more rows than
columns. That cut the module's tests from 74 seconds to 1. The trade --
squaring the condition number -- is documented, and the substitution is
checked against the QR it replaces.

The property suite adds the cross-module theorems none of these modules
can check alone: Little's law across every closed form at once, the two
independent routes from a birth-death chain to its stationary
distribution, the identity between a continuous-time chain and its
embedded discrete one reweighted by holding times, and the averaged
periodogram of a simulated ARMA against the density the model computes
from its coefficients -- an FFT that knows nothing about ARMA models
against a formula that knows nothing about the FFT. Random stationary
autoregressions are drawn through the Barndorff-Nielsen-Schou map from
partial autocorrelations, which lands on the stationary region by
construction.

3,390 library tests and 144 property tests pass; clippy is clean under
--all-targets -D warnings.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
rmt.rs covers the classical ensembles -- GOE, GUE, Ginibre, Wishart -- the
limiting spectral laws they converge to, the local statistics that
distinguish a correlated spectrum from uncorrelated levels, and
Marchenko-Pastur denoising of a sample correlation matrix.

The tests check universality rather than arithmetic. A GOE spectrum is
matched against the semicircle by a Kolmogorov-Smirnov distance and its
second moment against the value the scaling fixes exactly; a Wishart
spectrum has to sit inside the Marchenko-Pastur band, which at an aspect
ratio of a quarter spreads purely noisy eigenvalues over a factor of four
even though every population eigenvalue is one. The spacing statistics
carry the real content: the ratio of adjacent gaps needs no unfolding,
since the local density cancels between numerator and denominator, and it
separates 0.5307 for the orthogonal class from 2 ln 2 - 1 for independent
points -- and separates the orthogonal class from the unitary one at
0.5996, which is the whole content of the symmetry classification.
Spectral rigidity separates the same two cases far more sharply, L/15
against something growing like a logarithm. Every surmise is checked to
integrate to one with unit mean, which constrains the prefactors rather
than merely the shape.

Two of these needed care in the test rather than the code. The spectral
densities vanish like a square root at both edges, and Marchenko-Pastur at
unit aspect ratio picks up an inverse-square-root singularity where its
lower edge reaches zero; midpoint quadrature converges at only h^(1/2)
there, so a uniform grid measures the quadrature and not the density. A
sine substitution cancels both. Separately, the Jacobi eigen-solver is
cubic per sweep, so the module's tests were sized down from matrices whose
cost dominated the suite; one test also carried a pooled spectrum left
over from an earlier approach that was built, sorted, and never read.

extreme.rs covers the generalised extreme value and generalised Pareto
families with maximum-likelihood fits, return levels, the Hill estimator,
the Ferro-Segers extremal index, rank correlations, and five copula
families with sampling, Kendall-tau inversion, tail dependence and the
Pickands dependence function.

The two routes into a tail are checked against each other: fitting a GEV
to block maxima and a generalised Pareto to threshold exceedances of the
same data recovers the same shape parameter, which is
Pickands-Balkema-de Haan and the reason the threshold route is worth
preferring. Return level and return period are checked as exact inverses;
the mean excess is checked to be linear in the threshold with slope
xi/(1-xi), which is what makes it a threshold diagnostic; the extremal
index is checked against a moving maximum over a window of m, whose index
is 1/m. The copula tests assert uniform margins for every sampler, recover
each family's parameter by inverting Kendall's tau, and check that the
Pickands function stays between max(t, 1-t) and 1.

Tail dependence needed the tests restated. A coefficient that is zero only
asymptotically is not zero at a finite quantile: Gumbel's lower
coefficient at q = 0.01 with theta = 2 is q^(2^(1/2) - 1), about 0.15, and
the Gaussian's decays only logarithmically. So the tests assert the exact
finite-q values where a closed form exists, and otherwise assert the
asymptotic statement directly -- that the coefficient falls as the
quantile tightens. The Gaussian and t comparison is put the same way: at a
loose quantile the two are nearly indistinguishable, and the gap widens
monotonically as the quantile tightens, with the t settling on its exact
limit while the Gaussian decays away. That is the failure mode a
correlation-based risk model cannot see, and stating it as a widening gap
rather than a fixed threshold is what makes the test mean it.

Kendall's tau compared every pair, which is the definition but costs
O(n^2) -- minutes on the sample sizes a copula fit wants, and 41 seconds
of the module's own tests. Sorting by x and counting inversions in the
resulting y sequence by merge sort gives the same discordance count in
O(n log n), with the tie corrections handled by Knight's formula. That
took the tests to under a second, and the replacement is pinned against
the pair-counting definition over randomised samples carrying ties in one
coordinate, the other, and both.

The property suite adds the identities that cross modules: the even
moments of Wigner's semicircle are the Catalan numbers, computed here by a
numerical integral of a density and there by an exact integer recurrence
in discrete::combinatorics, with nothing shared between the two routes.
Alongside them, a noise covariance staying inside its predicted band
across random aspect ratios, denoising preserving the trace while never
widening a spectrum, and copula parameters surviving a round trip through
a rank statistic that ignores the margins entirely.

3,433 library tests and 153 property tests pass; clippy is clean under
--all-targets -D warnings.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Adds the simplex method, a primal-dual interior point method, LP duality,
sensitivity analysis, a small modelling language, and the classical models
that reduce to a linear program: diet, production planning,
transportation, zero-sum games, the Chebyshev centre, and L1 and minimax
regression.

Placed at optimization/lp.rs rather than the roadmap's opt/lp.rs. The
crate already has an optimization module covering exactly this subject
area, and two top-level modules named opt and optimization would be a
lasting wart for the sake of matching a path. Every signature the roadmap
names is present; only the directory differs.

Duality is the organising idea and the module commits to one convention,
stated in the module documentation and adhered to throughout: duals[i] is
the derivative of the reported objective with respect to b[i]. That is the
definition that makes shadow prices mean what people expect and makes
sensitivity ranges checkable, and it is what the tests check -- perturbing
a right-hand side within its reported range moves the objective by exactly
the shadow price times the perturbation, over hundreds of random programs
and both signs of perturbation.

The tests lean on theorems that are exact rather than asymptotic, so they
demand equality. Strong duality is an equation: the objective equals the
right-hand side dotted with the shadow prices, and nothing in the solver
imposes it -- the duals are read off the optimal basis and the objective
off the primal solution. Complementary slackness holds at every optimal
basis, in both directions: a variable in use has zero reduced cost, and a
slack row has zero shadow price. The two solvers walk the feasible region
in completely different ways -- one along the boundary vertex to vertex,
the other through the middle, never reaching a vertex -- and share only the
standardisation step, so their agreement on every instance is the
strongest available check on either. The dual of the dual returns the
primal value; weak duality brackets every feasible pair; and the minimax
theorem falls out as a corollary, since the two players' programs are
duals of one another.

Bland's rule is used throughout rather than a faster pivoting rule.
Degeneracy is not hypothetical -- Beale's example returns to its starting
basis after six pivots under Dantzig's rule and cycles forever -- and
Bland's rule cannot cycle because the basis sequence it visits is
lexicographically monotone. Beale's example is in the tests, alongside a
problem where three constraints meet at a single vertex so that the ratio
test ties at every pivot.

Two things the tests caught that were wrong in the tests rather than the
code. The Chebyshev centre is not unique: in a box four wide and six tall
the largest inscribed circle has radius two and slides freely up and down,
so only the coordinates the touching faces pin down are determined. The
radius is unique and the fit is checked instead by the properties that do
hold -- the ball fits inside every face, and touches at least one, so
nothing larger fits. Separately, a Matrix cannot be constructed with a zero
dimension, which made two emptiness guards unreachable; an untestable
branch is worse than none, so they were removed rather than left as
decoration.

Total unimodularity gets a test of its own: the transportation problem's
constraint matrix is totally unimodular, so integral supplies and demands
give an integral optimum straight from the simplex method, with no branch
and bound anywhere. The regression fits are checked against each other and
against ordinary least squares under all three norms -- each must win under
the norm it minimises -- and the minimax fit is checked to be pinned by at
least three residuals of alternating sign, which is what distinguishes a
minimax fit from merely a fit with a large residual.

3,463 library tests and 162 property tests pass; clippy is clean under
--all-targets -D warnings.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
integer.rs adds branch and bound over the linear relaxation, Chvatal-Gomory
rounding cuts, the knapsack family, subset sum and partition, bin packing,
set cover, facility location, cutting stock by column generation, the
dynamic programming classics, and exact cover by Algorithm X with sudoku,
n-queens and AC-3 on top of it. network.rs adds transshipment, the critical
path method and PERT, linear programming formulations of shortest path and
maximum flow, Clarke-Wright vehicle routing, and the sequencing rules.

Every method is checked against an independent exact answer rather than
against itself. The knapsack table and the branch-and-bound search tree
share no code and must agree on every instance, and both are checked
against enumeration. Each scheduling rule is checked against every
permutation of the jobs, on the objective it provably optimises and on
nothing else -- and shortest-processing-time is checked to be genuinely
worse than earliest-due-date on maximum lateness, because a rule that
looked good on every objective would mean the test was not measuring what
it claimed. The greedy methods are checked against their proven ratios,
against exact answers: first-fit-decreasing within 11/9 OPT + 6/9, set
cover within the harmonic number, longest-processing-time within
4/3 - 1/(3m). A bound nobody tests against an optimum is not a guarantee.

The two linear programming formulations exist to cross-check the graph
module. Shortest path and maximum flow both have totally unimodular
constraint matrices, so a general simplex solver answers them exactly, and
it agrees with Dijkstra and with the augmenting-path search over hundreds
of random graphs. The two routes share nothing but the graph itself.

The first cut generator was wrong. It rounded a variable's bound toward
whichever side the objective was not pushing, which is a branch rather than
a cut: it removed integer solutions, and the test caught it discarding an
optimum of 34 while reporting a bound of 33. Replaced with the
Chvatal-Gomory rounding cut, whose validity rests on an argument that can
be stated -- scale a row, round the coefficients down, and the left-hand
side becomes an integer, so it is bounded by the floor of the right -- and
which therefore holds for every non-negative integer point. The test now
enumerates every integer point in the box and checks that none is removed.

Separately, Miri went red on the previous commit, in
core::interval::tests::test_sqrt_exp and not in anything this change
touches. Reproduced locally and deterministic, so it is a newer nightly
Miri rather than a flake, and it will recur every run until addressed.
Instrumenting it showed two calls to exp(1.0) returning values several ulps
apart, while Interval::exp widens its bounds by a fixed two. That widening
silently assumes the host evaluates the elementary functions to within one
ulp -- true of every real platform's libm, not required by the language,
and deliberately untrue under Miri. So the enclosures this module returns
are rigorous conditional on that assumption, which is now documented on the
widening helpers where it belongs.

The test itself joins the five siblings in the same file that already carry
cfg_attr(miri, ignore) for the same root cause. It still runs on every
commit in the ordinary test job; what the attribute records is that one
interpreter deliberately violates the test's precondition. Widening every
interval for all users to accommodate an interpreter that randomises would
make the library worse, not more rigorous.

3,504 library tests and 170 property tests pass; clippy is clean under
--all-targets -D warnings.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
claude added 28 commits August 25, 2026 05:48
Adds spin.rs (spin operators at any spin, coherent states, XXZ chains
with a matrix-free Hamiltonian, Lanczos, Krylov time evolution, the
transverse-field Ising chain and its Jordan-Wigner solution, Larmor and
Rabi dynamics, Bloch equations, free induction decay) and solid_state.rs
(tight binding, SSH, graphene, Kronig-Penney, densities of states,
occupations, Debye and Einstein and Sommerfeld heat capacities, phonons,
Landau levels, Hofstadter, transport, semiconductors, BCS, Josephson,
Anderson localisation).

Three defects the tests caught.

Lanczos returned correct eigenvalues and wrong eigenvectors.
eigen_symmetric_tridiagonal hands back each eigenvector as a *row*, and I
indexed it as a column. The eigenvalues are unaffected -- they come from
the tridiagonal projection, which the indexing does not touch -- so the
error was invisible in every energy and showed only in the residual. It
had propagated into the Krylov time step as well, where it made the
evolution non-unitary.

Larmor precession turned the wrong way. dM/dt = gamma M x B puts the
angular velocity at -gamma B, so a positive gyromagnetic ratio precesses
clockwise seen from +z; the closed form turned anticlockwise and
disagreed with the Bloch integrator in the same module.

effective_mass_from_band called every band flat. The guard compared the
curvature against an absolute 1e-30, and a real band in SI units curves
by about 1e-38, so the function refused the case it exists for. The
threshold is relative to the band's own scale now.

Four test premises of mine were wrong. The magnon band tops out at 4 j s,
not 2 j s. The critical Ising chain is *not* the most entangled one at a
given size -- deep in the ordered phase the ground state is the
symmetry-broken cat and carries a full bit across every cut, more than
the critical chain -- so the test now checks the *scaling*, which is what
distinguishes criticality: the half-chain entropy climbs at c/6 = 1/12 of
a bit per doubling while both phases saturate. The free induction decay
test asked for a tail below 1e-3 from a record that was truncated rather
than decayed. And I read a 20 per cent gap in the Anderson localisation
ratio at the band centre as the Kappus-Wegner anomaly; at 150,000 sites
instead of 20,000 both ratios are 4.0 and the gap was sampling noise, so
the claim is gone and the test says why the chain has to be long.

Adds tests/properties/quantum_matter_props.rs: the angular momentum
algebra at every representation up to spin eight, coherent states
pointing where they were asked to, Lanczos eigenpairs certified by their
own residual and by the variational principle, Krylov evolution unitary
at every step size, the Ising chain against its free-fermion energy at
every field, SSH edge counts against the bulk winding number on random
couplings, and the occupation functions' exact symmetries -- including
the point at which the boson-fermion gap falls below what a double can
represent, where the test stops demanding a strict inequality.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Roadmap section 16. `statistical_mechanics.rs` becomes a directory so the
new material can sit beside the thermodynamics already there; the roadmap
calls the home `statmech/`, but every earlier session has kept new modules
under the existing names and this follows that.

ising.rs -- Ising2D with Metropolis, heat-bath and Wolff cluster updates,
correlation functions and lengths, autocorrelation times, the Onsager
magnetisation and energy, the exact one-dimensional chain by transfer
matrix, brute-force enumeration for small systems, Potts and XY models with
plaquette vorticity, Wang-Landau sampling with canonical reconstruction,
parallel tempering, Binder crossings and a fluctuation-dissipation check.

lattice_models.rs -- site and bond percolation with a disjoint-set spanning
test, cluster size distributions, exact self-avoiding walk enumeration and
Rosenbluth sampling, the connective constant, lattice random walks and
Polya return probabilities, Flory exponents, Kasteleyn dimer counts, KPZ
ballistic deposition with interface widths and growth exponents, the
Abelian sandpile, and the Clauset power-law fit.

Defects found and fixed while writing the tests:

- `sample` drove the chain with a *state-dependent* stopping rule: a "sweep"
  of Wolff steps ran until the flipped total reached the lattice size, so a
  measurement was always taken just after a large cluster. That biases the
  sample toward ordered configurations, and it showed: -3.90 per site
  against the exact -3.29. Each measurement now follows a fixed number of
  updates, and `autocorrelation_time` returns the work per update alongside
  tau so the two updates can still be compared honestly.

- `correlation_length_estimate` fitted a length from a single snapshot,
  which is a fit to noise -- the spread of the correlation function at large
  separation is comparable to its mean and does not shrink with the lattice.
  Added `sample_correlations` to average over a run, and the estimate now
  takes the ensemble average and its background.

- `connective_constant_estimate` read consecutive ratios, but the walk
  counts alternate with parity, so consecutive-ratio Richardson made the
  answer worse rather than better. Now averaged over a parity pair before
  extrapolating: 2.63928 against the true 2.638158.

Defects in the tests themselves, recorded rather than quietly patched:

- The negative control on the connective constant demanded the raw ratio be
  0.05 away from the truth; it is 0.0494. It now compares the two errors
  directly, which is what it was meant to show.

- `saw < 4^n` is not strict at n = 1: the first step cannot revisit, so the
  counts coincide there.

- The Potts relation was written `2 * ising_tc_exact() / 2`, which is just
  `ising_tc_exact()`. The relation is `ising_tc_exact() / 2`.

- Kasteleyn's formula is exact only to rounding, so the 1-by-2 count comes
  back as 0.9999999999999999 and a strict `>= 1.0` fails.

- The one-dimensional magnetisation saturates in `beta * h`, not in `h`; at
  beta = 0.05 a field of 50 reaches 0.988, not 1.

- The weighted and unweighted polymer means happened to coincide at 388/6
  for the weights I first picked, so the control could not have failed.

tests/properties/statmech_props.rs -- 29 property tests. The strong ones
are structural rather than statistical: percolation is checked for
monotonicity under a common random number, which couples two lattices
exactly and makes spanning monotone lattice by lattice rather than on
average; the total vorticity of an XY torus is checked to vanish before and
after thermalisation, since windings can only be created in pairs; the
dimer count is checked against the Fibonacci recurrence for a two-row
strip; the Monte Carlo sampler is checked against an exact enumeration of
the same sixteen-spin lattice under both updates; and the density of states
is checked against that enumeration at five temperatures, with the
fluctuation heat capacity checked against a finite difference of the mean
energy -- two independent routes that agree only if both are right.

3717 lib tests and 267 property tests pass in debug; clippy is clean under
--all-targets -D warnings.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Roadmap section 17, first half. `statistical_mechanics/md.rs` carries the
pair potentials, a cell-list force evaluation with the minimum-image
convention, velocity Verlet, Berendsen/Nose-Hoover/Langevin thermostats and
a Berendsen barostat, the radial distribution function, structure factor,
Lindemann ratio, mean squared displacement, velocity autocorrelation and
vibrational spectrum, plus Ewald summation, the second virial coefficient,
kinetic-theory lengths, a Green-Kubo transport integral, WHAM umbrella
sampling, steered pulling and the Jarzynski estimator.

Everything is in reduced Lennard-Jones units with k_B = 1, stated in the
module header and in `lj_reduced_units_note`. The equations of motion are
consistent under any consistent choice of units and silently wrong under an
inconsistent one, so there is no SI path through this module at all.

Two deviations from the roadmap's signatures, both recorded in the source.
`MdSystem` carries its own cell list rather than the general-purpose
`SpatialHash`, which owns a copy of every position and knows nothing about
periodic images. And it carries `unwrapped` positions alongside the wrapped
ones, because a mean squared displacement taken from wrapped coordinates
saturates at the box size and reports no diffusion however freely the
particles are moving.

Defects found while writing the tests:

- The cell list allocated one cell per cutoff per box edge with no bound.
  A dilute system -- a four-hundred-sigma box with a cutoff of a tenth --
  asks for sixty-four billion cells for a few hundred particles, and the
  process aborted on a 1.5 TB allocation. The counts are now halved until
  the grid is comparable to the particle count; a cell larger than the
  cutoff is still correct, only less selective.

- `green_kubo_viscosity_lite` integrated the stress autocorrelation to half
  the record. Past a few correlation times that estimate is noise, and
  integrating thousands of such lags accumulates a random walk as large as
  the signal: on an Ornstein-Uhlenbeck series whose integral is 1.01 by
  construction, it returned 0.14. It now stops at the first non-positive
  lag, which costs a few per cent of the tail and returns 0.99.

- The integrator caches the force between steps -- that is what makes
  velocity Verlet one evaluation per step rather than two -- but `pos` is
  public, so writing to it left the cache stale and the next step
  integrated the previous configuration's forces. The symptom was quiet:
  energy that almost conserved, and a trajectory that was no longer
  reversible. Added `refresh_forces` and documented the requirement at the
  field and at the method. The reversibility property test is what found
  it, which is the kind of thing it exists for.

Defects in the tests themselves, recorded rather than quietly patched:

- The Madelung constant is defined by the energy of *one* ion in the field
  of all the others, while a lattice energy counts each pair once, so the
  total per ion is half of it. The implementation was right and the
  expectation was off by that factor of two.

- A Lennard-Jones pair truncated at a tenth of sigma is not a free gas: it
  has a 10^12 core hidden just inside the cutoff, and two diffusing
  particles eventually find it and are ejected at enormous speed. The
  ideal-gas fixtures now use a genuinely zero potential.

- The Lennard-Jones force changes sign at 2^(1/6) sigma, not at sigma --
  the *energy* crosses zero at sigma and the pair is still repelling there.

- Particle 0 sits at the lower x in the two-body fixture, so a repulsive
  radial force points along -x; the sign expectation had the geometry
  backwards.

- The Debye structure factor below 2 pi / L measures the sample's extent
  rather than its structure and rises toward N, so the search for a Bragg
  peak found that instead. Documented and the search window moved above it.

- Continuity at the cutoff was tested against a fixed tolerance, which only
  tested the step size I happened to pick. It now measures the gap at two
  step sizes and checks that halving one halves the other.

- Three FCC cells at liquid density give a box only two cutoffs across, so
  the cell-list comparison was silently running the fallback against
  itself. Raised to five.

- `energy_drift` is a fitted slope, so a partial oscillation genuinely does
  register as a small trend. The property test asked for more than a linear
  fit can give; it now checks the closed form on a pure trend and compares
  a wobble against a trend twenty times its amplitude.

The tests lean on closed forms wherever one exists: the harmonic pair's
exact solution, the Einstein relation D = T / (m gamma) with its exp(-gamma t)
velocity autocorrelation, the hard-sphere B2 = 2 pi d^3 / 3, the Madelung
constant, the Fibonacci-like recurrences of the reduced-unit fixtures, and
a WHAM inversion of histograms built exactly from a chosen profile so the
recovery has no statistical error to hide behind. Where no closed form
exists the check is a comparison against an independent route -- the cell
list against the direct O(N^2) loop, the fluctuation heat capacity against
a finite difference, velocity Verlet against explicit Euler on the same
trajectory.

tests/properties/md_props.rs adds 19 property tests. The strongest is exact
reversibility: run forward, negate the velocities, run the same number of
steps, and every particle returns to within 1e-7 of where it started.

3757 lib tests and 286 property tests pass in debug; clippy is clean under
--all-targets -D warnings, and the module checks on nightly-2025-11-21.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Roadmap section 17, second half. `statistical_mechanics/kinetics.rs` carries
reaction networks and their stoichiometry, an adaptive implicit integrator
for the rate equations, Gillespie's exact stochastic algorithm and explicit
tau-leaping, enzyme saturation with fits and the three inhibition
mechanisms, equilibrium composition by Newton on the logarithms, the
Brusselator, Oregonator and chemical Lotka-Volterra, Eyring and
transition-state rate theory with the Kramers correction, nucleation and
Avrami transformation, and the acid-base and electrochemical relations.

The elementary single-formula relations already in `chemistry` -- the
Arrhenius rate, the equilibrium constant from a free energy, the Nernst
potential, pH from a proton concentration -- are used rather than
duplicated. What is here is the part that needs a solver.

Defects found while writing the tests:

- `rate_equations` was built on BDF2, which assumes a uniform step. An
  adaptive controller varies the step every step, so the history was at the
  wrong spacing, that inconsistency dominated the error estimate, and the
  controller shrank the step in response until the integration stalled and
  gave up. Replaced with backward Euler plus Richardson extrapolation: a
  one-step method has no history to get wrong, and the extrapolated value is
  second order anyway.

- The step-doubling error estimate can be fooled outright on an oscillatory
  system. An L-stable method damps hard at a step much longer than the
  period, so the coarse and fine solutions both collapse toward the fixed
  point, agree closely with each other, and report a small error -- and the
  controller then grows the step further. A run can step clean over whole
  oscillations while its error estimate reports success. The step is now
  also bounded by the solution's own timescale, |c| / |dc/dt|, which looks
  at the dynamics rather than at the difference between two equally wrong
  answers.

- `mass_action_rates` looked up only the reactant concentrations, so a
  composition too short to cover the products was silently accepted and the
  wrong system integrated. It now checks every species the network mentions.

- `steady_state_approx_check` skipped a fraction of the *steps* before
  measuring, and the adaptive integrator front-loads its steps into the
  induction period -- exactly the region where the approximation is not
  claimed to hold. It now skips an initial *time*, fifty complex-filling
  times, and refuses a run that ends before then.

Defects in the tests themselves, recorded rather than quietly patched:

- Two tests sampled a trace by step index rather than by time and drew the
  wrong conclusion from the transient: the stiff network's quasi-equilibrium
  read 0.33 instead of 0.5, and the "has this settled" helper reported a
  swing where there was none. Every such question here has to be asked of a
  time window.

- The same helper then demanded a dense tail, which fails on precisely the
  runs that are most obviously converged: a settled system produces one
  enormous final step, and that single sample is itself the evidence.

- The Lineweaver-Burk bias is a statement about *additive* noise. My fixture
  applied noise proportional to the rate, which survives the transform
  unchanged -- the two fits erred by 0.2202 and 0.2199, and the test proved
  nothing until the noise model was corrected.

- Henderson-Hasselbalch fails for a *dilute* buffer, not a lopsided one. At
  0.101 M acid with 0.1 M base the shortcut and the full balance agree to
  five decimals; at a micromolar they differ by more than a unit.

- `jmak_avrami` reaches exactly 1.0 in double precision once (k t)^n passes
  about 37, so a strict "less than one" was testing the float format rather
  than the curve.

- 2.302_585 is ln(10) to seven digits, a relative error of 4e-8, which
  exceeded the 1e-9 tolerances two property tests used.

The tests lean on closed forms where they exist -- first-order decay, the
quadratic for a weak acid, the logistic ignition time, the Fibonacci-free
exact Poisson moments -- and on independent routes where they do not: the
Gillespie mean against the rate equations for a linear network, where the
two agree exactly in the mean; tau-leaping against Gillespie as the leap
shortens; the inhibition mechanisms refitted rather than inspected; and the
implicit integrator's step count against the seven million an explicit
method would need on the same stiff system.

tests/properties/kinetics_props.rs adds 13 property tests, several over
randomly generated mass-balanced networks so the conservation law being
checked is one the integrator has no way to know about.

3793 lib tests and 299 property tests pass in debug; clippy is clean under
--all-targets -D warnings, and the module checks on nightly-2025-11-21.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Roadmap section 18, first module. `biophysics.rs` becomes a directory so the
population-scale models can sit beside the membrane and transport relations
already there; the roadmap calls the home `bio/`, but every earlier session
has kept new modules under the existing names and this follows that.

epidemiology.rs -- SIR, SIS, SIRS, SEIR, SEIRS and MSIR on an adaptive
Runge-Kutta with Richardson extrapolation; R0, the herd immunity threshold
and the final-size equation by bisection; branching-process extinction;
vaccination, demography, two competing strains and an age-structured model
with its next-generation R0; the network epidemic threshold from the
adjacency spectral radius; an exact stochastic SIR and an SIR on a contact
graph; and the Cori and Wallinga-Teunis reproduction-number estimators with
serial-interval and SEIR fitting.

Defects found while writing the tests:

- `final_size_equation` bisected with the sign inverted. The function
  1 - z - exp(-R0 z) vanishes at zero and is *positive* just above it --
  its slope there is R0 - 1 -- and negative at one, which is the opposite of
  the usual arrangement. Every final size came back as 1e-12.

- `sir_with_vaccination` added the vaccinated fraction to the removed class
  a second time. `sir` already places 1 - s0 - i0 there, which for
  s0 = 1 - coverage - i0 is exactly the vaccinated, so the population summed
  to 1 + coverage.

- `seir_fit_to_incidence` ran one Nelder-Mead pass, which contracts onto a
  direction and stops exploring the others -- and this objective has exactly
  the valley that punishes: the growth rate constrains beta and sigma only
  in combination. Added a restart from the best point.

Defects in the tests themselves, recorded rather than quietly patched:

- I asserted competitive exclusion for the two-strain model, which is false
  for it. Exclusion is a statement about a system that replenishes its
  susceptibles; a one-off epidemic is a finite race, and a strain with a
  thousandfold head start out-infects a rival with nearly twice its
  reproduction number before the susceptibles run out. The documentation
  said the same wrong thing and is corrected. The test now checks both
  halves, and the crossover in head start is checked for monotonicity so it
  reads as a threshold rather than an accident.

- I asserted that concentrating contact within age groups raises R0. It does
  not: with equal group sizes and equal row sums the next-generation matrix
  has the same dominant eigenvalue either way, 5.0 against 5.0. What raises
  R0 is heterogeneous *activity* -- under proportionate mixing the
  eigenvalue is <k^2>/<k gamma> rather than <k>/gamma -- and the test now
  checks that closed form across three spreads, along with the core-group
  case where a tenth of the population sustains an R0 of 25.

- The same test then asserted R0 was *below* a crude average of the groups'
  own reproduction numbers. It is above it, and that is the whole lesson.

- At R0 = 1.1 the epidemic grows at 0.025 per unit time and needs some 550
  time units merely to climb from a millionth. A fixed horizon of 400
  truncated it and the final size came out short -- a run that had not
  finished, not a defect. The horizon now scales with the growth rate and
  the test asserts the epidemic actually ended before comparing.

The tests lean on closed forms where they exist: the final size against the
transcendental equation it solves, the peak against S = 1/R0, the SIS
endemic equilibrium at 1 - 1/R0, the demographic equilibrium at S = 1/R0
with a damped oscillation on the way, the complete graph's spectral radius
of n - 1 and the star's of sqrt(n - 1), extinction at (1/R0)^i0 measured
over three thousand stochastic runs, and the Cori estimator inverted against
a renewal process built with a known R and serial interval. Where a claim
could only be established numerically -- the heterogeneous-activity
eigenvalue -- it was computed independently before being written down.

tests/properties/epidemiology_props.rs adds 12 property tests, including a
check of the network threshold against a direct power iteration on the
adjacency matrix and a comparison of the integrated final size with the
implicit solution over random parameters.

3811 lib tests and 311 property tests pass in debug; clippy is clean under
--all-targets -D warnings, and the module checks on nightly-2025-11-21.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Roadmap section 18, second module. The adaptive integrator moves from
`epidemiology.rs` up to `biophysics/mod.rs` so both modules share one copy.

population.rs -- logistic, Gompertz and Richards growth in closed form, the
Allee effect, Lotka-Volterra with its conserved quantity, Rosenzweig-
MacArthur with the enrichment threshold, two-species competition with its
four outcomes, Leslie matrices with the stable age distribution and
Euler-Lotka, Ricker and Beverton-Holt maps with a bifurcation diagram, the
Levins metapopulation, Wright-Fisher and Moran drift with the exact fixation
probability, heterozygosity decay, Hardy-Weinberg with its chi-square test,
one-locus selection with the balanced polymorphism, mutation-selection
balance, Hamilton's rule, the Price equation, coalescent times, Watterson's
theta, nucleotide diversity, Tajima's D and Fst.

Defects found while writing the tests:

- The shared integrator reported a numerical breakdown when it had in fact
  finished. Accumulated time overshoots the end by a rounding residue -- of
  order 1e-14 on a span of 235 -- leaving the loop condition true and a
  final "step" smaller than the working precision. An interval too short to
  bother with and an error controller forced into an impossible step are
  different conditions and are now distinguished.

- `richards` was written with `e^(-r nu t)`, which solves the tidier-looking
  dN/dt = r N (1 - (N/K)^nu) and destroys the Gompertz limit: the effective
  rate is r nu, so letting nu fall at fixed r freezes the curve at its
  initial value instead of approaching anything. It now uses `e^(-r t)`,
  solving dN/dt = (r/nu) N (1 - (N/K)^nu), where r is the intrinsic rate in
  both limits and the family is a genuine interpolation rather than two
  special cases with a gap between them.

Defects in the tests themselves, recorded rather than quietly patched:

- I asserted a four-cycle in the Ricker map at r = 2.5. It begins at 2.526;
  2.5 is still inside the two-cycle window. The windows narrow fast and
  guessing at their edges gets them wrong, so the cascade was measured
  before being asserted.

- Locating the bifurcations by counting attractor points then failed at the
  8-to-16 split, giving a Feigenbaum ratio of 2750. The reason is physical:
  convergence at a bifurcation is algebraic rather than geometric, so no
  finite transient settles and a 4-cycle is indistinguishable from an
  8-cycle just below the split. They are now found from the multiplier of
  the cycle, which has no such problem, and the second ratio comes out 4.59
  against Feigenbaum's 4.669.

- My doc comment claimed a recessive allele at mu = 1e-6, s = 0.1 sits three
  hundred times commoner than one with h = 0.1. The factor is thirty-one --
  sqrt(s/mu) h -- and the quoted frequency was wrong by a decade as well.
  Both are corrected and the test now checks the closed form rather than a
  round number picked by eye.

The tests lean on closed forms throughout: each growth law is checked
against a central difference of its own differential equation rather than
against a remembered curve; Beverton-Holt against its exact solution at
every step; the Leslie eigenvalue against the Euler-Lotka root, two entirely
separate computations; Moran fixation against 4,000 simulations per case;
Wright-Fisher against both the martingale property and the closed-form
variance p(1-p)(1-(1-1/2N)^t); heterozygosity decay against the same
simulation; coalescent intervals against 4N/(k(k-1)) over 20,000 runs; and
the Price equation as the exact identity it is.

tests/properties/population_props.rs adds 17 property tests over random
parameters, including the three growth laws against their own equations, the
competition criterion against the integrated outcome, and Fst, Hardy-Weinberg
and the Price equation as identities.

3834 lib tests and 328 property tests pass in debug; clippy is clean under
--all-targets -D warnings, and the module checks on nightly-2025-11-21.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Roadmap section 18, third module. seq_align.rs carries Needleman-Wunsch,
Smith-Waterman, Gotoh affine gaps, banded alignment and Hirschberg's
linear-space traceback; the BLOSUM62 and PAM250 matrices; reverse
complement, transcription, the genetic code, translation and ORF finding;
Wallace and nearest-neighbour melting temperatures; Hamming, p, Jukes-Cantor
and Kimura distances; a k-mer index, minimizers and a suffix-array search;
centre-star multiple alignment with profiles, consensus and PSSM scoring;
and a de Bruijn assembler.

Every algorithm that returns an alignment also returns a score, and the two
are checked against each other by rescoring: a dynamic program that reports
a maximum it did not reach is the commonest way for one of these to be
wrong, and a score-only comparison cannot see it. `alignment_score` and
`alignment_score_affine` exist for that purpose and are public, since the
same check is worth having outside the tests.

Four of the algorithms compute the same optimum by different means -- the
quadratic table, Hirschberg's linear-space recursion, Gotoh's three tables
with a free opening cost, and a band wide enough to hold the whole table --
and the property tests require all four to agree on random inputs. Any
disagreement is a defect in one of them.

Defects in the tests themselves, recorded rather than quietly patched:

- I asserted that the affine and linear optima coincide whenever the affine
  alignment has no gaps. They need not: with a costly opening the affine
  optimum takes mismatches where the linear one buys gaps, so the two
  optima legitimately differ. What is true, and is now checked, is that the
  same gapless alignment scores identically under either model.

- I asserted that identity scores highest for every letter of the BLOSUM and
  PAM alphabets. That is a statement about *residues*: B, Z and X are
  ambiguity codes whose scores are averages, and BLOSUM62 gives X/A zero
  against X/X of minus one. The property now runs over the twenty standard
  residues, which is what it was ever about.

- A minimizer assertion I wrote ended in `|| true` and could not fail. It is
  replaced by the real statement -- that each selected k-mer is minimal over
  some window containing it -- computed from the text directly rather than
  through the function under test.

The tests lean on structure rather than stored numbers: the genetic code is
checked for fourfold degeneracy in the third position and for methionine and
tryptophan being the only single-codon residues, rather than against a
table; the reverse complement for being an involution that preserves GC; the
k-mer index and suffix-array search against a naive scan, which is the only
thing there that is obviously right; Jukes-Cantor by inverting its own
closed form and by reducing to Kimura at the one-to-two transition ratio it
implicitly assumes; and the assembler for never inventing a k-mer no read
holds while losing none that they do.

The two melting-temperature models are checked to *disagree*: Wallace
ignores stacking, which is a small error at fourteen bases and thirty
degrees at sixty. A test that only checked their agreement would have been
asserting something false.

3852 lib tests and 339 property tests pass in debug; clippy is clean under
--all-targets -D warnings, and the module checks on nightly-2025-11-21.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Roadmap section 18, fourth module. `PhyloTree` stores a parent index and
a branch length per node, which makes root-walking and MRCA direct at the
cost of making "children of" a search. On top of it: Newick parse and
emit, leaves, height, total length, patristic distance, splits as rooted
clades and as unrooted bipartitions, and Robinson-Foulds. Then UPGMA and
neighbour joining, Fitch parsimony, Felsenstein pruning under JC69,
column bootstrap support, birth-death simulation with the extinct
lineages pruned away, the gamma statistic and lineage-through-time.

Two design points are worth stating because they decide what the tests
can assert. `bipartitions()` exists separately from `splits()` because a
neighbour-joining root is an artefact: on a four-taxon tree {A,B} and
{C,D} name the same branch, and comparing them as rooted clades would
report two replicates that found the same tree as disagreeing. Bootstrap
support is therefore computed on bipartitions, which is also the
convention support values are reported under. And `birth_death_tree`
stops at the first event *after* the target count is reached rather than
at the branching that reaches it: stopping on the branching leaves the
last internode interval exactly zero, which biases the gamma statistic
by about +sqrt(3/n) -- measurably, +0.50 at 40 tips before the fix and
+0.03 after, against a theoretical mean of zero.

The strongest tests are the ones with an exact answer to check against:

- Felsenstein pruning is compared against enumerating all 4^internal
  ancestral assignments on a five-tip tree. Pruning is a rearrangement
  of that sum, so the two must agree to rounding, and they do.
- The maximum-likelihood branch length for a pair of sequences is found
  by scanning and lands on the closed-form Jukes-Cantor distance.
- Neighbour joining is checked by inverting the patristic map: given
  distances that came from a tree it returns that tree's distances
  exactly. UPGMA does the same on ultrametric input.
- Patristic distances satisfy the four-point condition -- two of the
  three pairings equal, the third no larger -- which is the defining
  property of a tree metric.
- Gamma on pure-birth trees has mean 0.03 +- 0.05 and standard deviation
  1.00 over 250 replicates, which is its null distribution. That checks
  the simulator and the statistic against each other; either being wrong
  breaks it.
- Unequal rates fool UPGMA where neighbour joining holds: with two fast
  and two slow tips the slow pair is closest in the matrix without being
  related, average linkage joins them, and the Q correction does not.

Defects found and fixed while writing the tests, in my own text rather
than in the code:

- The `gamma_statistic` and `birth_death_tree` docs said extinction
  pushes gamma negative. It pushes it positive: near the present
  lineages have not had time to die, so the reconstructed tree's nodes
  crowd toward the tips. Measured +1.5 at mu/lambda = 0.5 against 0.0
  for pure birth. The bias runs opposite to the slowdown test, which is
  why a significantly negative gamma is read as conservative evidence.
- A first property test asserted that a Newick round trip preserves leaf
  *index* order. It does not, and should not: Newick encodes the tree,
  not the node numbering. Rewritten to compare distances by label.
- A hand-checked tree's total length was written as 8 where the string
  says 1+1+2+3.
- Two test sequences differed at 15 of 20 sites, which is the point
  where the Jukes-Cantor correction stops being finite; the module
  correctly refused them and the test, not the code, was wrong.

3889 lib tests and 355 property tests pass in debug; clippy is clean
under --all-targets -D warnings; checked on nightly-2025-11-21.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Roadmap section 18, fifth module, which completes the section. Conductance
models -- Hodgkin-Huxley, Morris-Lecar in both its type I and type II
parameterisations, FitzHugh-Nagumo -- alongside the integrate-and-fire
family: LIF with an exact F-I curve to check the simulation against,
Izhikevich with the five published presets, and AdEx with both adaptation
currents. Then spike train statistics, Poisson trains, PSTH and raster,
the spike-triggered average, a von Mises tuning fit, exponential and alpha
synapses, the STDP window and its all-to-all pairing, Izhikevich's random
network, Hopfield storage and recall, Wilson-Cowan, the passive cable, and
the drift-diffusion decision process with its gambler's-ruin accuracy.

`nernst_potential` and `goldman_potential` already exist in `biophysics`
and are not repeated; the module header says so.

Three defects the tests found in the code, all of them things that would
have returned plausible numbers rather than failing:

- The sealed-end boundary condition in `cable_equation_1d` used the
  one-sided difference V[n-1] = V[n-2]. That imposes a zero gradient only
  to first order, and the boundary decides the convergence rate of the
  whole solution: halving the spacing cut the error by 2.03 where a
  second-order scheme must cut it by 4. Replaced with a ghost node
  reflected through the end, giving 2 V[n-2] - (2+k) V[n-1] = 0. The
  ratio is now 4.02.
- `hopfield_recall` updated every unit simultaneously against the previous
  state. That has no Lyapunov property -- the energy can rise and the
  network can settle into a two-cycle between two states, neither of them
  stored -- and the doc comment claimed the energy never increases, which
  is true only of sequential updates. Switched to sweeping the units in
  index order, which makes the claim true and the recall convergent.
- `reaction_time_ddm` capped each trial at a step budget divided by the
  trial count. A decision time has a long tail, so that threw away exactly
  the slow trials the distribution is about, and errored on an ordinary
  run of 2000 trials. The budget is now shared across trials.

Two documentation errors, both about which way an effect runs:

- `hodgkin_huxley` did not say that a strongly *hyperpolarising* current
  is what the fixed step cannot follow. beta_m grows exponentially as the
  membrane hyperpolarises, so below about -25 uA/cm^2 it reaches thousands
  per millisecond and 0.01 ms is no longer stable; the function reports
  the breakdown, and there is now a test that it does. Depolarising
  currents integrate cleanly at 500 uA/cm^2 and simply block.
- The F-I curve's doc said the rate jumps at "the rheobase" without
  distinguishing the two rheobases. A 2.24 uA/cm^2 step makes the model
  fire once and then sit still; repetitive firing needs 6.3, where the
  rate is already 50 Hz. Both numbers are now tested, and the fact that
  they differ is its own test.

The tests that carry the most weight are the ones with an exact answer:

- The simulated LIF rate matches the closed-form 1/(t_ref + tau ln(...))
  to within 2% across five currents and randomised parameters.
- Simulated decision accuracy matches 1/(1 + exp(-2 A a / sigma^2)) to
  within sampling error, and the formula is shown to depend only on the
  combination A a / sigma^2.
- The cable solution matches cosh((L-x)/lambda)/cosh(L/lambda) and
  converges at second order.
- The von Mises fit recovers its own generating parameters to 1e-7 over
  forty randomised curves, because the log form is linear and solved
  rather than searched.
- Hodgkin-Huxley's all-or-none response: a 0.5 ms pulse at 10 uA/cm^2
  fails entirely, and doubling a suprathreshold one from 20 to 40 moves
  the peak by 1.4 mV rather than doubling it.
- Type I and type II excitability begin differently: the saddle-node
  parameterisation starts at 1.4 Hz where the Hopf one jumps to 7.1 Hz,
  from silence in both cases.
- A Hopfield probe can settle in a spurious state deeper than the pattern
  it started from -- asserted directly, since descending the energy finds
  a minimum and not the right one.

3935 lib tests and 376 property tests pass in debug; clippy is clean under
--all-targets -D warnings; checked on nightly-2025-11-21.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Roadmap section 19a, first module, and a new top-level `finance/` since
nothing existing is a home for it. Black-Scholes-Merton with a continuous
dividend yield, the Greeks, implied volatility, put-call parity, CRR
binomial and trinomial lattices with American exercise, Monte Carlo for
European, Asian, barrier and lookback payoffs, Longstaff-Schwartz least
squares, Merton jump diffusion, Heston by simulation, Crank-Nicolson on
the log-price grid, the SVI smile with a fit, and a delta-hedging
simulation.

Three defects the tests found in the code:

- `merton_jump_price` weighted its Poisson sum with intensity `lambda`
  where the risk-neutral intensity is `lambda (1 + k)`. The series still
  converged and still looked like a price -- it matched Black-Scholes at
  `lambda = 0` and produced a plausible smile -- but the discount factors
  no longer summed to `e^(-rT)`, so the call and put prices violated
  put-call parity. That is an arbitrage in a model whose whole purpose is
  to be free of them, and only the parity property caught it: no
  price-level comparison would have.
- `trinomial` indexed the previous layer by an offset from the terminal
  width instead of the constant shift of one that the recursion actually
  has, and read one past the end. Every call panicked.
- `implied_volatility` stopped on a price tolerance. Where vega is small
  that is meaningless: a call struck at 70 with the share at 100 and
  eighteen days to run prices identically at 5% and at 20% volatility to
  the last bit of a double, and the solver returned 10% -- the midpoint of
  its first bracket -- as though it had measured something. It now bisects
  on the volatility bracket, and returns `None` when vega falls below
  `1e-8` relative to the price, because the price does not determine a
  volatility there and reporting one is reporting rounding noise.

One property of my own writing that turned out to be false. I had asserted
that averaging two consecutive binomial step counts beats either, since the
error oscillates in sign. It does oscillate -- 100 steps lands 2.0e-2 below
the exact price and 101 steps 1.7e-2 above it, which is now a test -- but
across randomised parameters the averaging helped on only 11 of 40 draws,
so the phase is not predictable and the claim is not a theorem. Replaced
with two properties that are:

- The CRR lattice is arbitrage-free at *every* step count, because its
  up-probability is chosen to make the price an exact martingale. Parity
  holds to rounding even at seven steps, where the price is nowhere near
  the continuous answer.
- The trinomial is arbitrage-free only in the limit. Matching the first
  two moments of the log price makes the price a martingale to O(dt^2), so
  its parity residual is real at coarse steps -- 2.2e-3 on a two-and-a-half
  year option at seven steps -- and falls as one over the square of the
  step count, reaching 4.1e-8 by sixteen hundred. The test asserts the
  rate, which says the violation is a discretisation artefact rather than
  a defect, and the doc now says which lattice to use when an exactly
  consistent call and put matter more than smooth convergence.

The strongest tests are the model-free identities and the degenerate
cases, since both have exact targets:

- Put-call parity across 600 randomised parameter sets, and the bounds
  arbitrage would close.
- Homogeneity: doubling the spot and strike together doubles the price.
- The symmetry C(S,K,r,q) = P(K,S,q,r), exact to 1e-10.
- Merton with no jumps reproduces Black-Scholes to 1e-13, and Heston with
  no volatility of volatility reproduces it within its own standard error.
- A knock-in and a knock-out priced on identical paths sum to the
  barrier-free price to 1e-9, since every path pays into exactly one.
- Crank-Nicolson converges at second order: errors of 3.4e-3, 8.6e-4 and
  2.1e-4 as the grid doubles twice, ratios of 4.00 and 4.01.
- The Greeks match Richardson-extrapolated finite differences of the price
  they differentiate, over randomised parameters.
- Delta hedging's residual risk falls as one over the square root of the
  rebalance count, and hedging at 20% into a 30% world loses money well
  outside the sampling error.

3967 lib tests and 396 property tests pass in debug; clippy is clean under
--all-targets -D warnings; checked on nightly-2025-11-21.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Roadmap section 19a, second module. Compounding conventions and
conversion between them, discount factors, NPV, IRR and XIRR, bond
pricing and yield solving, Macaulay and modified duration, convexity,
zero-curve bootstrapping with linear interpolation in the zero rate,
forward rates, Nelson-Siegel with a fit, Vasicek and CIR bond prices, and
level-payment amortisation.

Four defects the tests found in the code:

- `ns_fit` fitted the basis `[1, slope, slope - decay]`, which is linear
  in the three coefficients and so solves exactly, but then converted
  back with `b1 = beta1 - beta2`. Matching coefficients gives `b1 = beta1`
  directly -- the `slope - decay` vector already carries the `+ b2 slope`
  term. The curve fit was off by 280 basis points while every internal
  residual looked fine, because the *fit* was right and only the reported
  parameters were wrong.
- `cir_bond_price` at zero volatility evaluated `1^infinity`. Its `A(t)`
  factor is a base tending to one raised to `2 kappa theta / sigma^2`;
  with `sigma = 0` the exponent is infinite, the base is exactly 1.0, and
  IEEE resolves `powf(1.0, inf)` to 1.0 -- silently dropping the whole
  factor and returning `e^(-B r0)`. That gave 0.9464 where the
  deterministic answer is 0.8339. Now special-cased to the Vasicek limit,
  with a doc note that the closed form is dominated by rounding below
  about `sigma = 1e-5`.
- `bracket_and_solve` pinned its lower end near -100%. Discounting a
  hundred-period schedule at -99.99% raises 1e-4 to the hundredth power
  and overflows, so every bond with a deeply negative yield failed to
  solve at all. It now walks down toward -1 and keeps the last point where
  the function is still finite.
- `nelson_siegel` computed `(1 - e^-x)/x` directly. That cancels
  catastrophically for small `t/tau`: at 1e-10 it keeps about six digits,
  and the short-end limit was wrong in the seventh. Switched to
  `-exp_m1(-x)/x`, which is accurate to the denormals.

One claim of my own that was false. I asserted a short-rate bond price is
always below one. It is for CIR, whose square-root diffusion keeps the
rate non-negative. It is not for Vasicek: with weak mean reversion the
convexity term `sigma^2/(2 kappa^2)` can exceed `theta` outright, the
long-run yield goes negative, and the bond is worth more than the unit it
pays. That is the Gaussian model's known feature, so it is now asserted
where it applies and demonstrated where it does not.

The tests lean on identities and on substituting answers back, since
almost everything here is defined as the solution to an equation:

- A rate converted between five compounding conventions returns exactly,
  and discounts identically at every horizon, not only at one year.
- IRR, XIRR and yield-to-maturity each zero the equation they were solved
  from, over randomised cashflows.
- A bond prices at par exactly when its coupon equals its yield, at every
  rate and term.
- Bootstrapping recovers the curve its bonds were priced from to 1e-12,
  and reprices them to 1e-10.
- A forward rate makes rolling equal to holding to 1e-12, which is the
  no-arbitrage identity it is defined by.
- Duration and convexity match Richardson-extrapolated differences of the
  price, and a zero-coupon bond's duration is exactly its maturity.
- Both short-rate models with no diffusion reproduce the deterministic
  integral `exp(-int r)` to 1e-13 and each other.
- Cashflows changing sign twice get no internal rate of return: both 10%
  and 20% zero the value of one such series, and reporting either as *the*
  return would be a mistake the code refuses to make.
- A mortgage at 5% over thirty years does not repay more principal than
  interest until period 195 of 360, and a higher rate pushes that later.

3988 lib tests and 412 property tests pass in debug; clippy is clean under
--all-targets -D warnings; checked on nightly-2025-11-21.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Roadmap section 19a, third and fourth modules, which complete the
section. portfolio.rs: simple and log returns, the Markowitz frontier in
closed form, minimum-variance and tangency weights, risk parity and risk
contributions, Sharpe, Sortino, max drawdown, Calmar, information ratio,
CAPM beta, and both Kelly fractions. risk.rs: historical, parametric and
Cornish-Fisher value at risk, expected shortfall, a GARCH(1,1) one-step
forecast, a moving-average crossover backtest, and Kupiec's coverage test.

One real defect, and it was invisible from the outside. `risk_parity_weights`
iterated `w_i <- w_i / (C w)_i`. At rest that gives `(C w)_i` equal across
assets -- which is the *minimum-variance* condition, not equal risk
contribution -- so it returned the minimum-variance weights under another
name, and every plausibility check on them passed. The damped update
`w_i <- sqrt(w_i / (C w)_i)` has the right fixed point: `w_i (C w)_i` is
then the same constant for every asset. The contributions are now 1/3 each
on a three-asset problem where they used to be 0.73, 0.16 and 0.11, which
is what gave it away.

Two things I had to get right about Cornish-Fisher, neither of which is a
bug so much as a limit that had to be measured before it could be
documented:

- Its kurtosis term carries the factor `z^3 - 3z`, which is zero at
  `z = -sqrt(3)`, an alpha of about 4.2%. So the same sample gets the
  correction applied one way at 1% and the other way at 5%. On a uniform
  sample the 1% estimate moves from 0.673 to 0.591 against a true 0.490 --
  the right direction -- while the 5% estimate moves from 0.476 to 0.483,
  the wrong one.
- It is asymptotic, not convergent. With a skew of -0.39 and an excess
  kurtosis of 0.85 it improves on the Gaussian fit, moving a 1% VaR from
  0.0197 to 0.0234 against a historical 0.0300. With a skew of -4.6 and an
  excess kurtosis of 33.8 it returns 0.0729 where the sample's own
  quantile is 0.0309. Both regimes are now tested, the second as a known
  failure rather than papered over, and the doc says the moments have to
  be looked at before the number is trusted.

The guard was also replaced. It had been an arbitrary bound on how far the
correction may move the quantile; it is now the standard validity
condition -- the corrected quantile must be increasing in z, since a
quantile function that decreases is not one.

The tests lean on perturbation and on coherence, both of which check a
solution without trusting the formula it came from:

- The minimum-variance portfolio's variance rises in every budget-
  preserving direction, and the excess scales exactly quadratically with
  the step, which is what a minimum looks like.
- Every frontier point has strictly less variance than any portfolio with
  the same expected return, tested by perturbing along directions
  orthogonalised against both constraints.
- The tangency portfolio has the highest Sharpe ratio in its neighbourhood.
- Expected shortfall is positively homogeneous, translation-equivariant
  and subadditive on every sample. Value at risk satisfies the first two
  and fails the third: two independent bonds each defaulting in 4% of
  scenarios have a *negative* 95% VaR individually and a VaR of 0.5
  combined, so diversifying raised the measured risk. That is the reason
  the regulatory measure changed, and it is now a test.
- A beta built into a series comes back out of it to 1e-9, alpha included.
- Kelly is the vertex of the growth rate: growth falls on both sides and
  returns to exactly the risk-free rate at twice the fraction.
- The crossover backtest on a monotone series matches buy-and-hold over
  the period it was invested, exactly -- any lookahead would beat it.
- Kupiec's statistic is exactly zero at the expected breach count and
  grows in both directions from it.

A note on CI wall time, which I have raised before and which now has a
cause: the `test` job runs the suite twice, once plainly and once
instrumented under `cargo llvm-cov --summary-only`. That is why it sits
near eighteen minutes while the local debug suite takes three and a half.
Splitting coverage into its own job would roughly halve it, but that is a
workflow change rather than a module, so I have left it alone.

4017 lib tests and 424 property tests pass in debug; clippy is clean under
--all-targets -D warnings; checked on nightly-2025-11-21.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Roadmap section 19b, first part, under the existing astrophysics/
directory rather than a new astro/. Kepler's equation solved for elliptic
and hyperbolic orbits, conversions among the true, eccentric and mean
anomalies in both directions, state_from_elements as the inverse of the
existing OrbitalElements::from_state_vectors, two-body propagation by
Lagrange's f and g functions, the orbital period and vis-viva. The
element set, the state-to-elements conversion and the geometric
quantities already live in orbital_elements.rs and are reused rather than
repeated.

Three defects the tests found, and the first is the kind that only a
conservation law catches:

- The hyperbolic branch of the Lagrange coefficients had two sign errors.
  Substituting E = i H and sqrt(a) = i sqrt(-a) into the elliptic form
  cancels the imaginary units in `f` and `g_dot` but not in `g` or
  `f_dot`, and I had carried the elliptic signs through. The trajectory
  came out smooth and plausible -- it receded, it curved the right way --
  while the specific energy drifted by 4e-4 over a hundred seconds and by
  half its own value over a thousand. Energy conservation is now 1e-15.
- `kepler_solve_elliptic` wrapped its converged root to [0, 2pi). At a
  mean anomaly of zero Newton lands on zero from either side, and an
  undershoot of one ulp came back as a full turn. The root lies in the
  same revolution as the mean anomaly, so it is clamped rather than
  wrapped.
- `kepler_solve_hyperbolic` seeded small mean anomalies with the textbook
  `M/(e-1)`. That diverges as the orbit approaches parabolic: at e = 1.001
  it puts the first guess at four hundred, where cosh overflows and the
  iteration has no derivative left. Replaced with `asinh(M/e)`, which
  inverts the leading term and is bounded everywhere.

Two corrections to my own documentation:

- `vis_viva` rejected an infinite semi-major axis, which is exactly the
  parabolic case `1/a = 0` where it should return escape speed. The doc
  claimed the formula covers all three conics while the guard refused one
  of them.
- The same function's error condition said "outside the orbit". The real
  boundary is `r > 2a`, where the kinetic energy runs out. For a bound
  orbit that reaches past apoapsis, so between `a(1+e)` and `2a` the
  formula answers with the speed a body of that energy *would* have,
  which is not a speed anything reaches. The doc now says so and the
  property test pins the boundary at 2a rather than at apoapsis.

The tests are built on the three kinds of invariant orbital mechanics
supplies:

- Inverse pairs. Kepler's equation solved and read forward composes to the
  identity to 1e-11 at eccentricities up to 0.9999; the three anomalies
  cycle back to 1e-9; elements and state vectors invert each other to
  1e-7 in the angles and 1e-8 in the state.
- Conserved quantities. Energy, the angular momentum *vector* and the
  eccentricity vector are all unchanged by propagation, over spans up to
  three periods forward and back. The eccentricity vector is the one that
  pins the orbit's orientation within its plane, which the other two do
  not.
- Group structure. Propagating by t1 then t2 equals propagating by
  t1 + t2, and -t undoes t. Two-body motion is a one-parameter flow and a
  propagator that is not one is wrong somewhere.

Beyond those: propagation is checked against an entirely separate route --
convert to elements, add n dt to the mean anomaly, convert back -- and the
two agree to 1e-6 of the radius. Kepler's second law appears as an
inequality that holds everywhere on the orbit: between periapsis and
apoapsis the true anomaly leads the mean, and past apoapsis it lags. A
parabolic orbit has neither an elliptic nor a hyperbolic anomaly and is
refused rather than forced into the wrong branch.

4031 lib tests and 437 property tests pass in debug; clippy is clean under
--all-targets -D warnings; checked on nightly-2025-11-21.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Roadmap section 19b, second part, completing the section. lambert.rs
solves the boundary-value problem by universal variables and builds
porkchop grids of departure characteristic energy from it. maneuvers.rs
adds what propulsion.rs, tidal.rs and lagrange.rs do not already cover:
combined burns, the sphere of influence, patched-conic escape, gravity
assist deflection, the Oberth effect, J2 nodal drift, sun-synchronous
inclination and ground tracks. The Hohmann and bi-elliptic transfers,
the plane-change delta-v, Tsiolkovsky, staging, the Roche limit and the
Hill radius already existed and are referenced rather than repeated.

Two numerical defects, both in the same function and both invisible
until the geometry pushed on them:

- `stumpff_c` evaluated its positive branch as `(1 - cos u)/u^2`. At the
  single-revolution boundary `z = 4 pi^2` the cosine is within an ulp of
  one, the subtraction keeps no digits, and the result comes back as
  zero or negative -- which made the flight time infinite and the whole
  upper bracket unusable. Rewritten as `2 sin^2(u/2)/z`, identical in
  exact arithmetic and accurate at both ends: the sine is *small* there
  rather than large, so squaring it loses nothing. Verified to follow the
  expected quadratic vanishing to a part in a thousand at 1e-10 from the
  boundary.
- The lower bracket walked z downward looking for a bound that does not
  exist, doubling to -1e12 and giving up. It now stops at the first z
  that is either fast enough or has no positive chord, the latter being a
  valid lower bracket since the bisection treats a missing solution as
  "too fast".

One documentation claim of mine was wrong. I had written that a flight
time shorter than the minimum-energy transfer has no solution. It does:
within one revolution a transfer exists for every positive flight time,
and hurrying simply costs more without limit. The minimum-energy transfer
is a particular duration, not a floor on one -- which is now a test, with
the departure speed scanned across two orders of magnitude of flight time
and its minimum shown to be interior, falling before and rising after.

Four of my own test claims were also wrong and are recorded rather than
quietly fixed: that the saving from a combined burn grows with the plane
change (it depends on which order the separate burns are taken in, so
only the inequality is a theorem); that the sphere of influence is
*inside* the equal-force radius (it is three and a half times outside it,
924,000 km against 259,000 -- a smaller exponent on a ratio below one
gives a larger answer); that a 500 km orbit at 45 degrees drifts 4.9
degrees a day (5.4); and that a mirror symmetry through the equator flips
the prograde flag (it does not -- reflecting z leaves the z component of
`r_a x r_b` unchanged, so the *same* flag gives the mirrored solution,
which is now the property tested).

Lambert reproduces Vallado's example 7-5 to six decimal places. The
property that carries the most weight is independent of that: given an
arc generated by `propagate_kepler`, the solver recovers the very
velocities that generated it, and flying its answer lands on the target.
Over three hundred randomised geometries the departure velocity agrees to
a part in 1e8 or better.

That last figure is a conditioning limit, not a tolerance chosen for
convenience. The velocities come out as `(r2 - f r1)/g`, and as the
transfer angle approaches pi that numerator is a difference of two nearly
equal vectors. The worst residual over three thousand draws was 1.2e-8,
and its transfer angle was 179.99 degrees. Exactly pi is refused, since
the plane is then undefined and infinitely many orbits connect the
points; the approach to it is merely imprecise, and the docs now say so.

Other properties: a porkchop cell reproduces the Lambert solution it came
from exactly; a combined burn never exceeds either sequential ordering
and is symmetric in the two speeds and even in the angle; a flyby's turn
depends only on the combination `r_p v^2 / mu`, which is checked by
scaling two of the three and finding the turn unmoved; the Oberth gain
matches `v dv + dv^2/2` exactly and periapsis beats apoapsis by more than
a factor of four on a 0.7-eccentricity ellipse; the J2 drift is westward
prograde, exactly zero at the pole and eastward retrograde, and the
sun-synchronous solver inverts it to a part in 1e9; and a ground track's
latitude is bounded by the orbit's inclination and attains it, while
successive ascending nodes walk west by exactly one body rotation per
orbital period.

4047 lib tests and 448 property tests pass in debug; clippy is clean under
--all-targets -D warnings; checked on nightly-2025-11-21.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Roadmap section 19b, final part. Two modules under the existing
astrophysics/ directory.

time_systems.rs: julian_date and jd_to_calendar, gmst and
local_sidereal, tle_epoch_to_jd, and the J2000/JULIAN_CENTURY
constants.

coords.rs: equatorial_to_horizontal and its inverse, the ecliptic
pair, mean_obliquity and precession_approx, sun_position_approx,
moon_position_approx, planet_position_low_precision over Standish's
elements, rise_set_times, and tle_parse_lite. The TLE reader parses
and checksums only. A TLE's numbers are *defined* by SGP4 -- they are
mean elements in Brouwer's theory, not osculating ones -- so
converting them to a state vector without SGP4 would give an answer
that is wrong by kilometres while looking entirely reasonable. The
doc comment says so rather than leaving the omission to be guessed at.

Two defects the tests found:

- jd_to_calendar used the textbook Julian-calendar branch below JD
  2299161 (the 1582 reform) while julian_date is proleptic Gregorian
  throughout, so the two stopped inverting each other before the
  reform: 1 January -4712 went out as JD 38 and came back as 8
  February. Made proleptic on both sides; the historian's convention
  is a different function, not this one.

- equatorial_to_horizontal took the altitude with asin. Near the
  zenith the argument is within an ulp of one, where asin has a
  square root's conditioning -- 1e-16 in becomes 1.5e-8 out. Now
  atan2(up, hypot(south, east)), which is well conditioned
  everywhere, and the round trip closes to 1e-15 at the pole.

Also corrected in my own tests: precession in right ascension is
m = 46.12"/yr, not the 50.29" of general precession in longitude;
those are different quantities.

18 unit tests and 14 property tests. Suite is 4,065 lib + 462
property tests, green in debug, clippy clean under --all-targets
-D warnings, and checked on nightly-2025-11-21.

Still outstanding for you, unchanged from the last two sessions: the
CI test job runs the suite twice (cargo test, then cargo llvm-cov
rebuilds instrumented and reruns it), which is the 14-18 minute wall
time; splitting coverage into its own job is a workflow change I have
left alone. And PR #4 now spans sessions 4-37.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Roadmap section 19c, first part. New fem/ module; fem1d.rs holds
fem_1d_poisson, fem_1d_general and fem_1d_quadratic, the Bc enum, a
Fem1dSolution wrapper that can evaluate between nodes, the L2 and H1
error norms, and convergence_rate.

Assembly is five-point Gauss per element into a symmetric banded
matrix, solved by L D L^T without pivoting -- which is unconditionally
stable while the problem is coercive, and a reaction term negative
enough to lose coercivity is a genuinely singular operator rather than
something to pivot around.

Flux conditions use the outward normal at both ends, so the same
Neumann value means the same physical thing on the left and the right,
and Robin is p du/dn + alpha u = g in the same convention, which keeps
the matrix symmetric. Dirichlet data is eliminated symmetrically
rather than by zeroing a row.

A pure flux problem is reported as Singular, detected by the exact
criterion rather than a threshold: the constant function is in the
kernel exactly when every row of the assembled matrix sums to zero.
A reaction term, a Dirichlet end or a nonzero Robin coefficient each
independently removes it, and a Robin end with a zero coefficient is a
flux condition that pins nothing -- the boundary case the check has to
get right instead of treating "Robin" as a keyword.

Two things the tests turned up, both now documented rather than
papered over:

- Nodal exactness for Poisson is exact only up to the quadrature of
  the *load*. With a transcendental f the residual nodal error falls
  off as the five-point rule does, around h^11, not as the h^2 of the
  solution -- so the two are separated by measuring the rate rather
  than by loosening a tolerance, and an assembly error would still
  show up as second order.

- Linear elements have a constant derivative per element, so the
  stiffness quadrature reproduces the element *average* of p exactly.
  The discrete bilinear form therefore still agrees with the true one
  on the element space, and the discrete solution is the exact
  a-orthogonal projection rather than an approximation of one. The
  property test asserts the Pythagoras identity that follows, which
  holds to 1e-17, instead of the Cea inequality it implies -- an
  equality cannot be satisfied by accident, and the earlier inequality
  form was nearly non-strict for exactly this reason.

Quadratic elements are exact at element vertices and merely
third-order at the midsides, for a reason worth stating: the vertex
Green's function is piecewise linear and lies in the space, while the
midside one kinks inside an element and does not. Both halves are
asserted.

13 unit tests and 18 property tests, the latter covering Galerkin
orthogonality, Ritz minimisation and its exact quadratic excess,
nodal exactness, the patch test at both degrees, superposition, the
discrete maximum principle, nested-refinement energy monotonicity,
the exact conservation law from testing against the constant,
reflection symmetry, the sharp Poincare constant, and the h^2/h^3
convergence orders that identify the spaces.

Suite is 4,078 lib + 480 property tests, green in debug, clippy clean
under --all-targets -D warnings, checked on nightly-2025-11-21.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Roadmap section 19c, second part. fem2d.rs holds FemMesh2 with its
three generators (rect, disk, from_delaunay), refine_uniform,
quality_min_angle, the assembled stiffness and mass matrices,
element_gradient, dirichlet_energy, barycentric interpolate, and
fem_2d_poisson / fem_2d_reaction_diffusion solved by
Jacobi-preconditioned conjugate gradients.

The linear triangle needs no quadrature for the stiffness term at all
-- the shape function gradients are constant, so the element matrix is
the gradient product times the area, exactly. Coefficients and the
source are sampled at the centroid, a one-point rule of the same order
as the element itself.

FemMesh2::new orients every triangle counterclockwise rather than
rejecting a clockwise one: the sign of the area is a labelling
convention, while a zero area is not and is refused. The boundary is
derived from the edge counts, and an edge in three triangles is
reported as NotManifold.

The disk generator puts 6k points on ring k so the arc spacing tracks
the radial spacing, and merges consecutive rings by angle, which keeps
the triangles from going thin at the rim the way a fixed point count
per ring would.

Two things worth recording:

- Uniform refinement leaves quality_min_angle *exactly* unchanged,
  because the four children of a triangle are all similar to their
  parent. The property test asserts equality to 1e-13 rather than a
  bound, since a quality measure that drifts under midpoint refinement
  is measuring something other than shape.

- The off-diagonal stiffness entry for an edge is minus half the
  cotangent of the opposite angle. That single identity is why the
  Delaunay condition and the discrete maximum principle are the same
  statement, so the test checks the cotangent formula directly, one
  triangle at a time, and asserts that the entry turns positive exactly
  when the opposite angle turns obtuse.

One test tolerance was replaced rather than loosened. The patch test's
gradient check failed on a sliver in a Delaunay mesh at 1e-8. The
gradient of a linear field amplifies a nodal error by the sum of the
shape function gradient magnitudes, which on a sliver is large -- that
is what makes slivers bad. The bound asserted is now that amplification
itself, read off by differentiating each shape function's own indicator
vector, which is both sharper and a statement about the method.

10 unit tests and 13 property tests: Euler's formula on all three
generators, conformity and boundary-cycle structure, orientation,
exact area and shape preservation under refinement, the cotangent
identity, zero stiffness row sums and the mass matrix totalling the
area, the patch test, Ritz minimisation with its exact quadratic
excess, energy monotonicity under refinement, superposition, the
discrete maximum principle on a nonobtuse mesh, rotation invariance,
the inverse-square domain scaling, and second-order convergence.

Suite is 4,088 lib + 493 property tests, green in debug, clippy clean
under --all-targets -D warnings, checked on nightly-2025-11-21.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Roadmap section 19c, third part. fem_2d_helmholtz solves
-lap u - k^2 u = f; fem_eigenvalues_drum and fem_eigenmodes_drum solve
the generalised problem K phi = lambda M phi over the interior nodes.

Helmholtz needs a different solver from Poisson for a structural
reason rather than a numerical one: once k^2 passes the first Dirichlet
eigenvalue the operator stops being positive definite, and conjugate
gradients is a minimisation method with nothing left to minimise. A
dense LU is used instead, and the O(n^3) cost is documented rather than
hidden. At an eigenvalue the operator is genuinely singular -- that is
resonance, not a numerical accident -- and is reported as such.

The eigenproblem goes through the Cholesky factor of the mass matrix,
which turns it into a standard symmetric one. The consistent mass
matrix is used rather than a lumped one on purpose: lumping shifts the
eigenvalues downwards, and it is precisely their being *upper* bounds
that makes them useful. Every discrete eigenvalue is a Rayleigh
quotient minimised over a subspace of the true admissible space, so it
cannot fall below the true one, and a nested refinement can only lower
it. Both halves are asserted.

Validation is against analytic spectra rather than against itself: the
unit square's pi^2(m^2 + n^2), and the circular membrane's Bessel
zeros taken from the crate's own bessel_j_zeros, which ties the solver
to the Part 3 membrane.

Two things the tests turned up:

- I had the disk's mode order wrong. j_{2,1} = 5.136 comes in below
  j_{0,2} = 5.520, so the fourth and fifth modes of a circular drum are
  the doubly degenerate two-nodal-diameter pair and the second radially
  symmetric mode is only sixth. The test now asserts that ordering
  explicitly rather than assuming it.

- The doubled eigenvalue 5 pi^2 of the square splits on the rectangle
  mesh, because every cell is cut along the same diagonal and so the
  mesh is not symmetric under exchanging x and y. That is a mesh
  artefact of the same order as the discretisation error, and the test
  now asserts both that it happens and that it stays small.

The two drum tests were 12.5 s of the debug suite. Replacing the
absolute tolerance on a 16x16 mesh with the h^2 error-ratio check
across the 6x6 and 12x12 meshes, and taking the disk to eight rings,
brought that to 2.8 s while making the assertion stronger -- a rate
identifies the space where a tolerance on one mesh does not.

6 unit tests and 7 property tests, the latter covering Betti
reciprocity (which holds for the indefinite operator too, since
symmetry has nothing to do with definiteness), the simple pole of the
resonant response, the upper-bound and refinement-monotonicity of the
spectrum, the inverse-square domain scaling and rotation invariance,
Courant's nodal domain theorem for the first two modes, and
mass-orthonormality with the Rayleigh quotient returning its own
eigenvalue.

Suite is 4,094 lib + 500 property tests, green in debug, clippy clean
under --all-targets -D warnings, checked on nightly-2025-11-21.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Roadmap section 19c, fourth part. fem_2d_elasticity_plane_stress on the
constant-strain triangle, element_strain / element_stress /
strain_energy / von_mises_stress, and fem_2d_heat_transient marching
the theta scheme.

Two defects in my own earlier code, both found by the elasticity patch
test failing at 1e-9 when it should have been at rounding:

- apply_dirichlet wrote a bare 1.0 on the diagonal of every pinned row.
  That is the textbook recipe and it is wrong for any problem whose
  natural scale is not one: an elasticity matrix has diagonal entries
  of order Young's modulus, so a row of 1 among rows of 1e10 gave the
  assembled system a condition number of 1e10 that the physics never
  had. The pinned rows now carry the mean free diagonal instead, which
  leaves the solution identical -- the row still says u_i = g -- and
  removed a factor of 1300 from the patch test error.

- The tolerance pcg_jacobi takes is relative to the norm of the
  right-hand side, and I was multiplying it by the data scale at all
  three call sites. With elasticity data of order 1e8 that turned a
  requested 1e-13 into an actual 1e-5. Passing it as the pure number it
  is brought the patch test to 1e-13 relative, which is where it should
  have been all along. The Poisson and heat solvers were affected the
  same way; only the small numbers involved had hidden it.

Both fixes are in shared code, so the Poisson, Helmholtz, eigenvalue
and heat paths all get the accuracy too.

The elasticity solver checks the three rigid body motions explicitly
against the constraints and reports Singular if any survives, rather
than letting the solver discover it. Stress is per triangle, not per
node, because the strain of a linear displacement field is constant on
an element -- and the doc says why averaging that to the nodes before
showing it to anyone is how a coarse mesh comes to look convincing.

A plane-stress subtlety worth the paragraph it gets: equal biaxial
tension has a von Mises value equal to the tension, not zero. The
three-dimensional intuition that hydrostatic stress cannot yield a
material does not survive into plane stress, because a state that is
hydrostatic in plane has a free surface out of it and so is not
hydrostatic at all. Asserted directly.

The theta scheme's doc and tests carry the A-stable / L-stable
distinction: for a mode too stiff to resolve, backward Euler's
amplification factor tends to zero and Crank-Nicolson's tends to minus
one, so the stiff mode dies under one and survives under the other
while flipping sign every step. That is why a discontinuous initial
condition rings under Crank-Nicolson and why the remedy is to start
with backward Euler steps.

11 unit tests and 9 property tests: rigid motions in the kernel, the
uniform-strain patch test with a shape-independent stress, the
uniaxial, pure-shear and biaxial closed forms across random materials,
frame indifference of von Mises, Clapeyron's theorem with the load and
modulus scalings, exact conservation of heat for the insulated
problem, the theta scheme's amplification factor reproduced to 1e-7 on
a discrete eigenmode, the A-stable/L-stable contrast, and the march
settling onto the Poisson solution.

Suite is 4,105 lib + 509 property tests, green in debug, clippy clean
under --all-targets -D warnings, checked on nightly-2025-11-21.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Roadmap section 19c, fifth part. fdtd.rs holds fdtd_courant_check and
its two-dimensional counterpart, fdtd_1d marching the Yee scheme with a
choice of perfect conductor or first-order Mur absorbing ends, an
Fdtd1d result type carrying both fields and the conserved energy, and
photonic_crystal_bandgap_1d from the Bloch dispersion relation.

Fields are normalised to E and eta_0 H, which removes the free-space
impedance from every line of the update and, more importantly, makes
the two terms of the energy comparable -- in unnormalised units one
would be 1e5 times the other and their sum would be numerical nonsense.

Three things worth recording, two of them my own errors caught by
probing the claims before writing tests around them:

- Fdtd1d::energy had the magnetic half-steps off by one. Snapshot k of
  the magnetic history holds H^{k-1/2}, so the pair straddling E^n is
  h[n] and h[n+1], not h[n-1] and h[n]. With the wrong pair the
  invariant held exactly only for a uniform permittivity at a Courant
  number of one -- where an extra symmetry rescues it -- and drifted by
  parts in 1e6 otherwise. With the right pair it is conserved to 9e-16
  for a graded permittivity at any admissible Courant number, which is
  what the algebra says it should be.

- The Mur update was reading the edge cell's neighbour from two steps
  back rather than one. The correct first-order form needs only values
  from within the step, so the cross-step history is gone entirely.

- The stability limit belongs to the *fastest* wave in the grid, not to
  vacuum. A permittivity below one -- a plasma above its cutoff, or an
  engineered medium -- has a phase speed above c and tightens the bound
  by exactly its index. Checking only the nominal Courant number would
  let such a grid through to blow up, so the check is against the
  smallest permittivity present, and the tests assert the threshold is
  sharp on both sides.

The conserved quantity is the one leapfrog actually has, not the
obvious sum of squares. The tests assert both halves: the leapfrog form
is constant to the last bit, and the naive form is not -- asserting the
wrong one would be asserting a tolerance rather than an invariant.

Validation is against closed forms rather than against itself: the
magic time step translates a pulse bit for bit and a Courant number of
0.6 demonstrably does not; a dielectric interface reproduces the
Fresnel amplitudes to a few percent with the sign inversion off a
denser medium; the Mur end leaks a factor of a thousand less than a
wall; and the quarter-wave stack's gaps sit at exactly the odd
multiples of its design frequency with relative width
(4/(m pi)) arcsin(|na-nb|/(na+nb)) to nine digits, while the even
multiples close.

9 unit tests and 9 property tests. Suite is 4,114 lib + 518 property
tests, green in debug, clippy clean under --all-targets -D warnings,
checked on nightly-2025-11-21. CI confirmed green on all five jobs for
6f74795 before this push.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Roadmap section 19c, sixth part. fdtd_2d_tm marches the transverse
magnetic Yee scheme with a Berenger split-field perfectly matched
layer; waveguide_cutoff_check_fdtd infers a parallel-plate guide's
cutoff from the evanescent decay it shows when driven below it, and
waveguide_cutoff_numerical gives the value it should find.

Two departures from the roadmap's signature, both to make the thing
testable rather than merely runnable:

- The source is a closure over the step index, as in fdtd_1d, not a
  frequency. Taking the waveform is the only way to switch the drive
  *off*, and with a source still running the field near it is the
  source's own and says nothing about what the boundary reflected.
  Every absorption measurement here depends on that.

- The result carries an envelope alongside the final field. One
  snapshot of a driven oscillation is whatever phase it landed on;
  what a steady-state calculation is for is the amplitude, and that
  cannot be reconstructed from a single frame.

The layer depth is per axis. A waveguide needs its ends absorbed and
its plates conducting: absorbing the plates would stop it being a
waveguide, and leaving the ends conducting lets the switch-on transient
rattle around forever and swamp the field being measured. That was not
a hypothetical -- it is what the first working version did.

Three errors of mine that probing the claims caught before any test was
written around them:

- The drive advanced its phase by omega per step rather than by
  omega * dt. That simulates a frequency 1/S times too high while still
  producing a perfectly clean exponential, so it would have passed any
  test that only checked the profile was exponential. With the units
  right the measured decay is 0.20832 against a predicted 0.208360.

- The decay fit ran the whole length of the guide. An evanescent field
  reaches a numerical floor within a few decay lengths, and the flat
  stretch beyond does not merely add scatter -- it drags the fitted
  slope towards zero, and on a long enough guide reports no decay at
  all. The fit now stops while the signal is still fifty times above
  the floor, and a correlation below 0.999 in the log is reported as a
  failed measurement rather than returned as a number.

- The fit also began a full guide width downstream, to let higher modes
  die. They are never excited: the source is the mode's own transverse
  pattern and the discrete sine vectors are exactly orthogonal. Backing
  off that far merely threw away the dynamic range a fast-decaying high
  mode needs, and starting three cells out is what let modes two and
  three work at all.

What comes back is the *numerical* cutoff, (2/S) arcsin(S sin(ky/2)),
not the textbook m pi / a. The grid has its own dispersion relation and
is always the slower of the two; the shortfall is (ky/2)^2 (1-S^2)/6,
which the tests check directly. Reporting the continuum figure would be
reporting what the answer ought to be rather than what the simulation
has. Measured against the numerical value the agreement is a couple of
parts in a thousand across widths 12 to 20 and modes one to three.

A mode near the grid's resolution limit -- three half waves across ten
cells -- decays within a couple of cells and leaves too little profile
above the floor to fit. That returns NoConvergence, which is the honest
answer.

5 unit tests and 5 property tests added: the matched layer returning a
thousandth of what a conductor does and improving with depth, exact
mirror and transpose symmetry of the Yee grid bit for bit, linearity,
the numerical cutoff zeroing the decay to rounding and closing on the
continuum value as the square of the cell size, and the measured decay
recovering the grid's own cutoff more closely than the continuum one.

Suite is 4,119 lib + 523 property tests, green in debug, clippy clean
under --all-targets -D warnings, checked on nightly-2025-11-21. CI
confirmed green on all five jobs for 7fff238 before this push.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
chebyshev_points and cheb_diff_matrix, chebyshev_collocation_bvp for
-(p u')' + q u = f with Dirichlet ends, spectral_poisson_periodic and
its inverse spectral_second_derivative, and spectral_convergence_demo.

The roadmap asked for the periodic solver to wrap the existing
fft_poisson_2d. It does not, and the doc says why: that function
divides by the eigenvalue of the *five-point* Laplacian, which makes
the discrete residual vanish to rounding -- exactly what a pressure
projection wants, since there the finite-difference divergence is the
thing that must be zero -- but leaves it second-order accurate against
the continuum. spectral_poisson_periodic divides by the true symbol
-k^2 instead. The two solve different problems and both are right, and
a test demonstrates the split directly: the spectral solution satisfies
the continuum equation to 1e-12 and the three-point difference equation
only to O(h^2), with the difference residual falling by exactly four
per refinement.

The differentiation matrix takes its diagonal as minus the sum of the
rest of its row rather than from the closed form. The two agree
analytically and differ in floating point by cancellation that grows
with n; the sum makes the matrix annihilate constants by construction,
which matters because the constant is the one thing every derivative
operator must kill and an error in it pollutes everything else.

The convergence claim is stated as what it actually is. "Spectral
methods converge exponentially" is false as a property of the method
and true as a property of smooth data, so the tests ask which model the
errors follow rather than how small they are: for analytic data
log(error) is linear in n, for data with k continuous derivatives it is
linear in log(n), and comparing the two correlations separates the
cases without naming a rate. |x|^3 comes out at n^-2.2 and |x|^5 at
n^-4.7, with the smoother one more accurate at every size.

One test needed its window chosen per function rather than fixed.
Geometric convergence runs into the rounding floor, and past that the
recorded errors are cancellation noise; cos(2x) is entire and is there
by n = 16 while 1/(2+x) has a pole a unit from the interval and is
still converging at n = 24. Fitting a model across the floor is fitting
nothing, and that is what the first version of the test did -- it
reported that an entire function follows a power law.

Cross-validation is against a different discretisation rather than
against itself: twenty-four collocation points and four hundred linear
elements solve the same variable-coefficient problem and agree to the
accuracy of the weaker one.

10 unit tests and 7 property tests: exactness on every polynomial the
space holds, D applied twice giving the second derivative,
centro-antisymmetry and zero row sums, the Jacobian being the only
thing an interval change introduces, the periodic solver exact within
the band and mean-free with a nonzero source mean dropped, linearity,
the collocation patch test, and the finite-element cross-check.

Section 19c is now complete: fem1d, fem2d, fdtd, spectral_pde.

Suite is 4,129 lib + 530 property tests, green in debug, clippy clean
under --all-targets -D warnings, checked on nightly-2025-11-21. CI
confirmed green on all five jobs for 062a3a9 before this push.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Roadmap section 19d, first part. New learn/ module. nn.rs holds Mlp
with Act and Loss, forward, backward, numerical_grad_check, train_sgd
and train_adam, predict, preactivations, conv2d_forward, and
linear_regression_gd_check.

The module is built around the observation that a learning algorithm is
unusually easy to test badly. A falling training curve is not evidence
of anything: gradient descent reduces the loss under a wrong gradient
too, just more slowly and towards somewhere else. So no test here uses
a loss curve as its main assertion. What settles backpropagation is the
central difference, and that is asserted across random architectures,
activations, losses and inputs, to eight digits.

Softmax and cross-entropy are fused rather than composed. Taken
separately the activation has a Jacobian and the loss has a gradient;
taken together the product collapses to exactly p - y at the logits.
That cancellation is worth having for accuracy as much as speed, since
computing the two separately loses precision precisely where the
network is confident. Cross-entropy therefore requires a softmax
output, and a softmax output with squared error is refused rather than
silently computing something else -- it would need the full Jacobian,
which is not implemented.

One test was rewritten rather than tuned. The rectifier gradient check
disagrees with a central difference whenever a pre-activation lands
within the difference step of zero, because the derivative genuinely
does not exist at the kink. My first version asserted a pass rate, and
at 90 of 120 random architectures that rate was neither meeting the
threshold nor meaning anything -- it depends on the widths and depths
drawn. The test now asserts the *cause*: every disagreement is required
to have a pre-activation within a thousand difference steps of zero,
which a genuinely wrong gradient would fail while sitting nowhere near
one. Making that checkable is why preactivations is public.

Other exact properties asserted: softmax invariant under a shift of its
input, including at magnitudes where the naive computation overflows
and where the order of the outputs must still match the order of the
logits; a bias-free rectifier network positively homogeneous at any
depth; permuting a hidden layer's units together with the next layer's
columns leaving the computed function untouched, which is why two
networks cannot be compared parameter by parameter; convolution linear
and, away from the padding it cannot be shift invariant in, commuting
with a shift; a uniform kernel giving the window mean and a delta
kernel the identity, both exactly.

XOR gets its classical treatment: a single layer cannot get below the
0.125 that predicting the mean costs, and one hidden layer solves it.
And descent on linear least squares is checked against the closed form
through the normal equations rather than against itself, with the step
size taken as the reciprocal of the largest eigenvalue of X^T X, which
is the largest step for which descent on a quadratic converges at all.

11 unit tests and 9 property tests. Suite is 4,140 lib + 539 property
tests, green in debug, clippy clean under --all-targets -D warnings,
checked on nightly-2025-11-21. CI confirmed green on all five jobs for
9d6e269 before this push.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Roadmap section 19d, second part. gp.rs holds KernelFn (Rbf, Matern32,
Matern52, Periodic, Linear, and closure under Sum and Product), Gp with
fit by Cholesky, predict returning mean and variance,
log_marginal_likelihood, optimize_hyperparams by Nelder-Mead over the
logarithms, condition_estimate, sample_prior and sample_posterior.

Regression here is conditioning rather than fitting: there is no
optimisation in fit, only a factorisation, and the answer is exact
given the kernel. Two consequences are asserted with `==` rather than a
tolerance, because they are identities:

- The posterior variance does not depend on the observations at all.
  Two processes fitted to the same inputs with entirely different
  targets return variances agreeing bit for bit. Uncertainty in a
  Gaussian process is a statement about where the data is, not about
  what it said.

- A periodic kernel repeats exactly. The separation enters through a
  sine of half the lag over the period, so k(x, x+p) equals k(x,x) to
  the last bit rather than to a tolerance.

Softmax-style fusion has an analogue here: log|K| is read off the
Cholesky diagonal rather than formed as a determinant, which for any
sizeable n underflows. The value is cross-checked in the tests against
an independent LU determinant and solve that share no code with it.

Hyperparameters are optimised in log space, which keeps every one
positive without a constraint and makes the search scale-free -- a
length scale of 0.01 and one of 100 are the same distance from 1, which
is how they should be treated when nothing is known about the scale.

condition_estimate is public because of what the tests found. Fitting a
squared exponential to points spaced well inside its length scale gives
a covariance matrix that is singular to working precision, and the
jitter that makes the Cholesky succeed is then what limits the
interpolation accuracy: the error is the jitter times the condition
number, which reached 1e-4 at a condition number of 2e5 in the property
tests. My first version asserted a fixed 1e-8 and was measuring the
conditioning rather than the method. The tolerance is now that product,
and the doc says the remedy is a shorter length scale, a rougher kernel
or a nonzero noise -- statements about the model, not about the
arithmetic. The estimate is documented as a lower bound, since the
factor's diagonal says nothing about how the off-diagonal mass is
arranged, which is why the tolerance carries a safety factor over it.

One other test was rewritten rather than tuned: I had asserted that
unit noise leaves a residual above 0.5, which depends on the data's
amplitude and spacing and is not a property of anything. It now asserts
the limit that is -- overwhelming noise collapses the posterior mean
onto the prior's.

10 unit tests and 8 property tests: exact interpolation and vanishing
variance at noiseless data, the variance ignoring the targets while the
mean is exactly linear in them, conditioning never raising uncertainty
anywhere, the prior recovered far from data, every kernel giving a
positive semi-definite Gram matrix including the compound ones, the
marginal likelihood against an independent determinant, tuning never
lowering it, and both samplers reproducing the distributions they came
from within their own standard errors.

Suite is 4,150 lib + 547 property tests, green in debug, clippy clean
under --all-targets -D warnings, checked on nightly-2025-11-21. CI
confirmed green on all five jobs for 32def5b before this push.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
…t trap

Roadmap section 19d, third part. cluster.rs holds kmeans and
kmeans_once with kmeans_pp_init and elbow_data, dbscan,
hierarchical_agglomerative with four linkages and dendrogram_cut,
gaussian_mixture_em, silhouette_score, adjusted_rand_index,
davies_bouldin, knn_classify and knn_regress.

THE RNG FINDING, which is the important part of this commit.

A property test asserting that two independent random partitions score
near zero on the adjusted Rand index failed with a score of exactly
one. The cause is in monte_carlo: Rng is a plain linear congruential
generator that returns its raw state, and for such a generator bit k
has period at most 2^(k+1). Taking `next_u64() % m` for a power of two
m reads exactly those bits. Measured: `% 2` gives 0,1,0,1 for ever,
`% 4` gives 0,3,2,1, `% 8` has period eight. Two "random" label
sequences drawn one after another are therefore perfectly correlated --
not a subtle statistical weakness but no randomness at all. A modulus
with an odd factor mixes in higher bits and is fine, which is why this
went unnoticed.

Thirty-one sites across eleven files were drawing small integers this
way, most of them in tests written over many earlier sessions. They
were not producing wrong answers -- the invariants they assert hold for
any input -- but their coverage was a repeating cycle of length two,
four or eight rather than the random spread the code reads as.

Fixed by adding Rng::below, which takes its answer from the top of the
word, documenting the hazard on next_u64, and converting all thirty-one
sites. The full suite passes with the widened randomisation, so no
latent defect was hiding behind the narrow coverage -- but that was
worth finding out rather than assuming.

Two defects in this session's own code, both found the same way:

- gaussian_mixture_em initialised every covariance at the *global*
  spread of the data. A component wide enough to cover the whole
  dataset claims every point almost equally, so the first maximisation
  dragged all the means back to the global mean and threw away the
  k-means initialisation that had just been computed. It converged to a
  local optimum eighty log-units worse than the right one. Seeding each
  covariance from its own cluster's scatter fixes it.

- kmeans ran Lloyd's algorithm once. On three well-separated blobs
  about one run in two hundred lands on a stable configuration with two
  centres inside one blob and one spanning the other two -- inertia 800
  against the best 21, and no number of iterations escapes it because
  no single point wants to move. It now restarts ten times and keeps
  the lowest inertia, with kmeans_once left public so a single monotone
  trajectory can still be observed.

One assertion was wrong rather than the code: I had claimed the
restarted run always beats a single one. Best-of-ten is a minimum over
its own draws and says nothing about an independent eleventh, so the
test now compares the two distributions, which is the claim restarts
actually support.

Centroid linkage is documented and tested as *inverting* rather than
quietly producing dendrograms that cannot be drawn: merging two
clusters puts their centre between them, which can be nearer a third
than either original was. The property test requires an inversion to
actually occur across random point sets, so the caveat cannot rot.
DBSCAN's core points are asserted invariant to input order while border
points are explicitly not, which is the algorithm as defined.

11 unit tests and 11 property tests. Suite is 4,161 lib + 558 property
tests, green in debug, clippy clean under --all-targets -D warnings,
checked on nightly-2025-11-21. CI confirmed green on all five jobs for
e67b208 before this push.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Roadmap section 19d, fourth part, completing learn/. tree.rs holds gini
and entropy, decision_tree_fit and regression_tree_fit sharing one
split search, tree_predict and tree_predict_value, feature_importance,
random_forest_fit with bagging and per-split feature subsampling, and
gradient_boosting_lite under squared loss.

The tests are built around what a tree can *express* rather than how
often it happens to be right, because the second depends on the data
and the first does not:

- A tree of depth d has at most 2^d leaves and can therefore name at
  most 2^d distinct classes, whatever it is given. My first attempt
  asserted an accuracy bound instead -- that one split cannot get more
  than half of four quadrants right -- and it was wrong: unequal
  quadrant counts let a leaf's majority exceed a quarter, and the stump
  reached fifty-five per cent. The bound on leaves holds always.

- Splits are decided by the order of a column's values, not their
  magnitudes, so any increasing affine rescaling of any feature leaves
  the tree computing the same function, node for node. That is asserted
  across random columns and factors spanning six orders of magnitude,
  and it is what distinguishes trees from every distance-based method
  in this crate -- k-means, k-nearest-neighbours and a Gaussian process
  all give different answers under the same rescaling.

- Feature importances are nonnegative and sum to exactly the total
  weighted impurity the tree removed, so dividing the credit among the
  columns neither creates nor loses any.

- A regression tree's every leaf is a mean of training targets, so no
  prediction can leave their range -- checked a million units outside
  the training data. That is the same statement as "a tree never
  extrapolates", which is what makes it safe against runaway outputs
  and useless for trends.

- Boosting's loss falls at every round, and a tree of depth zero cannot
  split, so a round of them must change nothing at all. The recorded
  first loss is the variance of the targets exactly, since the model
  starts at their mean, and gbm_predict is checked against the fit's
  own bookkeeping rather than trusted.

Gini and entropy are asserted at their exact values -- zero for a pure
node, exactly 1 - 1/k and exactly ln k for k equal classes -- and shown
blind to the order of the counts and to scaling them all together,
since they see proportions.

The module documentation says why forests and boosting are opposite
strategies rather than variants: a forest averages deep overfitted
trees whose errors are decorrelated, while boosting adds shallow
underfitted ones each fitted to the previous residual. Which is also
why a forest's round count is harmless and boosting's has to be
stopped early.

7 unit tests and 7 property tests. Suite is 4,168 lib + 565 property
tests, green in debug, clippy clean under --all-targets -D warnings,
checked on nightly-2025-11-21.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
Roadmap section 19d, final part, completing Part 4. src/units.rs became
src/units/mod.rs unchanged, with two submodules alongside it.

quantity.rs: Dim as seven i8 SI exponents with exact mul/div/pow/sqrt,
Quantity with dimension-checked arithmetic and about thirty
constructors, parse_unit, parse_quantity, unit_convert,
si_prefixes_format, and the 2022 CODATA table.

dimensional.rs: buckingham_pi, is_dimensionless_group,
dimensionless_groups_named, natural_units_power and
natural_units_convert, planck_units.

Buckingham's theorem is a rank computation, so it is done over the
crate's exact Rational rather than in floating point. A group is
*exactly* in the null space or it is not, and one whose dimensions
cancel to 1e-16 rather than to zero is a rounding error about to be
reported as physics. The returned exponents stay rational for the same
reason: Reynolds happens to have integer exponents, a general null
space basis does not, and rounding it would silently change the group.
is_dimensionless_group compares each row against zero, not against a
tolerance.

Two parser decisions the tests forced, both now documented rather than
implicit:

- Juxtaposition is multiplication. The CODATA table's own units are
  written "J s" and "1/mol", and the first version rejected both --
  which was caught by a test that parses every unit in the table rather
  than trusting it. Whitespace now separates factors and a bare "1" is
  a valid term.

- Parentheses are not supported, so "J/(mol K)" is refused as an
  unknown unit rather than quietly parsed as something else. A "/"
  applies to the single term after it, so the table writes "J/mol/K".
  Refusing is the safer of the two ways not to support them.

The gram carries the SI prefixes rather than the kilogram, so kg comes
out at exactly one and mg at 1e-6 -- the kilogram being the only base
unit whose name already contains a prefix. And unit names are resolved
whole before any prefix is split off, which is what makes m a metre,
mm a millimetre, min a minute and T a tesla. That is a rule rather than
a deduction and the doc says so, because any other rule gives different
answers for the same strings.

The CODATA table is checked for internal consistency rather than
transcribed and trusted: the seven constants that are exact by
definition since the 2019 SI revision are asserted exactly, the gas
constant is the product of two of them, epsilon_0 mu_0 c^2 is one, and
the fine-structure and Rydberg constants are recomputed from the
others. The Planck units are derived from hbar, c and G rather than
copied, and the tests check their defining relations -- l_P = c t_P,
E_P = m_P c^2, and the Schwarzschild radius of the Planck mass being
twice the Planck length, which is the statement that gravity and
quantum mechanics meet there.

Natural units refuse a dimension involving amperes, kelvin, moles or
candela rather than guessing at a convention to absorb them, and the
property test checks the bookkeeping is consistent by requiring the
converted magnitudes to multiply.

6 unit tests in quantity, 5 in dimensional, and 8 property tests.
Suite is 4,180 lib + 573 property tests, green in debug, clippy clean
under --all-targets -D warnings, checked on nightly-2025-11-21. CI
confirmed green on all five jobs for 566d8d6 before this push.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
…able

Completes the last two items on the Part 4 roadmap: the exact/symbolic
-> units/ cross-reference, and the constant consolidation.

dimensional_check_formula walks an Expr and returns its dimension,
enforcing the two rules a hand derivation drops: every term of a sum
has to have the same dimension, and a transcendental's argument has to
be dimensionless. Neither can be checked by evaluating the formula --
both sides of `x + v` are perfectly good floats -- so this is a check
numerical testing cannot do.

Two rules the tests forced, both of which came out of running the
checker on real `diff` output rather than out of theory:

  * Zero is the additive identity of every dimension at once, so a
    literally-zero term joins any sum. `diff` does not simplify, so the
    product rule leaves `0 * t` sitting beside `v * 1`, and a checker
    that refused that sum would be useless on anything differentiated.
    The waiver does not reach inside the zero term: its subexpressions
    are still checked.

  * A Const exponent is read as the dyadic rational it exactly is, via
    Rational::from_f64_exact. `0.5` is one half, so Pow(x, 0.5) is a
    square root -- which matters because that is exactly how `diff`
    writes the derivative of one. `0.1` is not one tenth but the
    power-of-two fraction the float holds, and no dimension is
    divisible by that denominator, so `l^0.1` is reported as a root
    that does not exist rather than rounded into one that does.

Constants: math::constants was already the one table, but four modules
carried their own copies, two of them at different values.

  * chemistry::FARADAY was 96485.0, which differs from N_A e in the
    sixth digit. math::constants now computes FARADAY from its two
    exact factors and chemistry re-exports it.
  * particle_physics::FINE_STRUCTURE was 7.297e-3 against ALPHA's
    7.2973525693e-3, a disagreement at 5e-5 relative.
  * habitable_zone and magnetosphere each had their own SOLAR_TEMPERATURE
    and SOLAR_LUMINOSITY; atmosphere had its own STANDARD_PRESSURE.

kinetics_props built the Nernst slope from its own transcribed 96485.0
and 8.314462618, at 1e-9 tolerance, so it was asserting the
transcription rather than the formula. It now uses the crate's R and
FARADAY, and caught the Faraday change -- which is what it should have
been doing all along.

Tests. The two constant tables are pinned to each other: the eight
constants fixed by the 2019 SI redefinition must agree bit-for-bit,
since nothing but a transcription error could move them, and the eleven
measured ones to 1e-8, which separates the 2018-to-2022 CODATA revision
(1e-13 to 1.5e-9 observed) from a mistyped digit. Derived constants are
checked against their definitions rather than against themselves, and
every unit string in the CODATA table has to parse.

For the checker, the cross-checks matter more than the direct ones: it
agrees with Quantity arithmetic, which implements the same algebra with
no code in common; it certifies the groups buckingham_pi finds by an
exact null space over the rationals as dimensionless; and the
derivative of anything has the dimension of the thing over the
variable's, which makes it a check on the differentiator too. The
property generator builds an expression alongside the dimension its
construction guarantees, so agreeing is evidence rather than tautology.

4193 lib tests, 577 property tests, clippy clean, nightly-2025-11-21
clean.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUkMGnfUbaopkUJLWeYCYi
@Magic-Man-us
Magic-Man-us merged commit f221d18 into main Aug 26, 2026
5 checks passed
@Magic-Man-us
Magic-Man-us deleted the part-4-symbolic branch August 26, 2026 01:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants