Skip to content

Conversation

@KrishGaur1354
Copy link

@KrishGaur1354 KrishGaur1354 commented Jul 27, 2025

This PR refactors the SDIRK solver implementation to use a unified tableau structure (SDIRKTableau)
and a single generic perform_step! function, consolidating 95% of SDIRK methods and reducing code
duplication. All methods now share consistent tableau coefficients while maintaining
full backward compatibility and performance. Special handling preserved for TRBDF2's unique structure.
KenCarp/Kvaerno methods retain separate implementations for additive splitting complexity.

Remaining Work:

  • KenCarp/Kvaerno Family (18 cache types):
    • Kvaerno3/4/5 (ConstantCache + Cache variants)
    • KenCarp3/4/5/47/58 (ConstantCache + Cache variants)
    • CFNLIRK3 (ConstantCache + Cache variants)
    • Kvaerno3/4/5 (ConstantCache + Cache variants)
    • KenCarp3/4/5/47/58 (ConstantCache + Cache variants)
    • CFNLIRK3 (ConstantCache + Cache variants)
  • Hairer Methods (2 cache types):
    • Hairer4ConstantCache, Hairer4Cache
  • Special Array Variant:
    • TRBDF2Cache{<:Array} (specialized version)
  • Duplication Removal of SDIRK related code
  • Re-run tests to check if working proper

Part of SciML Small Grants Program for OrdinaryDiffEq.jl solver set refactoring

- Implement SDIRKTableau struct consolidating all Butcher coefficients
- Add generic_sdirk_perform_step! reducing duplication across 20+ methods
- Migrate ImplicitEuler, SDIRK2/22, Cash4, all ESDIRK/SFSDIRK methods
- Preserve TRBDF2 special handling with unified coefficients
- Unify KenCarp/Kvaerno tableaus (separate perform_step! for additive splitting)
- Reduce codebase by ~3000 lines while maintaining full compatibility
- Pass all tests (JET, Aqua, integration)

Part of SciML Small Grants: OrdinaryDiffEq.jl tableau refactoring
Claimed by: Krish Gaur (July 4 - August 4, 2025)
Ref: https://github.com/SciML/sciml.ai/blob/master/small_grants.md#refactor-ordinarydiffeqjl-solver-sets-to-reuse-perform_step-implementations-via-tableaus-100solver-set
@KrishGaur1354 KrishGaur1354 marked this pull request as ready for review July 28, 2025 12:55
KrishGaur1354 and others added 2 commits July 29, 2025 12:30
…d Added missing tableau definitions for Hairer4, CFNLIRK3, KenCarp47, and KenCarp58
@KrishGaur1354
Copy link
Author

@ChrisRackauckas could you provide any suggestions or any changes I should make?

@oscardssmith
Copy link
Member

The primary thing this PR seems to be missing is a bunch of deletions. If this PR unifies the SDIRK tableau structure and perform steps, why isn't it deleting the old tableaus and perform step methods?

@ChrisRackauckas
Copy link
Member

What's the unique structure? TRBDF2 and KenCarps should be able to join just fine. Any step predictor here is just a linear combination of previous values, which these match, so you just need an alpha array of step predictor coefficients.

@KrishGaur1354
Copy link
Author

What's the unique structure? TRBDF2 and KenCarps should be able to join just fine. Any step predictor here is just a linear combination of previous values, which these match, so you just need an alpha array of step predictor coefficients.

Sure, I'll just ensure that the TRBDF2 and KenCarp methods would now be unified under consistent structure, which will utilize shared tableau and step predictor coefficients for it.

d = T(1 - sqrt(2) / 2)
ω = T(sqrt(2) / 4)

