Refactoring for Generality: Unifying Solver Tableaux in a Big Julia Library
- Julia
- Software Engineering
- Refactoring
- SciML
When people picture open-source contributions to a numerical library, they imagine adding a shiny new solver. In practice, my highest-leverage PRs to OrdinaryDiffEq.jl did the opposite: they removed code.
The problem: N solvers, N copies of the same step
A Runge–Kutta method is fully described by its Butcher tableau — a small table of coefficients. In theory, one stepping routine driven by a tableau can run any of them. In practice, mature libraries accrete a separate hand-written perform_step! for each method, because that's the fastest way to land "just one more solver." Over years, that becomes dozens of near-identical files that drift apart and hide subtle bugs.
The fix: generic, tableau-driven stepping
A recurring theme in my PRs was collapsing families into one generic implementation:
- Unified SDIRK/ESDIRK methods under a single dispatched
perform_step!. - Migrated one-off IMEX methods (CFNLIRK3, ABDF2's Euler cache) into a shared ESDIRKIMEX tableau form.
- Unified SFSDIRK and Hairer4/42 into a generic
PureSDIRKimplementation. - Unified DPRKN/ERKN (Runge–Kutta–Nyström) velocity-independent methods.
- Specialized Verner and LowStorageRK tableaux/constructors.
# Before: a bespoke stepper per method (×N)
# After: one stepper, the method *is* its tableau
struct GenericSDIRK{T} <: AbstractAlgorithm
tableau::T
end
The catch: don't regress performance
Julia's superpower is that generic code can be as fast as hand-written code — if you let the compiler specialize. The risk in this kind of refactor is accidental dynamic dispatch or allocations creeping in. So these PRs came with guardrails:
@generatedperform_step!to eliminate per-step overhead.- AllocCheck.jl allocation tests across every solver package, so a future change that allocates in the hot loop fails CI.
- Tableau embedded-pair consistency tests so adaptive stepping stays correct after the merge.
What I took away
- Deleting code is a feature. Less duplication means fewer places for bugs to hide.
- A refactor without tests is a gamble. Allocation and convergence tests are what let you change load-bearing code with confidence.
- Generality pays compound interest. Every method that now plugs into the generic path is a future PR that takes an afternoon instead of a week.
It's not glamorous work. But a library that thousands of researchers depend on is exactly where boring, careful generality matters most.