Skip to content

Remove CG and GMRES; LSMR is the sole iterative solver#35

Merged
schroedk merged 12 commits into
mainfrom
refactor/remove-cg-gmres
May 10, 2026
Merged

Remove CG and GMRES; LSMR is the sole iterative solver#35
schroedk merged 12 commits into
mainfrom
refactor/remove-cg-gmres

Conversation

@schroedk
Copy link
Copy Markdown
Collaborator

Modified LSMR replaces CG and GMRES as the sole iterative solver, motivated by the empirical comparison in #27: GMRES disagrees with CG/LSMR by ~1e-3 (invariant under tol → 0), is 1.7×–2.5× slower on large problems, and has higher per-iteration cost; LSMR matches CG on well-conditioned problems and is 9×–88× tighter on demeaning error for ill-conditioned panels.

Drops the supporting infrastructure that was only needed for CG/GMRES:

  • CG, GMRES, multiplicative Schwarz, iterative refinement
  • Explicit Gramian (CSR) and GramianOperator (LSMR uses sqrt(W) D directly)
  • DesignOperator, build_schwarz, FeSchwarz, WithinError::Overflow
  • schwarz_precond::solve module; namespace flattening in schwarz_precond::schwarz
  • Python CG / GMRES / MultiplicativeSchwarz classes

Marks Preconditioner and FePreconditioner #[non_exhaustive] to leave room for future variants (e.g. two-level Schwarz with a coarse space) without another major version bump.

Net diff: −9,400+ lines across 11 commits, organized as schwarz-precond removals first (one concern per commit), then within-crate removals layered (solvers → Gramian → dead surface), then the changelog backfill.

Closes #27.

Test plan

  • cargo test --workspace — all suites pass
  • pixi run test — Python suite passes
  • No \bcg\b|gmres|multiplicative|iterative_refinement matches in .rs/.py sources
  • Public-surface diff vs v0.1.0 audited against CHANGELOG [Unreleased] section

schroedk added 11 commits May 8, 2026 11:02
LSMR subsumes both: unpreconditioned path replaces CG, and the Gramian-
side preconditioner path replaces GMRES for non-symmetric preconditioners.
Deletes solve::cg, solve::gmres, their tests, and purges stale references
from module docs, the README example, example labels, and test strings.
Removes the multiplicative variant along with its ResidualUpdater
abstraction. The additive SchwarzPreconditioner remains the only
preconditioner. Also strips stale references from crate-level docs and
the README, and prunes dead test fixtures (DiagOperator, IdentityOp,
SpdMatrix3, JacobiPrecond3, NonsymMatrix3, DiagLocalSolver, path_graph,
cycle_graph) left behind by the CG/GMRES and multiplicative removals.
It existed only as test infrastructure for the now-deleted CG dedup
path. Now defined as a private struct inside lsmr/tests.rs (its sole
legitimate use), with the three trivial integration tests deleted.
The combined mlsmr(Option<&M>) entry point is replaced by two
non-optional functions: lsmr (Golub-Kahan, no preconditioner) and
mlsmr (Modified Golub-Kahan, &M required). Updates the module
docstring, README example, and equivalence-test doc comment to match.
The additive subdirectory existed only to host the additive variant
alongside multiplicative; with multiplicative gone, the wrapper module
no longer earns its keep. Internal restructure only — public API
(SchwarzPreconditioner, AdditiveSchwarzDiagnostics, ReductionStrategy
re-exported from the crate root) is unchanged.
With CG and GMRES gone, the solve module wrapped a single solver and
its name redundant with the only function inside (solve::lsmr::lsmr).
Move src/solve/lsmr.rs and src/solve/lsmr/ up one level to src/lsmr.rs
and src/lsmr/, fold the dot/vec_norm helpers from solve.rs into lsmr.rs
as pub(crate), and re-export lsmr/mlsmr/LsmrResult/LsmrStopReason from
the crate root alongside SchwarzPreconditioner.
The buffer-pooling types (BufferPool, LocalSolveScratch,
AdditiveSweepBuffers, SchwarzBuffers, WorkerReductionBuffers) were
used only by AdditiveExecutor, so the file boundary served no
isolation purpose. Fold them into executor.rs and tighten visibility:
all buffer types and helpers become file-private (no pub(super)),
since they're only touched by AdditiveExecutor. Only AdditiveExecutor
itself plus the seven fields/methods preconditioner.rs actually
accesses retain pub(super).
Modified LSMR is now the sole iterative solver. Removes:

- KrylovMethod and OperatorRepr enums (LSMR-only, no Gramian dispatch)
- Preconditioner::Multiplicative + FePreconditioner::Multiplicative variants
- max_refinements field (refinement is unnecessary since LSMR converges on
  the observation-space residual directly)