A = @SMatrix [0 0 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are static arrays actually faster here than just normal Vector/Matrix?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I did found static arrays being significantly faster than normal Vector/Matrix for SDIRK tableau operations by almost 1.8 times faster.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's interesting (and somewhat unfortunate). I would have expected the nonlinear solve cost to dominate.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Atleast the test that I did, Static Array was faster but the construction was slower due to compilation overhead in this case.

@KrishGaur1354 KrishGaur1354 changed the title [WIP]: Refactoring and implementing unified tableau structure and generic perform_step! for SDIRK solver set Refactoring and implementing unified tableau structure and generic perform_step! for SDIRK solver set Aug 6, 2025
uprev, uprev2, f, t, dt, reltol, p, calck,
::Val{false}) where {uEltypeNoUnits, uBottomEltypeNoUnits, tTypeNoUnits}
tab = Kvaerno3Tableau(constvalue(uBottomEltypeNoUnits), constvalue(tTypeNoUnits))
tab = get_sdirk_tableau(:Kvaerno3, constvalue(uBottomEltypeNoUnits), constvalue(tTypeNoUnits))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for Symbol rather than alg dispatch here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to get_sdirk_tableau(:Kvaerno3, ...) for uniformity during the refactor. I can change it if you want?

@ChrisRackauckas
Copy link
Member

Yes this is right on track. Looking fairly close. Left a bunch of comments.

Comment on lines 154 to 155
# For IMEX schemes, include explicit error contributions
# (This would require additional error estimation tableau components)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same as before?

Comment on lines 343 to 349
# Dispatch for unified caches - all SDIRK algorithms use these same cache types
@muladd function perform_step!(integrator, cache::SDIRKCache, repeat_step=false)
generic_sdirk_perform_step!(integrator, cache, repeat_step)
end

@muladd function perform_step!(integrator, cache::SDIRKConstantCache, repeat_step=false)
generic_sdirk_perform_step!(integrator, cache, repeat_step)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can just move the code into here instead of having a different function call?

is_A_stable::Bool
is_L_stable::Bool
predictor_type::Symbol
A_explicit::Union{SMatrix{S, S, T}, Nothing}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not type-stable to grab these: should just parameterize this?

b_embed=b_embed, embedded_order=3,
is_fsal=false, is_stiffly_accurate=true,
is_A_stable=true, is_L_stable=true,
predictor_type=:trbdf2_special)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did TRBDF2 not have alpha_preds?

c = @SVector [T2(1.0)]
γ = T(1.0)

SDIRKTableau(A, b, c, γ, 1;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is the special error estimators of ImplicitEuler and Trapezoidal handled?

is_A_stable=true, is_L_stable=false,
predictor_type=:kencarp_additive,
has_additive_splitting=true,
A_explicit=A_explicit, b_explicit=b_explicit, c_explicit=c_explicit)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This didn't have alpha preds?

A_explicit, b_explicit, c_explicit, α_pred)
end

function TRBDF2Tableau_unified(T=Float64, T2=Float64)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function TRBDF2Tableau_unified(T=Float64, T2=Float64)
function TRBDF2Tableau_unified(::Type{T}=Float64, ::Type{T2}=Float64) where {T,T2}

Need to force specialization on these in order to get inference.

end
end

get_sdirk_tableau(alg, T=Float64, T2=Float64) = get_sdirk_tableau(nameof(typeof(alg)), T, T2)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
get_sdirk_tableau(alg, T=Float64, T2=Float64) = get_sdirk_tableau(nameof(typeof(alg)), T, T2)
get_sdirk_tableau(alg, ::Type{T}=Float64, ::Type{T2}=Float64) where {T,T2} = get_sdirk_tableau(nameof(typeof(alg)), T, T2)

predictor_type=:hermite)
end

function get_sdirk_tableau(alg::Symbol, T=Float64, T2=Float64)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function get_sdirk_tableau(alg::Symbol, T=Float64, T2=Float64)
function get_sdirk_tableau(alg::Symbol, ::Type{T}=Float64, ::Type{T2}=Float64) where {T,T2}

@ChrisRackauckas
Copy link
Member

