Skip to content

Commit ebfadf5

Browse files
Fix SROCK2 NoiseWrapper and non-square noise crashes (SciML#3188, SciML#3170)
Non-diagonal problems with a single noise channel were routed into the diagonal code paths by length(W.dW) == 1 shortcuts and crashed with MethodError (oop) or DimensionMismatch (iip). Routing now depends only on is_diagonal_noise and the W.dW type. The general-noise paths drew Rademacher variables through W.rng, which does not exist on NoiseWrapper. They now go through an init_chi! helper that finds the generator on the wrapped source process, and the commented J_qr cross terms from Abdulle, Vilmart and Zygalakis (2013), eq. (17), are activated so SROCK2 keeps weak order 2 for m > 1. The KomBurSROCK2 stage corrections from the original PR SciML#3468 already landed in SciML#3883 and are unchanged here.
1 parent 2111422 commit ebfadf5

7 files changed

Lines changed: 202 additions & 32 deletions

File tree

lib/StochasticDiffEqROCK/Project.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "StochasticDiffEqROCK"
22
uuid = "db241ea8-0e6b-4abc-8f2d-1adff2294fd9"
33
authors = ["Chris Rackauckas <accounts@chrisrackauckas.com>"]
4-
version = "2.0.2"
4+
version = "2.0.3"
55

66
[deps]
77
DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e"
@@ -16,6 +16,7 @@ StochasticDiffEqCore = "19c5a474-6cd1-4a5f-be79-46dc34e54d7f"
1616

1717
[extras]
1818
DiffEqDevTools = "f3b72e0c-5b89-59e1-b016-84e28bfd966d"
19+
DiffEqNoiseProcess = "77a26b50-5914-5dd7-bc55-306e6241c503"
1920
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
2021
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
2122
SDEProblemLibrary = "c72e72a9-a271-4b2b-8966-303ed956772e"
@@ -40,14 +41,15 @@ SciMLBase = "3.39"
4041
StaticArrays = "1.9.18"
4142
StochasticDiffEqCore = "2"
4243
DiffEqDevTools = "3"
44+
DiffEqNoiseProcess = "5.30"
4345
Random = "1"
4446
SDEProblemLibrary = "0.1, 1"
4547
Test = "<0.0.1, 1"
4648
SafeTestsets = "0.1.0"
4749
julia = "1.10"
4850

4951
[targets]
50-
test = ["Test", "Pkg", "SafeTestsets", "DiffEqDevTools", "SDEProblemLibrary", "Random", "SciMLTesting"]
52+
test = ["Test", "Pkg", "SafeTestsets", "DiffEqDevTools", "DiffEqNoiseProcess", "SDEProblemLibrary", "Random", "SciMLTesting"]
5153

5254
[sources.OrdinaryDiffEqCore]
5355
path = "../OrdinaryDiffEqCore"

lib/StochasticDiffEqROCK/src/caches/SROCK_caches.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ function alg_cache(
161161
uᵢ₋₁ = zero(u)
162162
uᵢ₋₂ = zero(u)
163163
Gₛ = zero(noise_rate_prototype)
164-
if (!alg.strong_order_1 || is_diagonal_noise(prob) || ΔW isa Number || length(ΔW) == 1)
164+
if (!alg.strong_order_1 || is_diagonal_noise(prob) || ΔW isa Number)
165165
Gₛ₁ = Gₛ
166166
else
167167
Gₛ₁ = zero(noise_rate_prototype)
@@ -295,7 +295,7 @@ function alg_cache(
295295
uᵢ₋₁ = zero(u)
296296
uᵢ₋₂ = zero(u)
297297
Gₛ = zero(noise_rate_prototype)
298-
if ΔW isa Number || length(ΔW) == 1 || is_diagonal_noise(prob)
298+
if ΔW isa Number || is_diagonal_noise(prob)
299299
Gₛ₁ = Gₛ
300300
else
301301
Gₛ₁ = zero(noise_rate_prototype)
@@ -377,7 +377,7 @@ function alg_cache(
377377
Xₛ₋₃ = zero(noise_rate_prototype)
378378
vec_χ = false .* vec(ΔW)
379379
WikRange = false .* vec(ΔW)
380-
if ΔW isa Number || length(ΔW) == 1 || is_diagonal_noise(prob)
380+
if ΔW isa Number || is_diagonal_noise(prob)
381381
Gₛ = Xₛ₋₁
382382
SXₛ₋₁ = utmp
383383
SXₛ₋₂ = utmp

lib/StochasticDiffEqROCK/src/perform_step/SROCK_perform_step.jl

Lines changed: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -194,10 +194,12 @@ end
194194
(; recf, recf2, mα, mσ, mτ) = cache
195195

196196
gen_prob = !(
197-
(is_diagonal_noise(integrator.sol.prob)) || (W.dW isa Number) ||
198-
(length(W.dW) == 1)
197+
(is_diagonal_noise(integrator.sol.prob)) || (W.dW isa Number)
199198
)
200-
gen_prob && (vec_χ = 2 .* floor.(false .* W.dW .+ 1 // 2 .+ oftype(W.dW, rand(W.rng, length(W.dW)))) .- true)
199+
if gen_prob
200+
vec_χ = similar(W.dW)
201+
init_χ!(vec_χ, W)
202+
end
201203

202204
alg = unwrap_alg(integrator, true)
203205
alg.eigen_est === nothing ? maxeig!(integrator, cache) : alg.eigen_est(integrator)
@@ -265,7 +267,7 @@ end
265267
# Now uᵢ₋₂ = uₛ₋₂, uᵢ₋₁ = uₛ₋₁, uᵢ = uₛ
266268
# Similarly tᵢ₋₂ = tₛ₋₂, tᵢ₋₁ = tₛ₋₁, tᵢ = tₛ
267269

268-
if (W.dW isa Number) || (length(W.dW) == 1) || is_diagonal_noise(integrator.sol.prob)
270+
if (W.dW isa Number) || is_diagonal_noise(integrator.sol.prob)
269271
Gₛ = integrator.f.g(uᵢ₋₁, p, tᵢ₋₁)
270272
u += Gₛ .* W.dW
271273
Gₛ = integrator.f.g(uᵢ, p, tᵢ)
@@ -300,7 +302,11 @@ end
300302
for i in 1:length(W.dW)
301303
WikJ = W.dW[i]
302304
WikJ2 = vec_χ[i]
303-
WikRange = 1 // 2 .* (W.dW .* WikJ .- (1:length(W.dW) .== i) .* abs(dt)) #.- (1:length(W.dW) .> i) .* dt .* vec_χ .+ (1:length(W.dW) .< i) .* dt .* WikJ2)
305+
WikRange = 1 // 2 .* (
306+
W.dW .* WikJ .- (1:length(W.dW) .== i) .* abs(dt) .-
307+
(1:length(W.dW) .> i) .* abs(dt) .* vec_χ .+
308+
(1:length(W.dW) .< i) .* abs(dt) .* WikJ2
309+
)
304310
uₓ = Gₛ * WikRange
305311
WikRange = 1 // 2 .* (1:length(W.dW) .== i)
306312
uᵢ₋₂ = uᵢ + uₓ
@@ -332,8 +338,7 @@ end
332338
(; recf, recf2, mα, mσ, mτ) = cache.constantcache
333339
ccache = cache.constantcache
334340
gen_prob = !(
335-
(is_diagonal_noise(integrator.sol.prob)) || (W.dW isa Number) ||
336-
(length(W.dW) == 1)
341+
(is_diagonal_noise(integrator.sol.prob)) || (W.dW isa Number)
337342
)
338343

339344
alg = unwrap_alg(integrator, true)
@@ -362,8 +367,7 @@ end
362367

363368
sqrt_dt = sqrt(abs(dt))
364369
if gen_prob
365-
vec_χ .= 1 // 2 .+ oftype(W.dW, rand(W.rng, length(W.dW)))
366-
@.. vec_χ = 2 * floor(vec_χ) - 1
370+
init_χ!(vec_χ, W)
367371
end
368372

369373
μ = recf[start] # here κ = 0
@@ -418,7 +422,7 @@ end
418422
# Now uᵢ₋₂ = uₛ₋₂, uᵢ₋₁ = uₛ₋₁, uᵢ = uₛ
419423
# Similarly tᵢ₋₂ = tₛ₋₂, tᵢ₋₁ = tₛ₋₁, tᵢ = tₛ
420424

421-
if (W.dW isa Number) || (length(W.dW) == 1) || is_diagonal_noise(integrator.sol.prob)
425+
if (W.dW isa Number) || is_diagonal_noise(integrator.sol.prob)
422426
integrator.f.g(Gₛ, uᵢ₋₁, p, tᵢ₋₁)
423427
@.. u += Gₛ * W.dW
424428
integrator.f.g(Gₛ, uᵢ, p, tᵢ)
@@ -458,7 +462,12 @@ end
458462
WikJ2 = vec_χ[i]
459463
dwrange = 1:length(W.dW)
460464
abs_dt = abs(dt)
461-
@.. WikRange = 1 // 2 * (W.dW * WikJ - (dwrange == i) * abs_dt) #+ (dwrange < i) * dt * WikJ2 - (dwrange > i) * dt * vec_χ)
465+
@.. WikRange = 1 // 2 *
466+
(
467+
W.dW * WikJ - (dwrange == i) * abs_dt -
468+
(dwrange > i) * abs_dt * vec_χ +
469+
(dwrange < i) * abs_dt * WikJ2
470+
)
462471
mul!(uₓ, Gₛ, WikRange)
463472
@.. uᵢ₋₂ = uᵢ + uₓ
464473
@.. WikRange = 1 // 2 * (dwrange == i)
@@ -542,14 +551,14 @@ end
542551
end
543552

544553
Gₛ = integrator.f.g(u, p, tᵢ)
545-
if (W.dW isa Number) || (length(W.dW) == 1) || is_diagonal_noise(integrator.sol.prob)
554+
if (W.dW isa Number) || is_diagonal_noise(integrator.sol.prob)
546555
u += Gₛ .* W.dW
547556
else
548557
u += Gₛ * W.dW
549558
end
550559

551560
if integrator.alg.strong_order_1
552-
if (W.dW isa Number) || (length(W.dW) == 1) ||
561+
if (W.dW isa Number) ||
553562
(is_diagonal_noise(integrator.sol.prob))
554563
uᵢ₋₂ = @. 1 // 2 * Gₛ * (W.dW^2 - abs(dt))
555564
tmp = @. u + uᵢ₋₂
@@ -633,15 +642,15 @@ end
633642
end
634643

635644
integrator.f.g(Gₛ, u, p, tᵢ)
636-
if (W.dW isa Number) || (length(W.dW) == 1) || is_diagonal_noise(integrator.sol.prob)
645+
if (W.dW isa Number) || is_diagonal_noise(integrator.sol.prob)
637646
@.. u += Gₛ * W.dW
638647
else
639648
mul!(uᵢ₋₁, Gₛ, W.dW)
640649
u += uᵢ₋₁
641650
end
642651

643652
if integrator.alg.strong_order_1
644-
if (W.dW isa Number) || (length(W.dW) == 1) ||
653+
if (W.dW isa Number) ||
645654
(is_diagonal_noise(integrator.sol.prob))
646655
@.. uᵢ₋₂ = 1 // 2 * Gₛ * (W.dW^2 - abs(dt))
647656
@.. tmp = u + uᵢ₋₂
@@ -982,7 +991,7 @@ end
982991
end
983992
end
984993

985-
if (W.dW isa Number) || (length(W.dW) == 1)
994+
if (W.dW isa Number)
986995
Gₛ = integrator.f.g(Û₁, p, t̂₁)
987996
uₓ += Gₛ * W.dW
988997

@@ -1168,7 +1177,7 @@ end
11681177
end
11691178
end
11701179

1171-
if (W.dW isa Number) || (length(W.dW) == 1) || is_diagonal_noise(integrator.sol.prob)
1180+
if (W.dW isa Number) || is_diagonal_noise(integrator.sol.prob)
11721181
integrator.f.g(Gₛ, Û₁, p, t̂₁)
11731182
@.. uₓ += Gₛ * W.dW
11741183

@@ -1227,8 +1236,7 @@ end
12271236
(; recf, mσ, mτ, mδ) = cache
12281237

12291238
gen_prob = !(
1230-
(is_diagonal_noise(integrator.sol.prob)) || (W.dW isa Number) ||
1231-
(length(W.dW) == 1)
1239+
(is_diagonal_noise(integrator.sol.prob)) || (W.dW isa Number)
12321240
)
12331241

12341242
alg = unwrap_alg(integrator, true)
@@ -1245,7 +1253,10 @@ end
12451253
τ = mτ[deg_index]
12461254

12471255
sqrt_dt = sqrt(abs(dt))
1248-
(gen_prob) && (vec_χ = 2 .* floor.(1 // 2 .+ false .* W.dW .+ rand(length(W.dW))) .- 1)
1256+
if gen_prob
1257+
vec_χ = similar(W.dW)
1258+
init_χ!(vec_χ, W)
1259+
end
12491260

12501261
tᵢ₋₂ = t
12511262
uᵢ₋₂ = uprev
@@ -1289,7 +1300,7 @@ end
12891300
tᵢ₋₁ += θₛ₋₃ * (tᵢ₋₁ - tᵢ₋₂)
12901301
tᵢ₋₂ = ttmp
12911302

1292-
if W.dW isa Number || length(W.dW) == 1 || is_diagonal_noise(integrator.sol.prob)
1303+
if W.dW isa Number || is_diagonal_noise(integrator.sol.prob)
12931304
# stage s-3
12941305
yₛ₋₃ = integrator.f(uᵢ₋₁, p, tᵢ₋₁)
12951306
utmp = uᵢ₋₁ + μₛ₋₃ * yₛ₋₃
@@ -1431,8 +1442,7 @@ end
14311442

14321443
ccache = cache.constantcache
14331444
gen_prob = !(
1434-
(is_diagonal_noise(integrator.sol.prob)) || (W.dW isa Number) ||
1435-
(length(W.dW) == 1)
1445+
(is_diagonal_noise(integrator.sol.prob)) || (W.dW isa Number)
14361446
)
14371447

14381448
alg = unwrap_alg(integrator, true)
@@ -1459,7 +1469,9 @@ end
14591469
τ = mτ[deg_index]
14601470

14611471
sqrt_dt = sqrt(abs(dt))
1462-
(gen_prob) && (vec_χ .= 2 .* floor.(1 // 2 .+ false .* vec_χ .+ rand(length(vec_χ))) .- 1)
1472+
if gen_prob
1473+
init_χ!(vec_χ, W)
1474+
end
14631475

14641476
tᵢ₋₂ = t
14651477
@.. uᵢ₋₂ = uprev
@@ -1502,7 +1514,7 @@ end
15021514
tᵢ₋₁ += θₛ₋₃ * (tᵢ₋₁ - tᵢ₋₂)
15031515
tᵢ₋₂ = ttmp
15041516

1505-
if W.dW isa Number || length(W.dW) == 1 || is_diagonal_noise(integrator.sol.prob)
1517+
if W.dW isa Number || is_diagonal_noise(integrator.sol.prob)
15061518
# stage s-3
15071519
integrator.f(yₛ₋₃, uᵢ₋₁, p, tᵢ₋₁)
15081520
@.. utmp = uᵢ₋₁ + μₛ₋₃ * yₛ₋₃
@@ -1711,7 +1723,7 @@ end
17111723
uᵢ₋₂ = integrator.f(uᵢ₋₂, p, tᵢ₋₂)
17121724
u += dt *+ τ) * uᵢ₋₂
17131725

1714-
if (W.dW isa Number) || (length(W.dW) == 1) || is_diagonal_noise(integrator.sol.prob)
1726+
if (W.dW isa Number) || is_diagonal_noise(integrator.sol.prob)
17151727
Gₛ = integrator.f.g(uᵢ₋₁, p, tᵢ₋₁)
17161728
u += Gₛ .* W.dW
17171729

@@ -1806,7 +1818,7 @@ end
18061818
integrator.f(k, uᵢ₋₂, p, tᵢ₋₂)
18071819
@.. u += dt *+ τ) * k
18081820

1809-
if (W.dW isa Number) || (length(W.dW) == 1) || is_diagonal_noise(integrator.sol.prob)
1821+
if (W.dW isa Number) || is_diagonal_noise(integrator.sol.prob)
18101822
integrator.f.g(Gₛ, uᵢ₋₁, p, tᵢ₋₁)
18111823
@.. u += Gₛ * W.dW
18121824

@@ -1840,3 +1852,18 @@ end
18401852

18411853
integrator.u = u
18421854
end
1855+
1856+
# Fill `vec_χ` with independent Rademacher (±1) samples drawn from the RNG of the
1857+
# integrator's noise process. Kept as a scalar loop so it works for both `Vector`
1858+
# and `MArray` storage without requiring the Random stdlib as a dependency.
1859+
function init_χ!(vec_χ, W)
1860+
r = rng(W)
1861+
for i in eachindex(vec_χ)
1862+
vec_χ[i] = 2 * (rand(r) < 1 // 2) - 1
1863+
end
1864+
return vec_χ
1865+
end
1866+
1867+
# `NoiseWrapper` does not carry its own `rng`; the generator lives on the wrapped
1868+
# source process, so accessing `W.rng` directly crashes (#3188).
1869+
rng(W) = hasfield(typeof(W), :rng) ? W.rng : W.source.rng

lib/StochasticDiffEqROCK/test/runtests.jl

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ if TEST_GROUP == "ALL" || TEST_GROUP == "Core"
2424
@time @safetestset "KomBurSROCK2 non-diagonal noise" begin
2525
include("kombursrock2_nondiag_tests.jl")
2626
end
27+
28+
@time @safetestset "SROCK non-diagonal noise regressions" begin
29+
include("srock_nondiag_tests.jl")
30+
end
2731
end
2832

2933
if TEST_GROUP == "ALL" || TEST_GROUP == "SROCKC2WeakConvergence"
@@ -32,6 +36,12 @@ if TEST_GROUP == "ALL" || TEST_GROUP == "SROCKC2WeakConvergence"
3236
end
3337
end
3438

39+
if TEST_GROUP == "ALL" || TEST_GROUP == "SROCK2NonDiagonalConvergence"
40+
@time @safetestset "SROCK2 Non-Diagonal Weak Convergence Tests" begin
41+
include("weak_convergence/weak_srock2_nondiag.jl")
42+
end
43+
end
44+
3545
# Run QA tests (Aqua, JET) - skip on pre-release Julia
3646
if (TEST_GROUP == "QA" || TEST_GROUP == "ALL") && isempty(VERSION.prerelease)
3747
activate_qa_env()
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using StochasticDiffEqROCK, DiffEqNoiseProcess, Test, Random
2+
import SciMLBase
3+
4+
# Non-square noise (n = 2 states, m = 1 Brownian motion) used to crash with
5+
# MethodError / DimensionMismatch because a length-one W.dW shortcut routed these
6+
# problems into the diagonal-noise code path, see SciML/OrdinaryDiffEq.jl#3170.
7+
@testset "m=1 non-diagonal noise does not crash" begin
8+
n, m = 2, 1
9+
f_oop(u, p, t) = zeros(n)
10+
g_oop(u, p, t) = ones(n, m)
11+
f_iip(du, u, p, t) = (du .= 0.0)
12+
g_iip(du, u, p, t) = (du .= 1.0)
13+
u0 = zeros(n)
14+
nrp = ones(n, m)
15+
prob_oop = SDEProblem(f_oop, g_oop, u0, (0.0, 1.0); noise_rate_prototype = nrp)
16+
prob_iip = SDEProblem(f_iip, g_iip, u0, (0.0, 1.0); noise_rate_prototype = nrp)
17+
18+
@testset "$(nameof(typeof(alg)))" for alg in (
19+
SROCK2(), SROCKEM(), SROCKC2(), KomBurSROCK2(), TangXiaoSROCK2(),
20+
)
21+
for prob in (prob_oop, prob_iip)
22+
Random.seed!(100)
23+
sol = solve(prob, alg, dt = 1 / 2^4)
24+
@test SciMLBase.successful_retcode(sol)
25+
@test all(isfinite, sol.u[end])
26+
end
27+
end
28+
end
29+
30+
# NoiseWrapper keeps its RNG on the wrapped source process, so the former direct
31+
# `W.rng` access crashed for non-diagonal problems, see SciML/OrdinaryDiffEq.jl#3188.
32+
@testset "SROCK2 with NoiseWrapper (non-diagonal noise)" begin
33+
f_iip(du, u, p, t) = (du .= -0.5 .* u)
34+
function g_iip(du, u, p, t)
35+
du[1, 1] = 0.1 * u[1]
36+
du[1, 2] = 0.05 * u[2]
37+
du[2, 1] = 0.05 * u[1]
38+
du[2, 2] = 0.1 * u[2]
39+
return nothing
40+
end
41+
u0 = [1.0, 1.0]
42+
tspan = (0.0, 1.0)
43+
Random.seed!(100)
44+
prob = SDEProblem(f_iip, g_iip, u0, tspan; noise_rate_prototype = zeros(2, 2))
45+
sol = solve(prob, SROCK2(), dt = 1 / 2^6, save_noise = true)
46+
@test SciMLBase.successful_retcode(sol)
47+
48+
prob_wrapped = SDEProblem(
49+
f_iip, g_iip, u0, tspan;
50+
noise = NoiseWrapper(sol.W), noise_rate_prototype = zeros(2, 2)
51+
)
52+
sol_wrapped = solve(prob_wrapped, SROCK2(), dt = 1 / 2^6)
53+
@test SciMLBase.successful_retcode(sol_wrapped)
54+
@test all(isfinite, sol_wrapped.u[end])
55+
end

lib/StochasticDiffEqROCK/test/test_groups.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,10 @@ versions = ["1"]
99
timeout = 240
1010
local_only = true
1111

12+
[SROCK2NonDiagonalConvergence]
13+
versions = ["1"]
14+
timeout = 240
15+
local_only = true
16+
1217
[QA]
1318
versions = ["lts", "1"]

0 commit comments

Comments
 (0)