Skip to content

Commit 0b3efcc

Browse files
Fix SROCK2 NoiseWrapper and non-square noise crashes (#3188, #3170) (#3468)
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 whose accessor walks the source chain of a wrapped process and falls back to the global generator for processes that replay a recorded path and carry none, such as NoiseGrid. KomBurSROCK2 draws from the same helper, so its trajectories for general noise differ from the previous global rand draws. The commented J_qr cross terms of Abdulle, Vilmart and Zygalakis (2013), eq. (17), are activated. They cancel for commutative diffusions, so the new coverage for them is a second moment check on a problem whose two noise channels do not commute. The KomBurSROCK2 stage corrections from the original submission already landed in #3883 and are unchanged here.
1 parent 3ef1504 commit 0b3efcc

8 files changed

Lines changed: 289 additions & 33 deletions

File tree

lib/StochasticDiffEqROCK/Project.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
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"
88
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
99
MuladdMacro = "46d2c3a1-f734-5fdb-9937-b9b9aeba4221"
1010
OrdinaryDiffEqCore = "bbf590c4-e513-4bbe-9b18-05decba2e5d8"
11+
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
1112
RecursiveArrayTools = "731186ca-8d62-57ce-b412-fbd966d074cd"
1213
Reexport = "189a3867-3050-52da-a836-e630ba90ab69"
1314
SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462"
@@ -16,8 +17,8 @@ StochasticDiffEqCore = "19c5a474-6cd1-4a5f-be79-46dc34e54d7f"
1617

1718
[extras]
1819
DiffEqDevTools = "f3b72e0c-5b89-59e1-b016-84e28bfd966d"
20+
DiffEqNoiseProcess = "77a26b50-5914-5dd7-bc55-306e6241c503"
1921
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
20-
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
2122
SDEProblemLibrary = "c72e72a9-a271-4b2b-8966-303ed956772e"
2223
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
2324
SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f"
@@ -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", "SciMLTesting"]
5153

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

lib/StochasticDiffEqROCK/src/StochasticDiffEqROCK.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import DiffEqBase: full_cache, rand_cache, ratenoise_cache
1919
import MuladdMacro: @muladd
2020
import SciMLBase
2121

22+
import Random
23+
2224
using LinearAlgebra
2325
using StaticArrays
2426
using RecursiveArrayTools

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,9 @@ 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+
vec_χ = gen_prob ? init_χ!(similar(W.dW), W) : nothing
201200

202201
alg = unwrap_alg(integrator, true)
203202
alg.eigen_est === nothing ? maxeig!(integrator, cache) : alg.eigen_est(integrator)
@@ -265,7 +264,7 @@ end
265264
# Now uᵢ₋₂ = uₛ₋₂, uᵢ₋₁ = uₛ₋₁, uᵢ = uₛ
266265
# Similarly tᵢ₋₂ = tₛ₋₂, tᵢ₋₁ = tₛ₋₁, tᵢ = tₛ
267266

268-
if (W.dW isa Number) || (length(W.dW) == 1) || is_diagonal_noise(integrator.sol.prob)
267+
if (W.dW isa Number) || is_diagonal_noise(integrator.sol.prob)
269268
Gₛ = integrator.f.g(uᵢ₋₁, p, tᵢ₋₁)
270269
u += Gₛ .* W.dW
271270
Gₛ = integrator.f.g(uᵢ, p, tᵢ)
@@ -300,7 +299,11 @@ end
300299
for i in 1:length(W.dW)
301300
WikJ = W.dW[i]
302301
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)
302+
WikRange = 1 // 2 .* (
303+
W.dW .* WikJ .- (1:length(W.dW) .== i) .* abs(dt) .-
304+
(1:length(W.dW) .> i) .* abs(dt) .* vec_χ .+
305+
(1:length(W.dW) .< i) .* abs(dt) .* WikJ2
306+
)
304307
uₓ = Gₛ * WikRange
305308
WikRange = 1 // 2 .* (1:length(W.dW) .== i)
306309
uᵢ₋₂ = uᵢ + uₓ
@@ -332,8 +335,7 @@ end
332335
(; recf, recf2, mα, mσ, mτ) = cache.constantcache
333336
ccache = cache.constantcache
334337
gen_prob = !(
335-
(is_diagonal_noise(integrator.sol.prob)) || (W.dW isa Number) ||
336-
(length(W.dW) == 1)
338+
(is_diagonal_noise(integrator.sol.prob)) || (W.dW isa Number)
337339
)
338340

339341
alg = unwrap_alg(integrator, true)
@@ -362,8 +364,7 @@ end
362364

363365
sqrt_dt = sqrt(abs(dt))
364366
if gen_prob
365-
vec_χ .= 1 // 2 .+ oftype(W.dW, rand(W.rng, length(W.dW)))
366-
@.. vec_χ = 2 * floor(vec_χ) - 1
367+
init_χ!(vec_χ, W)
367368
end
368369

