Skip to content

feat: block preconditioners for IDAKLU - #6009

Draft
MarcBerliner wants to merge 1 commit into
mainfrom
feat/block-preconditioners
Draft

MarcBerliner wants to merge 1 commit into
mainfrom
feat/block-preconditioners

Conversation

@MarcBerliner

Copy link
Copy Markdown
Member

Description

Adds block preconditioners to IDAKLUSolver, so models with large spatial fields (for example a 3D thermal field coupled to an electrochemical model) solve with an iterative linear solver whose set-up, solve time and memory scale roughly linearly with the number of states.

Block preconditioners (pybamm.preconditioners)

  • A preconditioner is a dict of block solvers keyed by state variable name: KLU, ILU, Jacobi, Tridiagonal. Unlisted variables get an exact KLU block in the last stage, with that stage's settings.
  • model.default_preconditioner holds the model's default; Simulation(preconditioner=...) and Discretisation(preconditioner=...) override it, as with var_pts. It is compiled against the states at discretisation (model.preconditioner); nothing needs the model before then.
  • Stage nests Schur eliminations and combines a stage's blocks additively (block Jacobi) or multiplicatively (block Gauss-Seidel).
  • options={"linear_solver": "SUNLinSol_SPGMR", "preconditioner": "block"} applies it to a Krylov solver; options={"linear_solver": "block"} applies an exact one directly.
  • External block solvers plug in through a small C ABI (pybammsolvers.get_include()/pybamm_block_solver.h) and subclass BlockSolver. Block solvers, the compiled tree, and models carrying them all serialise (JSON and pickle).

Jacobian-free path

With an iterative linear solver and preconditioner="block", Krylov products come from the Jacobian action, so only the Jacobian entries the preconditioner reads are computed:

  • pybamm.JacobianSparsity propagates a structural sparsity pattern up the expression tree (each symbol class defines its rule), avoiding CasADi's sparsity detection, which is quadratic when a Jacobian has both a dense row and a dense column (a field mean read by every equation).
  • pybamm.casadi_jacobian differentiates the densest rows in reverse mode and the rest in forward mode. On a 51k-state model with such a mean, forward colouring alone needs 50,562 Jacobian-vector products per evaluation; bidirectional colouring needs 18.
  • pybamm.CasadiConversion folds linear operators with input-only coefficients into one sparse product and computes input-only values once per solve rather than at every evaluation.

The default direct path (SUNLinSol_KLU) is unchanged: the same Jacobian, evaluation cost and results as main.

Other changes

  • BaseModel.jacobian_sparsity() and BaseModel.spy() work on any discretised model; IDAKLUSolver.spy/get_jacobian_sparsity are deprecated.
  • newton_mode="full" now converges consistent initial conditions (identity rows for differential states) and no longer segfaults with iterative linear solvers; BBDP, which reads IDA state that is unset there, raises a SolverError with it.
  • A failed first step reports its real cause (e.g. IDA_CONV_FAIL) instead of IDA_BAD_K.
  • Discretised models saved with save_model keep their compiled preconditioner.
  • Experiment state mappers take their input shapes from the model, so experiments with per-cell input parameters can be built before their inputs are given.

Benchmarks

SPM, SPMe and DFN coupled to a 3D heat equation on an unstructured hex mesh of a cylindrical cell; 600 s discharge; block Gauss-Seidel with ILU(fill=1) on the temperature field and KLU elsewhere.

Model States Build Set-up Solve Peak RSS
SPM 51k 0.25 s 1.2 s 1.0 s 0.9 GB
SPM 402k 1.8 s 9.4 s 12.0 s 4.9 GB
SPM 1.02M 4.6 s 25.2 s 32.4 s 9.3 GB
SPMe 51k 0.31 s 1.4 s 1.1 s 0.9 GB
SPMe 402k 1.9 s 10.9 s 11.2 s 4.9 GB
SPMe 1.02M 4.6 s 28.6 s 41.6 s 9.4 GB
DFN 52k 0.28 s 1.4 s 4.8 s 0.9 GB
DFN 403k 1.9 s 10.7 s 38.4 s 5.1 GB
DFN 1.02M 4.6 s 27.6 s 95.3 s 11.1 GB

For comparison, the default SUNLinSol_KLU path takes about 15 s to set up and 10 s to solve at 6k states. A CC-CV cycle (1C discharge, rest, C/2 charge, CV hold to C/50) at about 100k states runs in 27 s (SPM) and 64 s (DFN), peak 3.8 GB.

On standard models the default path keeps main's Jacobian and results; CC-CV with the block preconditioner on the DFN matches SUNLinSol_KLU to within 2 mV.

Type of change

Feature, with one breaking change (the "preconditioner" option defaults to "block", which only affects iterative linear solvers) and one deprecation; see CHANGELOG.md.

