Skip to content

Fix VR_FRM + noise_rate_prototype + adaptive SDE solver hang (#592)#593

Merged
ChrisRackauckas merged 1 commit into
SciML:masterfrom
ChrisRackauckas-Claude:fix-extended-jumparray-mul-zeroes-jump-u
May 23, 2026
Merged

Fix VR_FRM + noise_rate_prototype + adaptive SDE solver hang (#592)#593
ChrisRackauckas merged 1 commit into
SciML:masterfrom
ChrisRackauckas-Claude:fix-extended-jumparray-mul-zeroes-jump-u

Conversation

@ChrisRackauckas-Claude
Copy link
Copy Markdown
Contributor

Please ignore until reviewed by @ChrisRackauckas.

Closes #592.

Summary

mul!(c::ExtendedJumpArray, A, u) only writes to c.u when size(A, 1) == length(c.u) (the noise rate matrix is sized for the original state, not the extended state). The adaptive LambaEM step (and other adaptive non-diagonal SDE solvers) reuses the same scratchpad ExtendedJumpArray and writes scalar error-estimate values into c.jump_u during the error calculation. On the next step's mul!, c.jump_u is left untouched — so the SDE solver reads back those stale values and adds them to the integrator state as if they were a noise contribution on the jump-rate-integral component. The result is that jump_u is driven to ±1e15 within a few steps, the VR_FRM continuous-callback condition (u.jump_u[idx]) never crosses zero, and no variable-rate jumps ever fire.

The MRE in the issue (hybrid CLE↔SSA birth-death with noise_rate_prototype = zeros(1, 2)) reproduces this: after the CLE→SSA callback fires, u gets pinned at the threshold (95.0) forever even though the SSA rates are non-zero, because the corresponding continuous callbacks for the variable-rate jumps never trigger.

Fix

Zero c.jump_u after the partial mul!. Semantically: when the noise matrix only addresses the c.u portion, the noise contribution to c.jump_u is exactly zero, and mul! has overwrite semantics, so the result should reflect that — not whatever happened to be in the scratchpad.

function LinearAlgebra.mul!(c::ExtendedJumpArray, A::AbstractVecOrMat, u::AbstractVector)
    Nu = length(c.u)
    if size(A, 1) == Nu
        mul!(c.u, A, u)
        fill!(c.jump_u, zero(eltype(c.jump_u)))   # <-- added
    elseif size(A, 1) == length(c)
        mul!(c.u, @view(A[1:Nu, :]), u)
        mul!(c.jump_u, @view(A[(Nu + 1):end, :]), u)
    else
        ...
    end
end

Tests

  • test/extended_jump_array.jl: direct unit tests for both mul! branches.
  • test/variable_rate.jl: regression test that reproduces the hybrid CLE→SSA scenario from Potential bug when switching from SDE to SSA using Callbacks #592 with noise_rate_prototype and LambaEM(adaptive=true). The test verifies jump_u stays bounded (the bug drove it past ±1e15 within a few steps) and that the post-switch SSA jumps actually fire.

Test plan

  • CI green on Run Tests (InterfaceI / InterfaceII)
  • CI green on Spell Check, Downgrade, Thread Safety Tests
  • test/variable_rate.jl passes locally on Julia 1.11 with patched source
  • test/extended_jump_array.jl passes locally on Julia 1.11 with patched source
  • Manually verified the user's MRE from Potential bug when switching from SDE to SSA using Callbacks #592 now runs to completion and produces a trajectory that actually fires SSA jumps after the CLE→SSA switch (u decays from 95 → 17 over tspan = (0, 10) instead of getting pinned at 95)

🤖 Generated with Claude Code

`mul!(c::ExtendedJumpArray, A, u)` only writes to `c.u` when `A`
matches the original state size. The adaptive LambaEM step reuses the
same scratchpad ExtendedJumpArray and writes scalar error estimates
into `c.jump_u`, so the next step reads back those stale values as a
noise contribution on the jump-rate-integral state. With VR_FRM the
state-side integrator never observes `jump_u` crossing zero, so no
variable-rate jumps ever fire.

Zero `c.jump_u` after the partial mul! so the result reflects the
true semantics (no noise contribution on the jump-rate-integral
state) and the scratchpad cannot leak between adaptive steps.

Adds direct unit tests on `mul!` semantics and a regression test
that reproduces the hybrid CLE→SSA hang from the issue.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
@ChrisRackauckas-Claude
Copy link
Copy Markdown
Contributor Author

CI summary:

  • ✅ All test jobs pass: Tests (1, lts, pre × InterfaceI, InterfaceII, QA), GPU Tests, Spell Check
  • build (Documentation) fails — but this is a pre-existing failure on master since 2026-05-11 (master run), not caused by this PR. The error is UndefVarError: \JumpProblem` not defined in `Main.__atexample__named__tut3`indocs/src/tutorials/jump_diffusion.md— likely a name collision after the DifferentialEquations 8.0 / ModelingToolkitBase / Catalyst bump (the error log explicitly hintsJumpProblem` is "also exported by ModelingToolkitBase (loaded but not imported in Main)" and "also exported by Catalyst").

Investigating the master docs failure separately.

@ChrisRackauckas ChrisRackauckas marked this pull request as ready for review May 23, 2026 08:54
@ChrisRackauckas ChrisRackauckas merged commit 1126e3f into SciML:master May 23, 2026
11 of 12 checks passed
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.

Potential bug when switching from SDE to SSA using Callbacks

2 participants