Skip to content

Refactor numerical tower and fix critical bugs - #1

Open
longmathemagician wants to merge 14 commits into
mainfrom
claude/rust-library-abstractions-az9256
Open

Refactor numerical tower and fix critical bugs#1
longmathemagician wants to merge 14 commits into
mainfrom
claude/rust-library-abstractions-az9256

Conversation

@longmathemagician

Copy link
Copy Markdown
Owner

Summary

This is a major architectural refactor of talrost's numerical tower and algebraic abstractions, fixing three critical correctness bugs and establishing the foundation for a polyhedral homotopy solver. The crate is reorganized around a proper trait hierarchy (MonoidGroupSemiringRingField), with new Real and Scalar traits replacing the inverted Float/Number design. All container types (Matrix, Vector, Polynomial) are now generic over the algebraic tower, enabling integer matrices and dual-number automatic differentiation as first-class citizens.

Key Changes

Critical bug fixes:

  • Fixed Matrix kernel dispatch using bitwise & instead of && (e.g., M == 2 & N & O now M == 2 && N == 2 && O == 2), which silently corrupted results for non-matching dimensions
  • Fixed Complex::magnitude() returning |z|⁴ instead of |z| (was .powi(2).powi(2), now .sqrt())
  • Fixed Complex::normalize() and related operations that depended on the broken magnitude

Numerical tower redesign:

  • Introduced Real trait for IEEE-754 ordered fields (f32, f64), replacing the inverted Float trait
  • Introduced Scalar trait for fields with real-valued norms (implemented by Real types and Complex<F>)
  • Removed Number, Natural, and Float traits; replaced with proper algebraic hierarchy
  • Complex<F> now requires F: Real (not Float) and no longer implements PartialOrd (mathematically correct)
  • Added Smith's division algorithm for Complex to maintain accuracy at extreme magnitudes (1e±300)

Container generalization:

  • Matrix<T, M, N> now requires only T: Ring for structural operations (add/sub/mul/transpose), T: Scalar for numerics (determinant/inverse/solve)
  • Vector<T, N> similarly split: Ring for algebra, Scalar for norms
  • Polynomial<T, N> generalized; root finders remain Real-bound
  • Changed Matrix storage from column-major [[T; M]; N] to row-major [[T; N]; M] (conventional)
  • Added Index<(usize, usize)> and IndexMut for matrix element access

New modules:

  • src/real.rs: The Real trait with IEEE-754 constants and transcendental functions
  • src/scalar.rs: The Scalar trait unifying real and complex numeric operations
  • src/dual.rs: Dual numbers (Dual<T> and DualN<T, K>) for forward-mode automatic differentiation
  • src/mvpoly.rs: Multivariate sparse polynomials (MPoly, MSystem) with monomial support
  • src/lattice.rs: Smith and Hermite normal forms for integer matrices
  • src/matrix/lu.rs: LU factorization with partial pivoting (extracted from matrix.rs)
  • src/matrix/kernels.rs: Specialized multiply kernels (Strassen 2×2, Laderman 3×3, AlphaTensor 4×4) gated by min_specialization
  • src/roots.rs: Roots type for bounded root collections (replaces NaN sentinels)

Documentation and testing:

  • Added comprehensive module-level documentation for all public types
  • REVIEW.md: Architecture review identifying defects and design decisions
  • HOMOTOPY.md: Implementation notes for the homotopy solver pipeline
  • tests/oracle.rs: Property-based tests against nalgebra and num-complex
  • tests/laws.rs: Algebraic law verification for the trait tower
  • examples/bench.rs: Micro-benchmarks for matrix operations
  • tools/check_codegen.sh: Assembly-level codegen guard ensuring hot paths inline

**Feature and toolchain

https://claude.ai/code/session_012bGPMXfU7c6jnHVSX33DJW

claude added 14 commits July 4, 2026 18:02
…n proposals

Documents verified defects (matrix dispatch bitwise-AND bug, Complex
magnitude/recip/parse bugs, failing cubic roots path), proposes a
Real/Scalar rework of the numerical tower, a migration off
generic_const_exprs onto per-size inherent impls, an audit of stale
feature gates on current nightly, and a min_specialization-based
matrix kernel architecture verified against 1.98.0-nightly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bGPMXfU7c6jnHVSX33DJW
Implements REVIEW.md Phase 1 (sections 1.1-1.6 and 5):

