Skip to content

saloon: Cholesky, conjugate gradient, and one-sided Jacobi SVD (Hoon) - #88

Merged
sigilante merged 2 commits into
mainfrom
sigilante/saloon-linalg
Sep 21, 2026
Merged

sigilante merged 2 commits into
mainfrom
sigilante/saloon-linalg

Conversation

@sigilante

Copy link
Copy Markdown
Collaborator

Step 2 of the foundation work list in #86: Saloon's direct and iterative solvers, Hoon only, no jet hints.

Stacked on #87 — base branch is sigilante/lagoon-foundation, and +svd uses that PR's +argsort-dim for its descending sort. Review #87 first; this diff is only the saloon/ files.

What this adds (all %i754, bloq 4/5/6/7)

Cholesky and direct solves

  • ++chol — the lower-triangular L with A = L*L^T, right-looking and column by column. ++chol-unit is the same computation returning (unit ray), reporting a non-positive-definite matrix as ~ instead of crashing, which is the honest outcome for a numerical routine; ++chol is the crashing wrapper.
  • ++trsv-lo / ++trsv-up — forward and back substitution. The latter reads L transposed rather than materializing L^T.
  • ++chol-solve — one factorization plus the two substitutions.

Conjugate gradient

  • ++cg — from x0 = 0, stopping at |r| <= rtol*|v| or maxit, returning [x iter rnorm] so the caller can distinguish convergence from exhaustion. One +matvec and two +dotv per iteration; the matrix is touched only through products, never factored. A flat search direction (p.Ap = 0, i.e. not positive definite) returns the current iterate instead of dividing by zero.
  • ++pcg — the same with the Jacobi (diagonal) preconditioner; a zero diagonal entry is left unscaled.

One-sided Jacobi SVD

  • ++svd — the thin decomposition A = U*diag(S)*V^T for rows >= cols. Rotations orthogonalize the columns of a working copy of A, so at convergence the column norms are the singular values and the normalized columns are U, with V the accumulated rotations. The rotation is the one that zeroes a column inner product: t^2 + 2*zeta*t - 1 = 0, zeta = (beta-alpha)/(2*gamma) — the same shape as the symmetric sweep already in +eig, and it reuses +rot-cols, which already worked on rectangular input. Output is sorted descending, unlike +eig whose order is arbitrary, because singular values have a natural order. ++svd-vals for the values alone.
  • Sweeps are capped at 30 with a ~& trace on the cap, as +eig does.

Vector helpers on rank-1 rays (the shape +diag already returns, not n x 1 matrices): ++matvec, ++dotv, ++nrm2, ++axpyv, ++col-dot.

Two conventions carried over deliberately: every inner product sums left to right from the kind's +0 (the order a C or Rust kernel walks, so these stay jettable, and not NumPy's pairwise summation), and tolerances come from the core's .rtol exactly as +eig takes them, +feps substitution for the unusable bare default included.

Tests: 44 arms

saloon/desk/tests/lib/saloon-linalg.hoon, in the style of the existing suites.

The exact cases compare whole rays: integer Cholesky factors ([[4,2],[2,5]] -> [[2,0],[1,2]], a 3x3, and the same factorization at bloq 5), chol-solve, the triangular solves, one-step CG and PCG on identity and diagonal systems, and singular values of matrices whose columns are already orthogonal (so Jacobi performs no rotation and the norms are exact). Every intermediate there is exactly representable, so they hold at any width and in any rounding mode.

The approximate cases compare within a tolerance against NumPy: singular values of a 2x2 and a 4x3, reconstruction U*diag(S)*V^T ≈ A, V^T V ≈ I and U^T U ≈ I, CG convergence, CG agreeing with chol-solve, and PCG on a badly scaled diag(1e6, 1). Edge cases too: maxit=0, a zero right-hand side, a zero singular value leaving its U column zero, and the crash paths (asymmetric input to chol/cg, a wide matrix to svd).

saloon/tools/linalg_check.py re-derives all of it — 46 checks, 0 failures. Like #87's oracle it mirrors every operation in fractions.Fraction and asserts that the exact cases never round, rather than just comparing final answers. It also checks the worked examples in the doccords, and corrected one of them (+cg's example converges to 1.0000000000000004, not what I first wrote).

The suite avoids +transpose:la — it has a local +tpose — because that jet crashes on runtimes older than urbit/vere#1057.

Verification