- The fused Gramian/preconditioner build path (dead since LSMR doesn't use
  the explicit Gramian): build_preconditioner_fused, from_pair_blocks,
  build_domains_and_gramian_blocks, PairBlockData, compose_gramian_from_blocks
- SparseGramianUpdater and the residual_update module (multiplicative-only)
- PyO3 classes PyCG, PyGMRES, PyMultiplicativeSchwarz, PyOperatorRepr;
  validate_cg_preconditioner runtime check
- Examples preconditioned_solve, lsmr_vs_cg, memory_profile
- Tests preconditioner_coverage, iterative_refinement, local_solver_properties
- Bench suites that compared CG vs GMRES (preconditioners, ac_comparison,
  fixest_comparison, akm_panel, verify)

SolverParams collapses to { tol, maxiter, local_size }.
LSMR works on sqrt(W) D directly via WeightedDesignOperator and never
touches the assembled Gramian. The remaining users of Gramian/
GramianOperator were tests asserting equivalence between explicit and
implicit forms — both forms are now obsolete.

Removes:
- Gramian struct (explicit CSR assembly + matvec, diagonal, extract_submatrix)
- GramianOperator (implicit D^T W D matvec)
- The entire crates/within/src/operator/gramian/explicit.rs file
- CrossTab::from_gramian_block (test-only helper to derive a CrossTab
  from an assembled Gramian) and its test_helpers module
- tests/gramian.rs (entire test file)
- prop_gramian_symmetry, prop_explicit_equals_implicit_gramian,
  prop_weighted_explicit_equals_implicit
- public re-exports of Gramian and GramianOperator from within::
- bench_matvec rewritten on WeightedDesignOperator (matvec_weighted_design)

The operator::gramian module now houses only the per-pair CrossTab
machinery used by Schwarz subdomain construction. The DENSE_TABLE_MAX_ENTRIES
constant moves into cross_tab.rs and the module becomes pub(crate).
After the LSMR-only refactor several types had no production callers.
Removes them and inlines the remaining preconditioner free functions as
methods on FePreconditioner.

Removed:
- WithinError::Overflow — no producer (the Gramian assembly that emitted
  it is gone)
- DesignOperator (unweighted) — tests now exercise WeightedDesignOperator
  directly
- build_schwarz public convenience builder — sole caller was an internal
  test that now uses build_additive_with_strategy
- pub use schwarz_precond::SchwarzPreconditioner re-export — no consumers
- Free functions additive_reduction_strategy,
  resolved_additive_reduction_strategy, and additive_schwarz_diagnostics —
  inlined as methods on FePreconditioner

Marked Preconditioner and FePreconditioner #[non_exhaustive] (BREAKING
for downstream match sites) so future variants — e.g. a two-level Schwarz
with a coarse space — can be added without a major version bump. The
diagnostic methods on FePreconditioner return Option<...> so non-additive
variants will return None rather than panic.
Add the public-surface removals that were not previously documented:
Gramian/GramianOperator/DesignOperator/build_schwarz/FeSchwarz from the
within crate; the schwarz-precond solve module flattening and
schwarz::additive namespace flattening; and #[non_exhaustive] markers on
Preconditioner / FePreconditioner.

Add the LSMR additions that landed but were not noted: local_size
windowed reorthogonalization knob, LsmrStopReason / stop_reason, and
non-finite input validation. Add a Performance section for parallelized
LSMR kernels.

Drop the obsolete "iterations sums initial + iterative-refinement" line
in Changed — iterative refinement was removed in this same release.
The four suites were deleted alongside the CG/GMRES removal because
their helpers (benchmark_cg, benchmark_gmres, MultiplicativeSchwarz)
were gone. The scenarios are still relevant for LSMR:

- akm_panel / akm_scaling — four AKM panel pathologies (power-law firms,
  low mobility, disconnected clusters, combined) and realistic-scaling
  sweep up to 1M workers.
- ac_comparison / graph_backend_comparison — AC vs AC2 (split=1 vs 2)
  local-solver comparison; the question is orthogonal to outer-solver
  choice.
- verify — LSMR Schwarz correctness across 2-FE / 3-FE topologies.
- fixest_comparison — fixest-style 3-FE difficult DGP scaling, exact vs
  approximate Schur complement on the local solver.

The CG-vs-GMRES preconditioner suite is not ported — its purpose
evaporated with the solvers.
@schroedk schroedk merged commit 837f47a into main May 10, 2026
3 checks passed
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.

Discussion: should we remove CG and GMRES?

1 participant