- matrix: kernel dispatch and determinant guards used bitwise & instead
  of &&, so e.g. Matrix<f64,2,3> * 2x2 identity ran the Strassen kernel
  and zeroed the third row; determinant returned numbers for non-square
  matrices. Now M == 2 && N == 2 && O == 2 etc.; non-square determinant
  hits the todo!() arm. Added regression tests for non-square products
  and a should_panic determinant test.
- complex: magnitude computed (re^2+im^2)^2 == |z|^4; final powi(2) is
  now sqrt(). Added magnitude/normalize tests.
- algebra/complex: impl_field! generated recip(self) = Self::recip(self),
  an infinite recursion for types without an inherent recip (stack
  overflow for Complex). The macro now calls <T>::recip unambiguously,
  and c32/c64 get a hand-written Field impl with recip = conj(z)/|z|^2.
- complex: replaced From<&str> (which silently parsed "3 - 4i" as 0+0i)
  with core::str::FromStr and a public ParseComplexError. Handles
  "a + bi", "a - bi", "a", "bi", "-a - bi" with tolerant whitespace and
  round-trips Display output. Updated demo example and README.
- polynomial: wired the cubic arm of roots() to the generic Blinn solver
  (previously fell through to NaN; roots_3_generic failed). Documented
  and enforced the ordering contract: finite roots ascending, non-finite
  entries last; solver modules keep their native order.
- natural/integer: powi(power as u32) turned negative exponents into
  huge ones; now asserts power >= 0 with a clear message (should_panic
  tests added).
- lib: removed stale feature gates (associated_type_bounds,
  const_float_bits_conv, generic_arg_infer stable; more_qualified_paths
  and min_specialization unused) and the removed soft_unstable lint.
  generic_const_exprs + incomplete_features stay until Phase 3.
- solvers: scoped allow(non_snake_case) for paper-notation variables,
  dropped unneeded mut/unused params, removed superseded commented-out
  code. cargo build and cargo test are now warning-free.
- toolchain: pinned rust-toolchain.toml to nightly-2026-07-02.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bGPMXfU7c6jnHVSX33DJW
Numerical tower (REVIEW.md §2):
- New Real trait (src/real.rs, replaces Float): Field + PartialOrd with
  float consts (EPSILON/INFINITY/NAN/MIN/MAX, DIGITS/MANTISSA_DIGITS/
  RADIX as u32, MIN_EXP/MAX_EXP as i32), by-value math methods, and
  from_u32 for building small solver constants. Implemented for f32/f64.
- New Scalar trait (src/scalar.rs): Field with a real-valued norm
  (norm_sqr/norm -> Self::Real), conj, mul_add (fused for reals),
  is_nan/is_finite, and Mul/Div<Self::Real> supertraits. Blanket impl
  for Real types plus impl for Complex<F>.
- Natural is now unsigned machine integers only (Semiring + Ord + Eq,
  BITS: u32, pow(u32)); Integer is signed machine integers only
  (Ring + Ord + Eq, plus abs). Floats and Complex no longer implement
  them; Complex loses PartialOrd and all todo!() Float methods.
- Deleted the Number identity trait; every T: Number<Type = T> + Float
  bound is now T: Scalar (or T: Real where genuinely real-only).
- Element drops the Display supertrait; Display bounds moved to the
  formatting impls. Group loses the redundant Neg method. Monoid gains
  iter::Sum. Each impl_* macro implements exactly one trait; the
  stack_* family macros compose them. Complex algebra impls are now
  generic over F: Real.

Const generics (REVIEW.md §3):
- Removed #![feature(generic_const_exprs)] and incomplete_features;
  the crate now builds and tests clean on stable Rust.
- New counted Roots<T, MAX> (src/roots.rs) with as_slice/len/Deref/
  IntoIterator/PartialEq on the live prefix; no NaN sentinels in the
  public roots API.
- Polynomial::roots is now per-size inherent impls for N = 2..=5
  (linear/quadratic/cubic/quartic) with a documented contract:
  ascending order, len == number of real roots found. The N + 0^(N-1)
  - 1 arrays, [(); N]: bounds, and NaN-padded returns are gone.
- Polynomial::eval is a single Horner fold over T: Scalar (complex
  coefficients work); per-degree eval helpers deleted. new is the only
  constructor plus a real From<[T; N]> impl.
