Harsh Singh

Full-Stack Developer & Open Source Contributor

I build performant web apps and contribute to scientific computing — React on the front, Julia & Go under the hood.

H
All posts
·6 min read

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 PureSDIRK implementation.
  • 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:

  • @generated perform_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.

Designed & built by Harsh Singh · singhharsh.in