Skip to content

Added support for multiterm (imex solvers) - #307

Merged
DanWaxman merged 30 commits into
mainfrom
imex_schemes
Aug 19, 2026
Merged

Added support for multiterm (imex solvers)#307
DanWaxman merged 30 commits into
mainfrom
imex_schemes

Conversation

@MatthieuDarcy

@MatthieuDarcy MatthieuDarcy commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Requires diffrax==0.7.2 which is only possible when CD-Dynamax is updated, so marking this as a draft in the meantime.
#304

@DanWaxman
DanWaxman changed the base branch from main to staging-advanced-pdes August 14, 2026 18:13
@MatthieuDarcy
MatthieuDarcy changed the base branch from staging-advanced-pdes to dw-upgrade-cd-dynamax August 14, 2026 19:43
Comment thread dynestyx/models/checkers.py Outdated
Checked structurally (not via `isinstance(..., ImExDrift)`) because the
concrete `ImExDrift` class lives in `state_evolution.py`, which imports
from `core.py`, which imports from this module -- importing it here
would cycle.

@mattlevine22 mattlevine22 Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This doesn't seem like a good reason to create a dummy class. Can you take a look to see what sort of file refactor / creation would be needed to avoid this circular import issue?

Comment thread dynestyx/types.py Outdated
raise NotImplementedError()


type _Drift = Callable[

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

How about models/types.py?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

just trying to think about big picture organization...dynestyx/types.py are pretty high-level types, so seems weird to drop ImExDrift there.

@mattlevine22 mattlevine22 Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

actually, I guess models/core.py is already mostly a "types.py" file.

@mattlevine22 mattlevine22 Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

How about models/drifts.py and include Drift, AffineDrift, and ImExDrift there?

@MatthieuDarcy MatthieuDarcy Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm going to be a bit verbose to make sure I understand things correctly, let me know if I'm off track.

Right now incheckers.py, we use _validate_continuous_state_evolution to check that we are not using both an ImExDrift AND a state_evolution.potential term (because we decided that combining these two is ambiguous), raising an error if so.

I think adding a drifts.py with Drift, AffineDrift, ImExDrift, Potential (?) and importing them into checkers.py could make sense.

One thing I'm not completely sure about is that diffusion.py is imported at the level of core.py, one level down so there is a difference between the two (and I would naively assume that they would mirror each other, I don't know if this is a concern). I could see if there is a way of checking the ImExDrift vs Potential at the level of core.py but it's definitely more complicated.