- Yuksel solvers genericized from f64-only to T: Real; the quartic
  path is reachable for the first time and tested. Blinn is now plain
  generic free functions (PhantomData struct deleted).
- Matrix<T, M, N> is now conventionally M rows x N cols with row-major
  storage [[T; N]; M]; Mul has the standard (M,K) x (K,N) -> (M,N)
  signature. IDENTITY/determinant/inverse live on Matrix<T, N, N>, so
  non-square usage is a compile error (compile_fail doctest included).
  determinant uses closed forms for N <= 3 and LU with norm_sqr
  partial pivoting above; inverse is Gauss-Jordan returning Option;
  transpose is implemented for all shapes.
- Vector is generic over Scalar: magnitude returns T::Real (a complex
  vector's magnitude is an f64), normalize divides by the real
  magnitude via Div<T::Real>.

Tests: 44 -> 70 unit tests plus a compile_fail doctest, all green on
both the pinned nightly and stable; demo example updated and passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bGPMXfU7c6jnHVSX33DJW
…tmul kernels

Task 1 — mul_add_fast (measured 5.7x perf trap):
- Scalar gains a provided `mul_add_fast`: hardware FMA on targets that have
  it (cfg target_feature=fma or aarch64), plain `a*x + b` elsewhere, so hot
  loops never hit the indirect libm software-fma call (8.19 ns vs 1.45 ns
  per Horner step on non-FMA x86-64).
- `mul_add` stays always-fused as the *precision* tool (Blinn's discriminant
  depends on it) and its docs now state the non-FMA libm cost.
- Polynomial::eval's Horner fold and the naive matrix-multiply accumulation
  now use mul_add_fast; a new test pins mul_add_fast == mul_add == plain on
  exactly-representable values for f32/f64/c64.

Task 2 — cargo features and no_std (REVIEW.md #6):
- Features: default = ["std"]; `libm` (optional dep, fetched as v0.2.16)
  supplies the float math backend for no_std builds; std wins if both are
  enabled; compile_error! if neither backend is selected.
- lib.rs is `#![no_std]` without the std feature; the Real impl macro picks
  std intrinsics or libm functions per cfg (powi's no_std fallback is
  exponentiation by squaring, matching powi's multiply-sequence semantics
  instead of a transcendental libm::pow).
- Display for Vector/Matrix/Polynomial writes straight to the Formatter
  (no String/format!/pop, no alloc); Complex FromStr reparsed core-only
  (trim + slice scanning, no String); ParseComplexError now implements
  core::error::Error; deleted the dead display.rs (format_f64).
- Verified: `--no-default-features --features libm` builds warning-free on
  nightly and stable, and cross-builds for thumbv7em-none-eabihf.

Task 3 — matmul kernel architecture (REVIEW.md #4.1):
- Default builds: Mul is only the naive triple loop with mul_add_fast
  accumulation; the crate stays stable-Rust-compatible.
- `--features specialization` (nightly): min_specialization is enabled and
  Mul delegates to an internal Gemm<Rhs> trait in src/matrix/kernels.rs with
  a `default fn` naive impl plus specialized impls for the concrete square
  sizes: 2x2 Strassen, 3x3 Laderman, 4x4 AlphaTensor-style. Dimension
  dispatch is type-checked (a 3x2 * 2x2 product can only match the default
  impl); the module is out-of-line so stable builds never parse the
  unstable `default fn` syntax. Kernel bodies moved unchanged; module docs
  record the stability/perf caveats.
- New test compares Mul against a plain reference loop for 2x2/3x3/4x4 and
  non-square/degenerate shapes on small-integer matrices (exact equality),
  in both feature configurations.

Acceptance (all warning-free): cargo test on nightly-2026-07-02 (default and
--features specialization) and stable 1.94.1 — 72+1 tests green each;
lib builds for {libm}, {libm,specialization}, and thumbv7em-none-eabihf+libm;
examples/demo passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bGPMXfU7c6jnHVSX33DJW
Design spec for building toward a polyhedral homotopy continuation
solver: ascending coefficient order, Algebra<T> evaluation trait,
Dual/DualN forward AD, Real/Complex completeness with Smith division,
LU factorization, Ring-relaxed containers, lattice (SNF/HNF) module,
running-error-bound evaluation, and multivariate sparse polynomials.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bGPMXfU7c6jnHVSX33DJW
…ce, mvpoly

Implements HOMOTOPY.md §5.1–§5.8, the tower refactor toward a polyhedral
homotopy continuation solver:

- §5.1 Polynomial flipped to ascending coefficient order (c[i]·x^i): Horner
  folds from the top via iter().rev(), Display still prints highest-degree
  first, Blinn/Yuksel indexing and all tests/demo updated.
- §5.2 Algebra<T> marker trait (Ring + Mul<T> + Add<T> + From<T>) with the
  blanket self-instance and Complex<F>: Algebra<F>; Polynomial::eval_at
  evaluates one Horner body over any Algebra (scalar, complex, dual points);
  monomorphic eval keeps its mul_add_fast fast path.
- §5.3 src/dual.rs: Dual<T> and DualN<T, K> forward AD as ring elements —
  full tower impls (Field recip = (1/v, −d/v²)), mixed-scalar ops, Algebra
  instances, nested Dual<Complex<F>>/DualN<Complex<F>, K>: Algebra<F>, and
  chain-rule lifts (sqrt/sin/cos/sin_cos/tan/exp/ln/powi/mul_add) on Real.
- §5.4 Real gains PI/TAU/E and exp/ln/powf/signum/min/max/clamp in both std
  and libm backends; Complex gains exp/ln/arg/powf/from_polar, by-value powi
  via exponentiation-by-squaring, nth_root_of_unity, and Smith's algorithm
  replaces textbook complex division (Div/DivAssign, f32/f64 LHS, recip) so
  1e±300-magnitude quotients survive.
- §5.5 Lu<T, N> packed factorization (partial pivoting by norm_sqr) as the
  linear-solve primitive: Matrix::lu/solve, Lu::solve/determinant; the N>3
  determinant arm and inverse now share this one pivoting code path.
- §5.6 Matrix/Vector structural ops (construction, ZERO/IDENTITY, add/sub/
  neg, scalar mul, transpose, matmul) relaxed to T: Ring — integer exponent
  matrices are first-class; norms/determinant/inverse/lu/solve stay Scalar.
  New src/lattice.rs: Smith and Hermite normal forms over Matrix<i64, M, N>
  by gcd reduction, returning unimodular transforms.
- §5.7 Polynomial::eval_with_error: Horner with Higham's running error
  bound, turning stopping rules into "below evaluation noise".
- §5.8 src/mvpoly.rs: Monomial/MPoly/MSystem sparse multivariate types with
  eval_at over any Algebra, structural partial, eval_grad via DualN,
  MSystem::eval/eval_jacobian, and allocation-free Display.

Deviations from the notes, both forced by min_specialization (it rejects
specializing impls that add non-marker bounds like Scalar over a Ring
default): the Gemm kernels' bounds are relaxed Scalar→Ring (algorithms
untouched — they only use +,−,*,ZERO) so Ring matmul exists under the
specialization feature too, and matmul accumulation is plain mul+add in all
configurations (the §5.6-accepted trade; LLVM still contracts to FMA).

Tests grow 72 → 132 (+1 doctest). Full feature matrix green with zero
warnings: nightly + stable cargo test, --features specialization,
--no-default-features --features libm (also with specialization, and for
thumbv7em-none-eabihf), and the demo example gains an eval_at + Dual
derivative + DualN Jacobian + Newton-corrector showcase.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bGPMXfU7c6jnHVSX33DJW
…s, CI

- Containers: hermitian Vector::dot, 3-D cross, Index/IndexMut, Default,
  From<array>, scalar Div/Neg; Matrix (row,col) indexing, Default,
  From<array>, Matrix x Vector; scalar-left multiplication macro-stamped
  for f32/f64/c32/c64 on both Vector and Matrix.
- tests/laws.rs: semiring/group/field law macros instantiated for u32,
  i32, f32, f64, c32, c64, Dual<f64> on exactly-representable samples.
- tests/oracle.rs: proptest oracles vs num-complex (ring ops, Smith
  division at 2^+/-950 magnitudes via exact power-of-two rescaling, exp,
  ln) and nalgebra (matmul/det/inverse, 4x4 and 5x5); root-finder
  residuals, LU solve residuals, Dual vs central differences, SNF
  invariants. Dev-deps pinned (=1.11.0 / =0.4.6 / =0.35.0).
- tools/check_codegen.sh + tools/codegen-probe: fails if any call lands
  in Polynomial::<f64,4>::eval / eval_at bodies, default and +fma configs
  (follows MergeFunctions symbol aliases); workspace-excluded.
- Docs: #![warn(missing_docs)] + #![forbid(unsafe_code)], every public
  item documented, crate-level homotopy example; cargo doc warning-free.
- CI: stable fmt/clippy/test, pinned-nightly test + specialization lane,
  no_std libm builds (host + thumbv7em-none-eabihf), advisory codegen
  guard.
- README rewritten around the tower/AD/LU/lattice/mvpoly APIs with the
  ascending-coefficient convention called out; snippets sourced from
  examples/demo.rs. examples/bench.rs micro-benchmarks (std::time only).
- Clippy-clean on stable 1.94 and pinned nightly with -D warnings
  (LU/lattice row ops rewritten iterator-style, test lint fixes).
- Version 0.2.0, rust-version 1.94.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bGPMXfU7c6jnHVSX33DJW
The stable and no_std jobs invoked bare cargo, which resolves the
toolchain through rust-toolchain.toml's nightly pin rather than the
action-installed default — so cargo fmt ran on a nightly without the
rustfmt component, and the thumbv7em target was added to stable while
the build used the pinned nightly. Set RUSTUP_TOOLCHAIN at the job
level (the env var outranks the toolchain file), correct the header
comment that claimed the action's default takes precedence, and bump
actions/checkout to v5 for the Node 20 deprecation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bGPMXfU7c6jnHVSX33DJW
…al starts

New std-gated module tree src/solvers/homotopy/ implementing the offline
half of a Huber–Sturmfels polyhedral homotopy on top of the Phase 5–6
tower (lattice SNF, Ring matrices, LU solve, mvpoly, Complex polar ops):

- support.rs: Support<NV> (deduplicated exponent-vector sets, extracted
  from MPoly/MSystem by skipping zero-coefficient padding terms) and
  Lifting<F> (per-point lift values; deterministic Knuth-MMIX LCG mapping
  the top 24 state bits to [0,1) — same seed, same subdivision), plus
  random_liftings drawing one stream across all supports.
- cells.rs: fine mixed-cell enumeration by naive Π C(|A_i|,2) edge-tuple
  search — level system solved per tuple via Matrix::lu/solve, strict
  minimality checked against a documented 1e-9 relative tolerance band.
  Ambiguous margins on tuples not otherwise rejected abort with the
  first-class GenericityError ("re-lift with a new seed") instead of
  guessing. MixedCell stores edges, normal α, and the i64 edge matrix V;
  volume() computes |det V| exactly as the product of the SNF diagonal.
  mixed_volume(supports, seed) does lift→cells→sum.
- start.rs: binomial start systems x^V = β with β_i = −c_{a_i}/c_{b_i},
  solved exactly through (U, S, W) = smith_normal_form(V): raise to U
  (γ = β^U), substitute x = y^W to reach y_i^{s_i} = γ_i, enumerate the
  s_i-th roots per coordinate, map back through x = y^W; |det V|
  pairwise-distinct solutions per cell.
- complex.rs: additive Complex::nth_roots(n) iterator (radius through
  the overflow-safe ln; no_std-friendly, ExactSizeIterator) with tests.
  Complex::powi already handled negative exponents — no change needed.

Anchor results: unit-simplex pair MV 1 (one cell), dense conic pair MV 4,
sparse trinomial pair MV 2 (< Bézout 4), three 3-var linear supports MV 1;
same seed reproduces identical cells, different seeds the same volume;
end-to-end lift→cells→starts residuals < 1e-9. 24 new tests (200 total);
fmt/clippy/test matrix, libm/thumbv7em builds, docs, and codegen guard
all clean on stable and the pinned nightly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bGPMXfU7c6jnHVSX33DJW
The online half of the Huber-Sturmfels pipeline, completing the solver:

- CellHomotopy<F, NV, MAXT>: precomputed per-term coefficients and shifted
  levels e_{i,a} = <a,alpha> + w_i(a) - m_i for one mixed cell, with
  allocation-free eval / eval_jacobian / dt and a write-into-buffer
  system_at variant so the tracker reuses one MSystem. Edge-term levels are
  pinned to exactly zero, so H(.,0) is the binomial start system and
  H(.,1) is the target bit-for-bit. Levels are globally normalized by
  t -> t^(1/e_min) (same path set, different speed): raw lifted levels can
  be ~0.07, which parks the binomial regime below t ~ 1e-14 and made the
  dense-conic paths untrackable before normalization.

- track_path: Euler predictor (J*ydot = -dH/dt) + Newton corrector at
  fixed t with step doubling/halving, relative update-norm convergence,
  a corrector-only first step (dH/dt ~ t^(e-1) is singular at t = 0), an
  exact landing on t = 1, and a terminal Newton polish against F itself.
  Honest statuses: Converged / MinStepReached / MaxStepsReached /
  SingularJacobian / Diverged, with t_reached, steps, and Newton counts in
  PathResult. Allocation-free by construction for future no_std exposure.

- solve(system, seed, options) -> SolveReport: supports -> seeded lifting
  -> mixed cells -> binomial starts -> one tracked path per unit of mixed
  volume; re-lifts once with seed+1 on GenericityError before surfacing
  it. SolveReport keeps every raw path and offers solutions() /
  distinct_solutions(tol) as views.

Validation: linear pair vs direct LU solve (1e-10), {x^2=2, y^2=3} smoke
(4 paths, +-sqrt2 x +-sqrt3), trinomial pair vs its eliminated quadratic
through the univariate solvers, dense conic pair (4 distinct verified
roots), proportional-equations failure honesty (no panic, non-Converged),
and bit-for-bit same-seed determinism.

Also: examples/homotopy.rs (end-to-end runnable proof), two bench rows
(tracker step ~1.8us, full trinomial solve ~65us), README "Polyhedral
homotopy (experimental)" section replacing the Roadmap future-work
framing, HOMOTOPY.md status appendix, crate-doc feature note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bGPMXfU7c6jnHVSX33DJW
…nostics

Predictors: TrackOptions::predictor selects Euler / Rk2 (midpoint) / Rk4
(classical), each stage a fresh Jacobian + LU tangent solve of the
Davidenko ODE. Rk4 is the benchmarked default (~20x fewer steps, ~8x less
wall time than Euler on the conic pair and cyclic-3; table recorded on
Predictor::default). TrackOptions stays exhaustive on purpose (documented):
pre-1.0 field additions are an accepted breaking change and callers keep
`..Default::default()` construction.

Step control: accepted steps now adapt dt by observed Newton effort
(1 iteration -> *grow, 2 -> hold, converged on the max_newton-th -> *0.8);
rejections still halve and the exact t=1 landing clamp stays. Documented on
TrackOptions, including the coupling to predictor order; regression test
pins total conic steps at <= the Phase 8 rule's measured 419 (now 186).

The γ-twist (the headline finding): cyclic-3 choked — the textbook
coefficient paths c·t^e never leave the real slice, so its real symmetric
(maximally non-generic) coefficients made all six paths fold pairwise on
the discriminant at one interior t (conjugate collisions, pivot ratios
~1e-8) for every seed. Fix: rotate every non-edge term by the
endpoint-preserving phase exp(iγe(1−t)) — a homotopy-level gamma trick
(γ = ln 2 fixed for reproducibility; CellHomotopy::with_gamma for explicit
control, γ = 0 = textbook). Both endpoints stay bit-for-bit; cyclic-3 now
tracks 6/6 on every seed and predictor.

Diagnostics: Lu::pivot_ratio() (min/max pivot-norm ratio; documented as a
cheap singularity-proximity signal, not a condition number) surfaces as
PathResult::pivot_ratio from the final polished Newton solve; SolveReport
gains converged_count(), failed_paths(), real_solutions(tol) (filters,
never zeroes imaginary parts) and allocation-free Display impls for
PathStatus and SolveReport; examples/homotopy.rs prints the report Display.

Validation: cyclic-3 end-to-end with the structural oracle (permutations of
(1, ω, ω̄): |coords| = 1, sum = 0, product = 1, MV = 6 = 6 distinct
verified roots); the trinomial pair end-to-end over Complex<f32>; a
proptest lane (tests/homotopy_prop.rs, 32 cases) with random complex
coefficients on the fixed trinomial supports. Bench gains a cyclic-3
solve() row and the Euler/RK2/RK4 comparison table (wall time + steps).
README and HOMOTOPY.md updated.

218 tests green on nightly, stable, and specialization; fmt/clippy clean on
both toolchains; no-default+libm builds (host + thumbv7em) and the codegen
guard unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bGPMXfU7c6jnHVSX33DJW
Tier A — examples/bench_suite.rs (also a test target): runs cyclic-3/4/5,
katsura-3/4, noon-3, eco-4/5 and the trinomial/conic calibration rows
end-to-end, printing mixed volume, cell/path counts, converged paths,
offline vs tracking wall time, us/path, verified max residual, and honest
failure tallies (--csv for machine-readable output). All 205 tracked paths
across the 9 zero-dimensional systems converge with residuals <= 6e-15;
cyclic-4 (proven positive-dimensional) fails all 16 paths honestly and
stays in the table as a negative control. A pre-run gate excludes systems
whose naive Pi C(|A_i|,2) cell enumeration projects past 60 s (cyclic-7:
~8.6e7 tuples, ~294 s) — the enumeration frontier, documented as a finding.

Tier C — tools/oracle-sympy: an exact sympy oracle (Groebner over Q/Q(i))
recording quotient dimensions, Seidenberg radicality, and torus counts for
every suite system. It settled: katsura-n (n+1-unknown convention) has 2^n
distinct roots of which exactly MV lie on the torus (katsura-3: 6 of 8,
katsura-4: 12 of 16 — matching the converged path counts exactly);
cyclic-4 is positive-dimensional (two curves, verified by substitution);
eco-4/5 have 4/8 roots, all on the torus. sympy's solve_poly_system was
caught silently dropping 6 of katsura-3's 8 roots, so all counts come from
quotient dimensions, never from solve().

Bug found by the suite and fixed: mixed_cells accepted exactly
integer-singular candidate edge tuples (f64 LU leaves a ~1e-16 pivot where
the exact pivot is 0), producing garbage normals that falsely tripped the
genericity check on every katsura seed. Tuple singularity is now decided
exactly over Z via Smith normal form, with a katsura-3 regression test.

Tier B — BENCHMARKS.md (methodology incl. the fairness asymmetry stated
both ways, recorded results, findings, literature context without any
restated numbers) and tools/bench-external/hc_bench.jl + README: a
same-schema HomotopyContinuation.jl harness for unrestricted machines (the
dev container's proxy 403-blocks the Julia CDN, so it is untested here by
construction).

Tests: 220 per lane (was 218), all three lanes green; fmt/clippy
(-D warnings, stable + nightly, --all-targets), no-default+libm (host +
thumbv7em), doc, codegen guard, demo/homotopy/bench examples all clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bGPMXfU7c6jnHVSX33DJW
Replace the naive Pi C(|A_i|,2) tuple scan with an LP-pruned depth-first
tree search (a simplified variant of DEMiCs, Mizutani-Takeda-Kojima, DCG
2007), moving the polyhedral solver's enumeration frontier from cyclic-5
to cyclic-7 end-to-end and cyclic-8 enumeration-only.

- src/solvers/homotopy/lp.rs (new, module-private): a dense two-phase
  simplex under Bland's rule solving the maximize-delta feasibility LP
  {A_eq*alpha = b_eq, A_in*alpha - delta*1 >= b_in}; delta* > band means
  strictly feasible, below -band prunes, inside the 1e-9 band surfaces
  GenericityError (same re-lift contract as before). Iteration-cap safety
  valve (Stalled) is never read as a pruning verdict; tolerances are
  documented and sit far below the genericity band.
- src/solvers/homotopy/cells.rs: mixed_cells is now the tree search
  (per-support edge pre-filter, static ascending-viable-count support
  ordering, per-node feasibility LPs); full-depth tuples go through the
  exact shared decision path (Z-SNF singularity gate, LU normal solve,
  relative-band minimality check), so accepted cells match the naive
  enumerator bit for bit. The old scan survives as mixed_cells_naive,
  the documented reference implementation.
- tests/cells_oracle.rs (new): asserts identical cell sets (count, edge
  tuples, edge matrices, exact volumes, normals to 1e-12) between both
  enumerators on trinomial/conic, cyclic-3/4/5, katsura-3/4, noon-3,
  eco-4/5, and seeded randomized supports; degenerate-lifting and
  Phase-10 singular-tuple regressions pass unchanged.
- examples/bench_suite.rs: cyclic-6/7 rows with published mixed volumes
  asserted (156, 924) - all 156/924 paths converge; the naive-projection
  exclusion gate is gone; new --enum mode measures naive vs DEMiCs side
  by side out to cyclic-8 (MV 2560 asserted, 10.4 s vs ~19 h projected).
- BENCHMARKS.md / HOMOTOPY.md: before/after tables (katsura-4 offline
  113 ms -> ~8 ms, cyclic-6 enumeration 558 ms -> 36 ms, cyclic-7
  ~294 s projected -> 0.70 s measured), frontier section rewritten,
  Phase 11 status recorded.

234 tests across 3 lanes; fmt/clippy (nightly + stable), no_std libm
(host + thumbv7em), doc, and the codegen guard all clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bGPMXfU7c6jnHVSX33DJW
Paths ending at singular isolated roots no longer die MinStepReached:
inside the endgame zone (t >= t_endgame, default 0.99) a min-step
failure — or an accepted corrector whose pivot ratio has collapsed
below endgame_pivot_threshold while dt sits below endgame_dt_threshold,
including the terminal accept at t = 1 where the ~sqrt(eps)-wide
cancellation zone of H around a multiple root lets Newton "converge" at
a point far less accurate than newton_tol suggests — hands the path to
a Cauchy endgame that computes the endpoint and its winding number.

- Complex-t evaluation on CellHomotopy: write_system_at_ct / eval_ct /
  eval_jacobian_ct / dt_ct continue c·t^e through the principal branch
  (single-valued for Re t > 0; the circle |1-t| <= r < 1 stays there)
  and the gamma-twist exp(i·gamma·e·(1-t)) verbatim (entire in t). The
  real-t methods remain the tracker's hot path (one real powf per term
  beats complex ln/exp; endpoint exactness is easiest there); axis
  agreement is pinned to ~1e-14 and dt_ct against complex central
  differences in both the real and imaginary directions.
- The endgame (track.rs, private Endgame): walk-out to
  r = max(1 - t_entry, endgame_radius) by radius-doubling Newton hops
  at real t (at the raw stall radius ~1e-13 the Newton noise floor
  eps/sigma_min exceeds the closure tolerance for windings >= 3), then
  circle tracking of t(theta) = 1 - r·e^{i·theta} with an
  Euler-in-theta predictor (dt/dtheta = -i·r·e^{i·theta}, sign pinned
  by unit test — a sign error makes closure impossible) and a Newton
  corrector at fixed complex t, bisecting failed theta-steps up to four
  times. Closure after each full loop finds the winding (<= 8 default);
  the endpoint is the mean of the equally spaced samples over the
  closed cycle (trapezoid on a periodic function = the Cauchy integral;
  kills every fractional Puiseux power exactly), gated by
  ||H(y,1)||_inf <= sqrt(newton_tol)·max(1, ||y||_inf) — singular roots
  cannot be Newton-polished to the regular tolerance.
- New statuses ConvergedSingular { winding } / EndgameFailed (Display:
  conv-singular / endgame-fail, width-aware); PathResult grows an
  endgame_entered flag so tests can *prove* healthy paths never enter.
- Report surface: SolveReport::singular_count(), multiplicity_of(),
  solutions() documented to include gated singular endpoints,
  failed_paths() excludes them, Display gains a (+n singular) tally.
- Validation: double root {x²-2x+1, y-x} — both paths winding 2,
  endpoints ~1e-15 from (1,1) (vs ~1e-8 attainable by plain Newton);
  triple root {(x-1)³, y-1} — three paths winding 3, ~2e-12; the same
  supports with simple roots stay endgame-free; f32 double root with
  f32-scaled trigger constants — winding 2, ~1e-7. The full regular
  suite (trinomial, conic, cyclic-3/5/6/7, katsura, noon, eco — 1000+
  paths) is asserted endgame-free, in cargo test and at release
  bench_suite scale.
- cyclic-4, the honest caveat: the endgame closes its 16 paths pairwise
  at winding 2 on points that genuinely lie on the positive-dimensional
  curves ((a,b,-a,-b), ab = ±1 — structurally verified, residuals
  ~1e-15). Nothing is fabricated, but winding certifies local branch
  structure, not isolatedness: a winding-2 landing on a curve is
  locally indistinguishable from an isolated double root, and telling
  them apart needs witness sets (deferred, documented on the status,
  in the pinned test, and in HOMOTOPY.md/BENCHMARKS.md).
- bench_suite: dbl-root row (endgame cost visibility), root-bearing
  conv column, endgame-fail tally, per-row no-endgame assertions on
  regular systems; README/HOMOTOPY.md/BENCHMARKS.md updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bGPMXfU7c6jnHVSX33DJW
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.

2 participants