Important checks:

Please confirm the following before marking the PR as ready for review:

  • No style issues: nox -s pre-commit
  • All tests pass: nox -s tests
  • The documentation builds: nox -s doctests
  • Code is commented for hard-to-understand areas
  • Tests added that prove fix is effective or that feature works

- pybamm.preconditioners: block solvers (KLU, ILU, Jacobi, Tridiagonal) keyed by
  state variable, staged Schur eliminations and additive or multiplicative stages,
  compiled at discretisation; extensible through a C block-solver plugin ABI.
- Jacobian-free path: an iterative linear solver with preconditioner='block'
  computes only the Jacobian entries the preconditioner reads, from a structural
  sparsity pattern with bidirectional colouring, folds linear operators and
  computes input-only values once per solve.
- BaseModel.jacobian_sparsity and spy; IDAKLUSolver.spy is deprecated.
- Fixes for newton_mode='full' initial conditions, first-step error reporting,
  discretised model serialisation of the preconditioner, and experiment state
  mappers with per-cell inputs.

void example_destroy(void *context) { delete static_cast<ExampleBlockSolver *>(context); }

pybamm_block_solver example_block_solver = {
PYBAMM_BLOCK_SOLVER_CAPSULE);
}, "Capsule of an example block solver plugin (Jacobi), for tests");

py::class_<BlockAnalysis, std::shared_ptr<BlockAnalysis>>(m, "BlockAnalysis")
}, "Sorted positions of the iteration matrix's CSC data that the tree reads")
.def_readonly("size", &BlockAnalysis::size);

py::class_<BlockPreconditioner>(m, "BlockPreconditioner")
std::string("preconditioner tree node is missing \"") + key + "\"");
}
auto array = node[key].cast<
py::array_t<int64_t, py::array::c_style | py::array::forcecast>>();
return static_cast<PreconditionerSolverContent *>(S->content);
}

SUNLinearSolver_Type get_type(SUNLinearSolver) { return SUNLINEARSOLVER_DIRECT; }

py::class_<BlockAnalysis, std::shared_ptr<BlockAnalysis>>(m, "BlockAnalysis")
.def(py::init([](const py::dict &tree,
py::array_t<int64_t, py::array::c_style | py::array::forcecast> indptr,
py::class_<BlockAnalysis, std::shared_ptr<BlockAnalysis>>(m, "BlockAnalysis")
.def(py::init([](const py::dict &tree,
py::array_t<int64_t, py::array::c_style | py::array::forcecast> indptr,
py::array_t<int64_t, py::array::c_style | py::array::forcecast> indices) {
.def_readonly("size", &BlockAnalysis::size);

py::class_<BlockPreconditioner>(m, "BlockPreconditioner")
.def(py::init([](std::shared_ptr<BlockAnalysis> analysis) {
.def(py::init([](std::shared_ptr<BlockAnalysis> analysis) {
return std::make_unique<BlockPreconditioner>(analysis);
}), py::arg("analysis"))
.def("setup", [](BlockPreconditioner &self, np_array matrix_data) {
return self.setup(matrix_data.data());
}, "Set up from the iteration matrix's CSC data; 0 on success",
py::arg("matrix_data"))
.def("solve", [](BlockPreconditioner &self, np_array r) {
@codecov

codecov Bot commented Oct 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.45455% with 33 lines in your changes missing coverage. Please review.
✅ Project coverage is 98.18%. Comparing base (63476bc) to head (1898d11).

Files with missing lines Patch % Lines
...amm/expression_tree/operations/linear_operators.py 90.99% 10 Missing ⚠️
...bamm/src/pybamm/expression_tree/unary_operators.py 77.77% 4 Missing ⚠️
...amm/src/pybamm/expression_tree/binary_operators.py 85.00% 3 Missing ⚠️
packages/pybamm/src/pybamm/models/base_model.py 94.54% 3 Missing ⚠️
packages/pybamm/src/pybamm/solvers/base_solver.py 92.30% 3 Missing ⚠️
.../pybamm/src/pybamm/solvers/preconditioners/tree.py 97.29% 3 Missing ⚠️
...ybamm/src/pybamm/expression_tree/concatenations.py 87.50% 2 Missing ⚠️
...ges/pybamm/src/pybamm/expression_tree/functions.py 83.33% 2 Missing ⚠️
...mm/expression_tree/operations/jacobian_sparsity.py 98.47% 2 Missing ⚠️
...ackages/pybamm/src/pybamm/solvers/idaklu_solver.py 98.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6009      +/-   ##
==========================================
- Coverage   98.23%   98.18%   -0.05%     
==========================================
  Files         350      356       +6     
  Lines       36173    36858     +685     
==========================================
+ Hits        35533    36188     +655     
- Misses        640      670      +30     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

This branch has not been deployed

No deployments
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