369370
μ = recf[start] # here κ = 0
@@ -418,7 +419,7 @@ end
418419
# Now uᵢ₋₂ = uₛ₋₂, uᵢ₋₁ = uₛ₋₁, uᵢ = uₛ
419420
# Similarly tᵢ₋₂ = tₛ₋₂, tᵢ₋₁ = tₛ₋₁, tᵢ = tₛ
420421

421-
if (W.dW isa Number) || (length(W.dW) == 1) || is_diagonal_noise(integrator.sol.prob)
422+
if (W.dW isa Number) || is_diagonal_noise(integrator.sol.prob)
422423
integrator.f.g(Gₛ, uᵢ₋₁, p, tᵢ₋₁)
423424
@.. u += Gₛ * W.dW
424425
integrator.f.g(Gₛ, uᵢ, p, tᵢ)
@@ -458,7 +459,12 @@ end
458459
WikJ2 = vec_χ[i]
459460
dwrange = 1:length(W.dW)
460461
abs_dt = abs(dt)
461-
@.. WikRange = 1 // 2 * (W.dW * WikJ - (dwrange == i) * abs_dt) #+ (dwrange < i) * dt * WikJ2 - (dwrange > i) * dt * vec_χ)
462+
@.. WikRange = 1 // 2 *
463+
(
464+
W.dW * WikJ - (dwrange == i) * abs_dt -
465+
(dwrange > i) * abs_dt * vec_χ +
466+
(dwrange < i) * abs_dt * WikJ2
467+
)
462468
mul!(uₓ, Gₛ, WikRange)
463469
@.. uᵢ₋₂ = uᵢ + uₓ
464470
@.. WikRange = 1 // 2 * (dwrange == i)
@@ -542,14 +548,14 @@ end
542548
end
543549

544550
Gₛ = integrator.f.g(u, p, tᵢ)
545-
if (W.dW isa Number) || (length(W.dW) == 1) || is_diagonal_noise(integrator.sol.prob)
551+
if (W.dW isa Number) || is_diagonal_noise(integrator.sol.prob)
546552
u += Gₛ .* W.dW
547553
else
548554
u += Gₛ * W.dW
549555
end
550556

551557
if integrator.alg.strong_order_1
552-
if (W.dW isa Number) || (length(W.dW) == 1) ||
558+
if (W.dW isa Number) ||
553559
(is_diagonal_noise(integrator.sol.prob))
554560
uᵢ₋₂ = @. 1 // 2 * Gₛ * (W.dW^2 - abs(dt))
555561
tmp = @. u + uᵢ₋₂
@@ -633,15 +639,15 @@ end
633639
end
634640

635641
integrator.f.g(Gₛ, u, p, tᵢ)
636-
if (W.dW isa Number) || (length(W.dW) == 1) || is_diagonal_noise(integrator.sol.prob)
642+
if (W.dW isa Number) || is_diagonal_noise(integrator.sol.prob)
637643
@.. u += Gₛ * W.dW
638644
else
639645
mul!(uᵢ₋₁, Gₛ, W.dW)
640646
u += uᵢ₋₁
641647
end
642648

643649
if integrator.alg.strong_order_1
644-
if (W.dW isa Number) || (length(W.dW) == 1) ||
650+
if (W.dW isa Number) ||
645651
(is_diagonal_noise(integrator.sol.prob))
646652
@.. uᵢ₋₂ = 1 // 2 * Gₛ * (W.dW^2 - abs(dt))
647653
@.. tmp = u + uᵢ₋₂
@@ -982,7 +988,7 @@ end
982988
end
983989
end
984990

985-
if (W.dW isa Number) || (length(W.dW) == 1)
991+
if (W.dW isa Number)
986992
Gₛ = integrator.f.g(Û₁, p, t̂₁)
987993
uₓ += Gₛ * W.dW
988994

@@ -1168,7 +1174,7 @@ end
11681174
end
11691175
end
11701176

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

@@ -1227,8 +1233,7 @@ end
12271233
(; recf, mσ, mτ, mδ) = cache
12281234

12291235
gen_prob = !(
1230-
(is_diagonal_noise(integrator.sol.prob)) || (W.dW isa Number) ||
1231-
(length(W.dW) == 1)
1236+
(is_diagonal_noise(integrator.sol.prob)) || (W.dW isa Number)
12321237
)
12331238

12341239
alg = unwrap_alg(integrator, true)
@@ -1245,7 +1250,7 @@ end
12451250
τ = mτ[deg_index]
12461251