I want to add that I believe that we have 3 choices:

  1. add the check in checkers.py, in _validate_continuous_state_evolution.
  2. add the check in core.py, in ContinuousTimeStateEvolution (doing it during class instantiation(
  3. add the check in odes.py, in _solve
    I'm not a fan of 3, because the error will be raised much later.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think it makes sense to include Potential too!

Not sure I understand the concern about diffusions.py. Both drifts.py and diffusions.py would be leaf modules imported by core.py; and checkers.py would additionally import ImExDrift

That should be OK? want to understand the complication here

@MatthieuDarcy MatthieuDarcy Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm just wanted to make sure that it was fine that checkers.py depends on drifts.py but not on diffusions.py is fine. I might be overthinking this, if it's not an issue, then will implement.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

yeah, checkers.py COULD depend on diffusions.py if we needed it to though right?

We just don't do any diffusion checks

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes

dirfts.py now includes potential, drift, imexdrift. pure leaf file

imported by checkers.py amd core.py
@MatthieuDarcy
MatthieuDarcy marked this pull request as ready for review August 18, 2026 14:08
@MatthieuDarcy

Copy link
Copy Markdown
Contributor Author

There now is a drifts.py, imported by core.py and checkers.py. It's a leaf, no internal Dynestyx imports. I think the solution works pretty well, let me know what you think @mattlevine22

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I guess this file path should change to models/drifts/drift.md

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I guess this file path should change to models/drifts/potential.md

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

change file path

@mattlevine22

Copy link
Copy Markdown
Collaborator

There now is a drifts.py, imported by core.py and checkers.py. It's a leaf, no internal Dynestyx imports. I think the solution works pretty well, let me know what you think @mattlevine22

Yeah looks reasonable---I guess you need to change some of the docs .md file paths accordingly though

@mattlevine22

Copy link
Copy Markdown
Collaborator

@MatthieuDarcy I did a quick iteration with Codex, which picked up 2 issues:

  1. MultiTerm is not specific to IMEX. Diffrax also uses it for SDE and Langevin solvers, where the terms have different meanings. Checking only get_origin(solver.term_structure) is dfx.MultiTerm can therefore misclassify solvers like EulerHeun and pass them two ODE terms incorrectly.

    Recommended fix: require both MultiTerm and dfx.AbstractImplicitSolver, or use an explicit supported-solver check:

    is_imex_solver = (
        isinstance(solver, dfx.AbstractImplicitSolver)
        and get_origin(solver.term_structure) is dfx.MultiTerm
    )
  2. A potential drift can be silently discarded inside dsx.plate. The ImExDrift + potential validation is skipped in plate contexts, while the IMEX solve evaluates only explicit_term and implicit_term, bypassing total_drift and omitting the potential gradient.

    Recommended fix: perform the incompatibility check unconditionally, outside plate-skipped shape validation. Alternatively, have ContinuousTimeStateEvolution construct the complete IMEX decomposition, including the potential contribution. A plated regression test would be helpful.

@mattlevine22

Copy link
Copy Markdown
Collaborator

Also, the reaction diffusion notebook kinda uses IMEX at the end, but it doesn't show any plots or justify the choice

@MatthieuDarcy

MatthieuDarcy commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

@MatthieuDarcy I did a quick iteration with Codex, which picked up 2 issues:

1. `MultiTerm` is not specific to IMEX. Diffrax also uses it for SDE and Langevin solvers, where the terms have different meanings. Checking only `get_origin(solver.term_structure) is dfx.MultiTerm` can therefore misclassify solvers like `EulerHeun` and pass them two ODE terms incorrectly.
   Recommended fix: require both `MultiTerm` and `dfx.AbstractImplicitSolver`, or use an explicit supported-solver check:
   ```python
   is_imex_solver = (
       isinstance(solver, dfx.AbstractImplicitSolver)
       and get_origin(solver.term_structure) is dfx.MultiTerm
   )
   ```

2. A potential drift can be silently discarded inside `dsx.plate`. The `ImExDrift + potential` validation is skipped in plate contexts, while the IMEX solve evaluates only `explicit_term` and `implicit_term`, bypassing `total_drift` and omitting the potential gradient.
   Recommended fix: perform the incompatibility check unconditionally, outside plate-skipped shape validation. Alternatively, have `ContinuousTimeStateEvolution` construct the complete IMEX decomposition, including the potential contribution. A plated regression test would be helpful.

I fixed both.

  1. In addition to what you suggested, I added a check at the very beginning of solve_ode_state_path raising an error if the solver is of typedfx.AbstractStratonovichSolver ordfx.AbstractItoSolver. The reason for this is because it's technically possible to have implicit SDE solvers, they just don't exist in diffrax yet. Checking for dfx.AbstractImplicitSolver only could therefore lead to a bug in a potential future release? In principle this should not alter solve_ode_state_path as it should already error if given an SDE solver and the error message should now be clearer. Let me know if you approve of this (another option is to add this check only when there is an ImExDrift, more specific). I put the dfx.AbstractImplicitSolver check only.
  2. I added a specific check for ImExDrift and Potential in checkers.py , called outside the _inside_plate check, only checks for continuous_time.
    Will work on the docs later.

@mattlevine22

Copy link
Copy Markdown
Collaborator

Yeah looks good, thanks!

@MatthieuDarcy

Copy link
Copy Markdown
Contributor Author

Actually nevermind, ODE solvers can also be labelled as dfx.AbstractStratonovichSolver or dfx.AbstractItoSolver. I'm changing this to only check for AbstractImplicitSolver, might break in a future release but so be it.

@DanWaxman
DanWaxman changed the base branch from dw-upgrade-cd-dynamax to main August 19, 2026 02:48
@DanWaxman

Copy link
Copy Markdown
Collaborator

FYI I updated the PR to merge to main + made a merge commit, since the previous base branch landed on main.

@mattlevine22 mattlevine22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm happy with this PR, so approving it.

Would recommend updating the end of the notebook with a plot of the pde solution that used the imex solver though.

mattlevine22 and others added 3 commits August 19, 2026 09:12
changed the order
added a drifts section (similar to diffusion)
@MatthieuDarcy

MatthieuDarcy commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

@mattlevine22 I modified some of the API documentation in /core

The order is changed (it seemed a bit random, I tried to have more coherent order).

There now is a dedicated Driftssection, similar to Diffusions, with Drift, Potential, AffineDrift, and ImExDrift.
Note that AffineDrifthas its own separate documentation in Specialized, I didn't want to touch it.

I updated the notebook with a plot and added a justification for using Imex.

Let me know what you think, didn't want to merge without your approval.

@DanWaxman DanWaxman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Some small comments

Comment thread dynestyx/models/drifts.py
Comment thread dynestyx/models/drifts.py Outdated
Comment thread dynestyx/models/drifts.py
Comment on lines -281 to -301
class AffineDrift(eqx.Module):
"""
Affine drift function for continuous-time models.

This implements an affine map of the form

$$f(x, u, t) = A x + B u + b,$$

where $A \\in \\mathbb{R}^{d_x \\times d_x}$, $B \\in \\mathbb{R}^{d_x \\times d_u}$
(optional), and $b \\in \\mathbb{R}^{d_x}$ (optional). The time argument $t$
is accepted for compatibility with the `Drift` protocol but is not used.

This is commonly used as the drift term $\\mu(x_t, u_t, t)$ inside
`ContinuousTimeStateEvolution`, and is a building block for LTI models such as
`LTI_continuous`.

Attributes:
A (jax.Array): Drift matrix with shape $(d_x, d_x)$.
B (jax.Array | None): Optional control matrix with shape $(d_x, d_u)$.
b (jax.Array | None): Optional additive bias with shape $(d_x,)$.
"""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think we have enough (even if the total number is small) users and this is a common enough class that we should be keeping this API entrypoint with a deprecation warning that it will be removed in v0.5.0.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

state_evolution.py now imports AffineDrift from drifts.pyso this restores the API entrypoint (with a warning).

@DanWaxman

Copy link
Copy Markdown
Collaborator

Sorry I realized afterwards that Matt had approved it already lol. But these are tiny, I'm happy for it to be merged afterwards :)

@DanWaxman DanWaxman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks!!

@DanWaxman
DanWaxman merged commit 8788ba6 into main Aug 19, 2026
3 checks passed
@MatthieuDarcy
MatthieuDarcy deleted the imex_schemes branch August 19, 2026 17:09
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.

3 participants