Skip to content

add selectable sparse matrix solver#267

Open
esaruoho wants to merge 2 commits intopfalstad:devfrom
esaruoho:add-selectable-sparse-solver
Open

add selectable sparse matrix solver#267
esaruoho wants to merge 2 commits intopfalstad:devfrom
esaruoho:add-selectable-sparse-solver

Conversation

@esaruoho
Copy link
Copy Markdown

@esaruoho esaruoho commented Mar 2, 2026

Summary

Ports the sparse CSC LU solver from sharpie7/circuitjs1 PR sharpie7#920 (contributed by H-Dynamite, derived from EJML) and makes it selectable via a 3-way dropdown in Other Options, as suggested by @pfalstad in #228 (comment).

  • Auto — uses dense for small circuits, sparse for large ones (threshold: 150 nodes)
  • Dense (Crout's LU) — the existing solver, always
  • Sparse (CSC LU) — the sparse solver, always

This addresses the reason the original PR wasn't merged: the sparse solver adds overhead for small circuits. Auto mode avoids that by only engaging sparse when the matrix is large enough to benefit. The sparse solver gives 5-52x speedups for circuits with 1000+ nodes.

Changes

  • 4 new files in matrix/ sub-package: DGrowArray, IGrowArray, DMatrixSparseCSC, SparseLU — ported from PR Using Sparse Matrix LU to Decompose and Solve Equations sharpie7/circuitjs1#920 with Apache 2.0 EJML attribution headers
  • SimulationManager.java — solver fields, dispatching lu_factor/lu_solve that routes to sparse or dense, original methods renamed to _dense variants, invertMatrix always uses dense (small local matrices), preference loaded from localStorage
  • EditOptions.java — "Matrix Solver" dropdown (option 15), persists to localStorage, triggers needAnalyze() on change

Notes

  • EJML code is Apache 2.0 licensed (GPL-compatible)
  • invertMatrix (used by CustomTransformerElm and ThreePhaseMotorElm) always uses dense — these are small local matrices, not the circuit matrix
  • Solver preference survives page reload via localStorage

Closes #228

Test plan

  • Load a small circuit (e.g. simple RC) — verify Auto uses dense solver
  • Load a large circuit (1000+ nodes) — verify Auto switches to sparse
  • Force "Sparse" via Other Options on a small circuit — verify it still simulates correctly
  • Force "Dense" on a large circuit — verify it still works (just slower)
  • Change solver type, reload page — verify setting persists
  • Load a custom transformer circuit — verify invertMatrix has no regression
  • Create a singular matrix scenario — verify error handling works with both solvers

🤖 Generated with Claude Code

@pfalstad
Copy link
Copy Markdown
Owner

This looks great and it's definitely faster (at least in some cases).

The option needs to be moved, because right now it's only visible if "Auto-adjust timestep" is checked.

@pfalstad
Copy link
Copy Markdown
Owner

Also I think the solver preference is a per-circuit thing not a global setting. It should be saved and restored with the circuit. Because some circuits might work better with it and some better without it. (Need to look into this some more.)

Wow, @MatNieuw 's massive relay circuit runs 9x faster with this! He will be thrilled.

@MatNieuw
Copy link
Copy Markdown

MatNieuw commented Mar 17, 2026

I will be over the moon :-) The circuit I sent you, when running on my PC under 3.1.4 offline version does 3.4 s/ms (stopwatched between 5 and 15 ms). The 4-beta online does 4.9s/ms . So a 9-fold improvement is massive. Maybe even more for my really big circuits, which do 57 s/ms !

@MatNieuw
Copy link
Copy Markdown

I tested online 4.1.0 alpha, and the test circuit I sent you is indeed 9 times faster! Fantastic. My largest circuit speeds up as well, over 4 times. Switching on developer mode on that circuit shows updateCircuit() between 5100 and 5800 . 4.1.0 alpha cuts test time from a full day to 6 hours (=overnight). Great stuff.

@pfalstad
Copy link
Copy Markdown
Owner

Great! 4.1.0 doesn't even include this PR. That's based on some other changes I made (using multiple matrices).

@esaruoho esaruoho force-pushed the add-selectable-sparse-solver branch from f5f0b39 to b7f854f Compare April 15, 2026 06:06
@esaruoho
Copy link
Copy Markdown
Author

@pfalstad addressed both points:

Always visible. Restructured EditOptions.getEditInfo() so "Matrix Solver" is at n=15 (always returned) and "Minimum time step size" moved to n=16 (still gated by adjustTimeStep). Previously both lived at n=15 with the min-timestep entry winning when Auto-Adjust was set, which hid the solver dropdown — your observation matches that behavior exactly.

Per-circuit. Added an st attribute on the circuit root in XMLSerializer/XMLDeserializer. Loading a saved circuit now applies its solver preference; saving writes it back. The global localStorage solverType continues to serve as the default for new circuits and as the fallback when a circuit XML has no st attr. To keep files small, st is only written when non-zero (i.e. not Auto).

So a circuit can be authored to pin Sparse for users who want MatNieuw's 9× speedup, while another circuit in the same session can stay on Dense or Auto. Build clean.

@esaruoho esaruoho changed the base branch from v3-dev to dev April 15, 2026 09:50
esaruoho and others added 2 commits April 15, 2026 13:26
Port the sparse CSC LU solver from sharpie7/circuitjs1 PR sharpie7#920
(contributed by H-Dynamite, derived from EJML) and make it selectable
via a 3-way dropdown in Other Options: Auto / Dense / Sparse.

Auto mode uses dense for small circuits and sparse for large ones
(threshold: 150 nodes), avoiding the overhead that prevented the
original PR from being merged. The sparse solver gives 5-52x speedups
for circuits with 1000+ nodes.

- Add matrix/ sub-package: DGrowArray, IGrowArray, DMatrixSparseCSC, SparseLU
- Rename lu_factor/lu_solve to lu_factor_dense/lu_solve_dense
- Add dispatching lu_factor/lu_solve that routes to sparse or dense
- invertMatrix always uses dense (small local matrices)
- Solver preference persisted to localStorage
- EJML code under Apache 2.0 (GPL-compatible)

Co-Authored-By: Claude Opus 4.6 <[email protected]>
- EditOptions: move "Matrix Solver" choice ahead of "Minimum time step
  size" so it is always visible regardless of whether Auto-Adjust
  Timestep is enabled. Previously the solver dropdown shared index
  n=15 with the min-timestep field and was hidden when adjustTimeStep
  was set.
- XMLSerializer/XMLDeserializer: write/read the "st" attribute on the
  circuit root so each circuit remembers its preferred solver. The
  global localStorage value still serves as the default for new
  circuits, but a saved circuit's choice now wins on load. Omitted
  from XML when Auto (0) to keep file size small.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@esaruoho esaruoho force-pushed the add-selectable-sparse-solver branch from b7f854f to d6e80f8 Compare April 15, 2026 10:28
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