12471252
sqrt_dt = sqrt(abs(dt))
1248-
(gen_prob) && (vec_χ = 2 .* floor.(1 // 2 .+ false .* W.dW .+ rand(length(W.dW))) .- 1)
1253+
vec_χ = gen_prob ? init_χ!(similar(W.dW), W) : nothing
12491254

12501255
tᵢ₋₂ = t
12511256
uᵢ₋₂ = uprev
@@ -1289,7 +1294,7 @@ end
12891294
tᵢ₋₁ += θₛ₋₃ * (tᵢ₋₁ - tᵢ₋₂)
12901295
tᵢ₋₂ = ttmp
12911296

1292-
if W.dW isa Number || length(W.dW) == 1 || is_diagonal_noise(integrator.sol.prob)
1297+
if W.dW isa Number || is_diagonal_noise(integrator.sol.prob)
12931298
# stage s-3
12941299
yₛ₋₃ = integrator.f(uᵢ₋₁, p, tᵢ₋₁)
12951300
utmp = uᵢ₋₁ + μₛ₋₃ * yₛ₋₃
@@ -1431,8 +1436,7 @@ end
14311436

14321437
ccache = cache.constantcache
14331438
gen_prob = !(
1434-
(is_diagonal_noise(integrator.sol.prob)) || (W.dW isa Number) ||
1435-
(length(W.dW) == 1)
1439+
(is_diagonal_noise(integrator.sol.prob)) || (W.dW isa Number)
14361440
)
14371441

14381442
alg = unwrap_alg(integrator, true)
@@ -1459,7 +1463,9 @@ end
14591463
τ = mτ[deg_index]
14601464

14611465
sqrt_dt = sqrt(abs(dt))
1462-
(gen_prob) && (vec_χ .= 2 .* floor.(1 // 2 .+ false .* vec_χ .+ rand(length(vec_χ))) .- 1)
1466+
if gen_prob
1467+
init_χ!(vec_χ, W)
1468+
end
14631469

14641470
tᵢ₋₂ = t
14651471
@.. uᵢ₋₂ = uprev
@@ -1502,7 +1508,7 @@ end
15021508
tᵢ₋₁ += θₛ₋₃ * (tᵢ₋₁ - tᵢ₋₂)
15031509
tᵢ₋₂ = ttmp
15041510

1505-
if W.dW isa Number || length(W.dW) == 1 || is_diagonal_noise(integrator.sol.prob)
1511+
if W.dW isa Number || is_diagonal_noise(integrator.sol.prob)
15061512
# stage s-3
15071513
integrator.f(yₛ₋₃, uᵢ₋₁, p, tᵢ₋₁)
15081514
@.. utmp = uᵢ₋₁ + μₛ₋₃ * yₛ₋₃
@@ -1711,7 +1717,7 @@ end
17111717
uᵢ₋₂ = integrator.f(uᵢ₋₂, p, tᵢ₋₂)
17121718
u += dt *+ τ) * uᵢ₋₂
17131719

1714-
if (W.dW isa Number) || (length(W.dW) == 1) || is_diagonal_noise(integrator.sol.prob)
1720+
if (W.dW isa Number) || is_diagonal_noise(integrator.sol.prob)
17151721
Gₛ = integrator.f.g(uᵢ₋₁, p, tᵢ₋₁)
17161722
u += Gₛ .* W.dW
17171723

@@ -1806,7 +1812,7 @@ end
18061812
integrator.f(k, uᵢ₋₂, p, tᵢ₋₂)
18071813
@.. u += dt *+ τ) * k
18081814

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

@@ -1840,3 +1846,24 @@ end
18401846

18411847
integrator.u = u
18421848
end
1849+
1850+
# Fill `vec_χ` with independent Rademacher (±1) samples drawn from the RNG of the
1851+
# integrator's noise process. Drawn in bulk and mapped by broadcast rather than by a
1852+
# scalar loop, so the routine does not scalar-index `vec_χ` and stays usable for array
1853+
# types that forbid it.
1854+
function init_χ!(vec_χ, W)
1855+
Random.rand!(rng(W), vec_χ)
1856+
one_χ = one(eltype(vec_χ))
1857+
vec_χ .= ifelse.(vec_χ .< 1 // 2, -one_χ, one_χ)
1858+
return vec_χ
1859+
end
1860+
1861+
# `NoiseWrapper` does not carry its own `rng`; the generator lives on the wrapped
1862+
# source process, which may itself be a wrapper (#3188). Processes that replay a
1863+
# recorded path (`NoiseGrid`) have no generator at all, so fall back to the global
1864+
# one rather than erroring.
1865+
function rng(W)
1866+
hasfield(typeof(W), :rng) && return W.rng
1867+
hasfield(typeof(W), :source) && return rng(W.source)
1868+
return Random.default_rng()
1869+
end

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()

0 commit comments

Comments
 (0)