Ship: fakezod at hoon-136 / [%zuse 409], %num desk carrying lagoon, saloon, math, complex, twoc, unum, fixed, rand, i754rand and the whole test tree.

python3 saloon/tools/linalg_check.py        # 46 checks, 0 failures
-test /=num=/tests/lib/saloon-linalg        # 44 tests, ok=%.y
-test /=num=/tests/lib                      this branch: 318 OK, 27 failed
                                            origin/main: 274 OK, 27 failed

Same 27 failures on both, +44 OK — exactly the new tests, no regression. Of those 27, 21 are the pre-existing lagoon Hoon-vs-C jet gap documented in #87 (this runtime predates urbit/vere#1057), and 6 are pre-existing on the Saloon side: 4 posit transcendental tests in saloon-unum, plus the two build failures the second commit fixes.

Second commit: the eig suites have been unrunnable

saloon-eig and saloon-eigh have not built on a ship since /lib/math's precision doors gained a third sample field. Their sort comparators pass ~(lth rd:math [%n .~1e-10]) and clay answers:

-need.[rtol=@rd atol=@rd]
-have.@rd
nest-fail
FAILED  /tests/lib/saloon-eig/hoon (build)

so 23 regression tests were silently dead. I hit the identical error writing my own comparator, which is how I noticed. Fixed at the three call sites and in the two generators that emit them (the files are generated, so fixing only the output would regress on the next run). Both suites are now green on the ship: saloon-eig 18 tests, saloon-eigh 5.

Happy to split that commit out if you would rather keep this PR to the new arms.

Not in this PR

Jets for any of it (the C mirror and NockVM both), LU with pivoting, Householder QR, and the %unum/%cplx kinds. Upstreaming the Hoon to urbit/urbit is separate, per AGENTS.md.

Refs #86.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TgBnKsUPYzkoPZonjePnaq

sigilante and others added 2 commits September 19, 2026 22:00
Step 2 of #86's foundation list, stacked on #87 (whose
+argsort-dim gives +svd its descending sort).  Hoon only, no jet hints, pure
addition to the %linalg section.

Factorization and direct solves: +chol (with +chol-unit reporting a
non-positive-definite matrix as ~ rather than crashing), +trsv-lo/+trsv-up
forward and back substitution, and +chol-solve.

Iterative: +cg from x0 = 0, returning [x iter rnorm] so a caller can tell
convergence from exhaustion, and +pcg with the Jacobi (diagonal)
preconditioner.  Each iteration is one +matvec and two +dotv, so neither
touches the matrix except through products.

+svd is the thin one-sided Jacobi decomposition: rotations orthogonalize the
COLUMNS of a working copy, so at convergence the column norms are the singular
values and the normalized columns are U, with V the accumulated rotations.  Its
output is sorted descending (unlike +eig, whose order is arbitrary) because
singular values have a natural order.  +svd-vals takes just the values.

Vector helpers on rank-1 rays, as +diag already returns: +matvec, +dotv, +nrm2,
+axpyv, +col-dot.

Every inner product sums LEFT TO RIGHT from the kind's +0 -- the order a C or
Rust kernel walks, so these stay jettable, and not NumPy's pairwise summation.
Tolerances come from the core's .rtol exactly as +eig takes them, including the
+feps substitution for the unusable bare default.

Tests: 44 arms in saloon-linalg.  The exact cases (integer Cholesky factors,
one-step CG on diagonal systems, singular values of matrices whose columns are
already orthogonal) compare whole rays, since every intermediate is exactly
representable at any width; the rest compare against NumPy within a tolerance.
saloon/tools/linalg_check.py re-derives all of it (46 checks): exact rational
arithmetic asserts op-by-op that the exact cases never round, and numpy supplies
the approximate references.  It also verifies the worked examples in the
doccords, one of which it corrected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TgBnKsUPYzkoPZonjePnaq
saloon-eig and saloon-eigh have not built on a ship since /lib/math's precision
doors gained a third sample field: their sort comparators pass
`~(lth rd:math [%n .~1e-10])`, and clay reports

    -need.[rtol=@rd atol=@rd]
    -have.@rd
    nest-fail
    FAILED  /tests/lib/saloon-eig/hoon (build)

so 23 regression tests have been silently unrunnable.  Add the missing atol
field at the three call sites and in the two generators that emit them (the
files are generated; fixing only the output would regress on the next run).

Verified on a fakezod at hoon-136: saloon-eig 18 tests and saloon-eigh 5 tests,
both green.

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

1 participant