|
| 1 | +using StochasticDiffEq, DiffEqNoiseProcess, SparseArrays, LinearAlgebra, |
| 2 | + AllocCheck |
| 3 | +using DiffEqBase: @.. |
| 4 | + |
| 5 | +@testset "EulerHeun sparse noise: no per-step alloc" begin |
| 6 | + |
| 7 | + # Simple linear drift |
| 8 | + f!(du, u, p, t) = (@.. du = 0.999 * u) |
| 9 | + |
| 10 | + # 2×2 identical-column block structure; g! only writes nzval of an existing sparsity pattern |
| 11 | + function sparse_proto(N) |
| 12 | + I = Vector{Int}(undef, 4N) |
| 13 | + J = similar(I) |
| 14 | + V = ones(Float64, 4N) |
| 15 | + @inbounds for i in 1:N |
| 16 | + I[4i - 3] = 2i - 1 |
| 17 | + J[4i - 3] = 2i - 1 |
| 18 | + |
| 19 | + I[4i - 2] = 2i |
| 20 | + J[4i - 2] = 2i - 1 |
| 21 | + |
| 22 | + I[4i - 1] = 2i - 1 |
| 23 | + J[4i - 1] = 2i |
| 24 | + |
| 25 | + I[4i] = 2i |
| 26 | + J[4i] = 2i |
| 27 | + end |
| 28 | + sparse(I, J, V, 2N, 2N) |
| 29 | + end |
| 30 | + |
| 31 | + @inline function ensure_pattern!(G::SparseMatrixCSC{T, Int}, N) where {T} |
| 32 | + s = one(T) |
| 33 | + @inbounds for i in 1:N |
| 34 | + G[2i - 1, 2i - 1] = s |
| 35 | + G[2i, 2i - 1] = s |
| 36 | + G[2i - 1, 2i] = s |
| 37 | + G[2i, 2i] = s |
| 38 | + end |
| 39 | + return nothing |
| 40 | + end |
| 41 | + |
| 42 | + # Dense g! |
| 43 | + function g!(G::StridedMatrix{T}, u, p, t) where {T} |
| 44 | + c012 = T(0.12) |
| 45 | + c18 = T(1.8) |
| 46 | + @inbounds for i in 1:(p.N) |
| 47 | + off = 2i - 1 |
| 48 | + G[off, off] = c012 * u[2i - 1] |
| 49 | + G[off + 1, off] = c18 * u[2i] |
| 50 | + |
| 51 | + G[off, off + 1] = c012 * u[2i - 1] |
| 52 | + G[off + 1, off + 1] = c18 * u[2i] |
| 53 | + end |
| 54 | + return nothing |
| 55 | + end |
| 56 | + |
| 57 | + # Sparse g! |
| 58 | + function g!(G::SparseMatrixCSC{T}, u, p, t) where {T} |
| 59 | + c012 = T(0.12) |
| 60 | + c18 = T(1.8) |
| 61 | + @inbounds for i in 1:(p.N) |
| 62 | + off = G.colptr[2i - 1] |
| 63 | + G.nzval[off] = c012 * u[2i - 1] |
| 64 | + G.nzval[off + 1] = c18 * u[2i] |
| 65 | + |
| 66 | + off = G.colptr[2i] |
| 67 | + G.nzval[off] = c012 * u[2i - 1] |
| 68 | + G.nzval[off + 1] = c18 * u[2i] |
| 69 | + end |
| 70 | + return nothing |
| 71 | + end |
| 72 | + |
| 73 | + function make_integrator_sparse(N) |
| 74 | + A = sparse_proto(N) |
| 75 | + p = (; N) |
| 76 | + W = SimpleWienerProcess!(0.0, zeros(2N); save_everystep = false) |
| 77 | + prob = SDEProblem( |
| 78 | + f!, g!, ones(2N), (0.0, 1.0), p; noise_rate_prototype = A, noise = W) |
| 79 | + integ = init(prob, EulerHeun(); dt = 0.01, adaptive = false, save_on = false) |
| 80 | + |
| 81 | + cache = integ.cache |
| 82 | + allocs = AllocCheck.check_allocs( |
| 83 | + StochasticDiffEq.perform_step!, (typeof(integ), typeof(cache)) |
| 84 | + ) |
| 85 | + @test isempty(allocs) |
| 86 | + end |
| 87 | + |
| 88 | + function make_integrator_dense(N) |
| 89 | + A = zeros(2N, 2N) |
| 90 | + p = (; N) |
| 91 | + W = SimpleWienerProcess!(0.0, zeros(2N); save_everystep = false) |
| 92 | + prob = SDEProblem( |
| 93 | + f!, g!, ones(2N), (0.0, 1.0), p; noise_rate_prototype = A, noise = W) |
| 94 | + integ = init(prob, EulerHeun(); dt = 0.01, adaptive = false, save_on = false) |
| 95 | + |
| 96 | + cache = integ.cache |
| 97 | + |
| 98 | + # Dense+BLAS: assert with `@allocated == 0` (not `check_allocs`). |
| 99 | + # `check_allocs` flags throw-only branches in LinearAlgebra’s generic gemv!/mul!, |
| 100 | + # while the BLAS hot path is allocation-free at runtime. We keep `check_allocs` |
| 101 | + # for the sparse/non-diagonal tests. |
| 102 | + StochasticDiffEq.perform_step!(integ, cache) # warm-up |
| 103 | + @test @allocated(StochasticDiffEq.perform_step!(integ, cache)) == 0 |
| 104 | + end |
| 105 | + |
| 106 | + make_integrator_dense(16) |
| 107 | + make_integrator_sparse(16) |
| 108 | +end |
0 commit comments