Okay this is generally looking right. Left some comments. Probably just needs one more small round? Most are just little inference things. I think the only thing that's not trivial inference stuff is:

  1. ImplicitEuler and Trapezoidal had the SPICE error estimators, how are those handled? I don't see that in there?
  2. There are some methods that had built-in step predictors like TRBDF2 and KenCarps that I don't see changed to using the alphas? Curious if that was intentional.

predictor_type=:hermite)
end

function get_sdirk_tableau(alg::Symbol, ::Type{T}=Float64, ::Type{T2}=Float64) where {T, T2}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit wonky. It should instead just be like SDIRKTableau(KenCarp4(), ...) i.e. dispatch on the algorithm rather than different functions and this would all go away. But I can just refactor that at some later point. I wouldn't worry about this for now but it is a very weird way to do dispatch.

SFSDIRK5Cache(u, uprev, fsalfirst, z₁, z₂, z₃, z₄, z₅, z₆, atmp, nlsolver, tab)
end
# Unified alg_cache functions for mutable cache
function alg_cache(alg::Union{ImplicitEuler, ImplicitMidpoint, Trapezoid, TRBDF2, SDIRK2, SDIRK22, SSPSDIRK2,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there not an abstract type for all of the SDIRK methods to dispatch with? That seems much more sane than a big union

step_limiter!(u, integrator, p, t + dt)

if integrator.opts.adaptive
if tab.has_spice_error && integrator.success_iter > 0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems the Trapezoid one is missing? This is just the implicit euler one?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and it's probably best to just if integrator.alg isa ImplicitEuler here, since it only applies to exactly that truncation error estimate.

@ChrisRackauckas
Copy link
Member

I think this looks good?

@oscardssmith take a look as well.

And @KrishGaur1354 run some of the stiff ODE benchmarks before and after like https://docs.sciml.ai/SciMLBenchmarksOutput/stable/StiffODE/Hires/ and https://docs.sciml.ai/SciMLBenchmarksOutput/stable/StiffODE/Pollution/ to see if there are any regressions, TRBDF2, ImplicitEuler, Trapezoid, and KenCarp4 in particular.

There are a few refactor things, specifically how the tableaus dispatch and how the cache dispatch is written that I noted, but those are relatively minor and I'd be willing to just Claude that after merging if there's no regressions and if @oscardssmith also approves.

@KrishGaur1354
Copy link
Author

Sure @ChrisRackauckas , I'll run the stiff ODE benchmarks with TRBDF2, impliciteuler, trapezoid, and KenCarp4 before and after the refactor, and report any regressions.
If everything looks stable, I’ll update here so we can move forward with the merge.

Also, apologies for the long gaps between updates, was a bit occupied, but I'm fully back on it now.

Let me know if you'd like me to include any additional benchmark cases.

@oscardssmith oscardssmith self-assigned this Nov 7, 2025
@ChrisRackauckas
Copy link
Member

No worries, take the time you need. Near the end now!

…proper isa checks

- Implemented α_pred predictor coefficients for TRBDF2, KenCarp3, and Kvaerno3
- Fixed SPICE error estimator broadcast bug by removing incorrect internalnorm calls
- Added ImplicitEuler cache compatibility for BDF module integration
@KrishGaur1354
Copy link
Author

No worries, take the time you need. Near the end now!

So, @ChrisRackauckas I have addressed some of the things you recommended to look into:

  • ImplicitEuler and Trapezoid SPICE error estimators which is now implemented with isa checks
  • TRBDF2, KenCarp3, Kvaerno3 which now use α_pred predictors
  • Package compiles successfully

Place where I got stuck: Trapezoid and TRBDF2 fail on first step because SPICE estimators need success_iter > 0. With no history and no embedded method, they set EEst = 1 causing immediate failure. So, how should these handle the first step before SPICE history exists?

ImplicitEuler and KenCarp4 work correctly. Once the first-step case is resolved, I can run full benchmarks.

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.

4 participants