Skip to content

Add Refactorize() for numeric refactorization reusing the symbolic analysis#55

Merged
wo80 merged 1 commit into
wo80:masterfrom
hbadi:feature/numeric-refactorize
Jun 8, 2026
Merged

Add Refactorize() for numeric refactorization reusing the symbolic analysis#55
wo80 merged 1 commit into
wo80:masterfrom
hbadi:feature/numeric-refactorize

Conversation

@hbadi
Copy link
Copy Markdown
Contributor

@hbadi hbadi commented Jun 7, 2026

Motivation

In Newton iterations and time-stepping loops the system matrix changes its values every iteration but keeps the same sparsity pattern. The only public entry point today is the static Create(), which re-runs the fill-reducing ordering + symbolic analysis every time, even though the pattern is constant.

Separating symbolic-once / numeric-many is a standard feature of sparse direct solvers (UMFPACK, PARDISO, MUMPS, CHOLMOD all expose it). The internals here already split the two phases (SymbolicAnalysisS, then the private numeric Factorize); this PR just exposes the numeric refactorization.

What

Adds a public Refactorize(matrix[, tol]) to the square direct factorizations, for both Double and Complex:

  • SparseLU.Refactorize(A, tol = 1.0)
  • SparseCholesky.Refactorize(A)
  • SparseLDL.Refactorize(A)

It re-runs only the numeric Factorize, reusing the cached SymbolicFactorization. A dimension guard throws ArgumentException on a size mismatch. Behaviour w.r.t. the pattern:

  • LU — the column ordering is a permutation, so a different pattern still factorizes correctly; only the fill may become sub-optimal.
  • Cholesky / LDL — the elimination tree is reused, so the matrix must keep the pattern used in Create (documented in the XML doc).

Usage

var lu = SparseLU.Create(A, ColumnOrdering.MinimumDegreeAtPlusA, 1.0);
lu.Solve(b, x);

// ... matrix values change, same sparsity pattern ...

lu.Refactorize(A2);   // reuse the ordering, redo the numeric only
lu.Solve(b2, x2);

Tests

TestRefactorize added for all six factorizations: factorize A, refactorize B = 2·A (same pattern), solve and check the residual; plus a dimension-guard throw. All 40 factorization tests pass.

Notes

  • Could be lifted to ISparseFactorization<T> if you'd prefer it uniform across all factorizations (would also need a story for SparseQR, whose symbolic/numeric split is structured differently).
  • Happy to adjust the naming (Refactorize vs Factorize / Numeric) or the scope to your preference.

Thanks for CSparse.NET!

…ization

SparseLU / SparseCholesky / SparseLDL (Double and Complex) gain a public
Refactorize(matrix[, tol]) that re-runs only the numeric factorization, reusing
the cached symbolic analysis (fill-reducing ordering, elimination tree).
Intended for Newton iterations and time-stepping loops where the matrix values
change but the sparsity pattern does not: it skips the ordering/symbolic phase
and keeps only the (dominant) numeric work.

- LU: a different pattern still factorizes correctly (column ordering is a
  permutation), only the fill may be sub-optimal.
- Cholesky / LDL: the elimination tree is reused, so the pattern must match.
- Dimension guard throws ArgumentException on a size mismatch.

Adds TestRefactorize for all six factorizations (refactorize B = 2*A on the same
pattern reproduces a correct solve; dimension guard throws). All 40 factorization
tests pass.
@wo80
Copy link
Copy Markdown
Owner

wo80 commented Jun 7, 2026

Thanks a lot!

Now that there are Refactorize methods, I guess it makes sense to make sure to allocate memory only on the first call to Factorize, right? So, for example in SparseLU.Factorize(...) you'd have:

if (L is null)
{
    L = CompressedColumnStorage<double>.Create(n, n, lnz);
    U = CompressedColumnStorage<double>.Create(n, n, unz);
}
else
{
    // lnz and unz do not change on re-factorization
    L.Clear();
    U.Clear();
}

Same for Cholesky and LDL'. Would you like to do another PR which implements this?

@wo80 wo80 merged commit 8898899 into wo80:master Jun 8, 2026
1 check passed
@hbadi
Copy link
Copy Markdown
Contributor Author

hbadi commented Jun 8, 2026

Done — added the allocate-once / Clear()-on-reuse optimization you suggested, for SparseLU / SparseCholesky / SparseLDL (Double and Complex), pushed to this branch.

I folded it into this PR rather than opening a separate one because the reuse path is only reachable (and testable) via Refactorize — the existing TestRefactorize tests now also exercise it. Happy to split it out if you'd prefer.

One note on SparseLU: it trims L/U at the end of Factorize (Resize(0)), so on reuse the growth check may reallocate once before settling. Cholesky/LDL allocate the exact L size, so their reuse is fully allocation-free. If you'd like LU reuse to be allocation-free too, we could drop the end-trim or restore capacity after Clear() — your call.

I am delighted to contribute to CSparse.NET ! Please feel free to share any other improvements you may need.

@wo80
Copy link
Copy Markdown
Owner

wo80 commented Jun 8, 2026

I merged this, probably while you were working on the changes. Could you rebase* and do a new PR, so we get a clean commit history?

* Probably easier to just sync your fork, create a new working branch and bring the latest changes from feature/numeric-refactorize to that branch.

@wo80
Copy link
Copy Markdown
Owner

wo80 commented Jun 8, 2026

I am delighted to contribute to CSparse.NET

Very welcome!

@wo80
Copy link
Copy Markdown
Owner

wo80 commented Jun 8, 2026

One note on SparseLU: it trims L/U at the end of Factorize ...

Good catch. I think the resize should be avoided when using the factorization inside an iterative solver. You could wrap the resize operation inside a if (SparseMatrix.AutoTrimStorage) { do resize }, see AutoTrimStorage option

/// <summary>
/// Gets or sets a value indicating whether the storage should be
/// automatically resized to non-zeros count. Defaults to true.
/// </summary>
/// <remarks>
/// Affects only sparse matrix addition and multiplication.
/// </remarks>
public static bool AutoTrimStorage { get; set; } = true;

wo80 added a commit that referenced this pull request Jun 8, 2026
Reuse factor buffers across re-factorizations (follow-up to #55)
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