Simple simulation

Is there an easy way to speed this up?

sims    = 10_000
T       = 1
σ       = .1
Δ       = .000001
σrΔ     = σ√Δ
steps   = (Int64 ∘ round)(T/Δ)
L       = .9
H       = 1.1
x       = ones(  sims )
alive   = trues( sims )

for t ∈ 1:steps
    x .+= randn(sims)σrΔ 
    @. alive &= L < x < H
end

mean(alive)

around 2x faster

julia> function main()
       sims    = 10_000
       T       = 1
       σ       = .1
       Δ       = .000001
       σrΔ     = σ√Δ
       steps   = (Int64 ∘ round)(T/Δ)
       L       = .9
       H       = 1.1
       x       = ones(  sims )
       alive   = trues( sims )
       cache = similar(x)
       for t ∈ 1:steps
           x .+= Random.randn!(cache) .* σrΔ
           @. alive &= L < x < H
       end

       mean(alive)end
main (generic function with 1 method)

julia> @time main()
 18.912667 seconds (9 allocations: 157.811 KiB)
0.3624

you could also try to switch random generator to check.

1 Like

(post deleted by author)

How fast you can go depends on what you want to record: if you only care about mean(alive), you can get very fast (if I didn’t make a mistake…).

sim_loop simply uses a loop instead of a vector of normals. You can then strip this further by using some problem-specific insights if you only care about mean(alive): 1) you can stop path-wise as soon as alive is false, 2) you don’t need to record alive as a vector, 3) instead of multiplying by a Float each time, you can also divide L and H by a Float once, which I would expect to be faster (but isn’t?)

using Random, BenchmarkTools
function sim_opt(sims::Int)
    T       = 1.0
    σ       = 0.1
    Δ       = 0.000001
    σrΔ     = σ * √Δ
    steps   = round(Int64, T / Δ)
    L       = 0.9
    H       = 1.1

    x       = ones(sims)
    alive   = trues(sims)
    rnd     = Vector{Float64}(undef, sims)

    for t ∈ 1:steps
        randn!(rnd)
        @. x += rnd * σrΔ
        @. alive &= (L < x < H)
    end
    
    mean(alive)
end    

function sim_loop(sims::Int)
    T       = 1.0
    σ       = 0.1
    Δ       = 0.000001
    σrΔ     = σ * √Δ
    steps   = round(Int64, T / Δ)
    L       = 0.9
    H       = 1.1

    x       = ones(sims)
    alive   = trues(sims)

    Threads.@threads for i in 1:sims
        for _ in 1:steps
            x[i] += randn() * σrΔ
            alive[i] &= (L < x[i] < H)
        end
    end
    
    mean(alive)
end    

function sim_loop_only_alive(sims::Int)
    T       = 1.0
    σ       = 0.1
    Δ       = 0.000001
    σrΔ     = σ * √Δ
    steps   = round(Int64, T / Δ)
    L       = 0.9
    H       = 1.1

    x       = ones(sims)
    alive   = trues(sims)

    Threads.@threads for i in 1:sims
        for _ in 1:steps
            x[i] += randn() * σrΔ
            if !(L < x[i] < H)
                alive[i] = false
                break
            end
        end
    end
    
    mean(alive)
end    

function sim_loop_only_alive_no_vecs(sims::Int)
    T       = 1.0
    σ       = 0.1
    Δ       = 0.000001
    σrΔ     = σ * √Δ
    steps   = round(Int64, T / Δ)
    L       = 0.9
    H       = 1.1

    x₀       = 1.
    n_alive = sims

    Threads.@threads for i in 1:sims
        x = x₀
        for _ in 1:steps
            x += randn() * σrΔ
            if !(L < x < H)
                n_alive -= 1
                break
            end
        end
    end
    
    n_alive / sims
end  

function sim_loop_only_alive_no_vecs_no_mul(sims::Int)
    T       = 1.0
    σ       = 0.1
    Δ       = 0.000001
    σrΔ     = σ * √Δ
    steps   = round(Int64, T / Δ)
    L_sc    = 0.9 / σrΔ
    H_sc    = 1.1 / σrΔ

    x₀       = 1. / σrΔ
    n_alive = sims

    Threads.@threads for i in 1:sims
        x = x₀
        for _ in 1:steps
            x += randn()
            if !(L_sc < x < H_sc)
                n_alive -= 1
                break
            end
        end
    end
    
    n_alive / sims
end  


function tests(N)
    @time sim_opt(N);
    @time sim_loop(N);
    @time sim_loop_only_alive(N);
    @time sim_loop_only_alive_no_vecs(N);
    @time sim_loop_only_alive_no_vecs_no_mul(N);
end

tests(1000)

gives

  3.063867 seconds (1.00 M allocations: 4.053 GiB, 6.50% gc time)
  1.090280 seconds (16.19 k allocations: 1.043 MiB, 6.25% compilation time)
  0.722454 seconds (14.61 k allocations: 995.672 KiB, 10.36% compilation time)
  0.699596 seconds (13.65 k allocations: 907.141 KiB, 10.81% compilation time)
  0.731058 seconds (13.49 k allocations: 896.547 KiB, 10.69% compilation time)
0.38
3 Likes

Note that if you only care about alive, a loop-based version will get relatively faster when you add more and more sims: a vectorized approach requires all calculations to be done for all paths, also for the ones that are no longer alive.

much appreciated.
could @turbo be used? or does the sequential calculation make it ineffective

When dealing with unreasonably slow code, it’s best to consider the algorithm. The example takes 24 seconds on my well provisioned Mac.

For a Brownian motion with constant drift=0, σ, and absorbing barriers at L and H starting from x₀=1 (the midpoint), there are closed-form survival probabilities via eigenfunction expansion. That would replace the entire simulation. Your Δ=1e-6 is very small, likely to avoid missing barrier crossings between discrete steps. Brownian bridge correction lets you use much larger Δ (e.g., 1e-3) while still accounting for intra-step barrier hits, reducing steps from 1M to 1K:

function barrier_sim_bb(; sims=10_000, T=1, σ=0.1, Δ=1e-3, L=0.9, H=1.1)
    σrΔ   = σ * √Δ
    steps = round(Int, T / Δ)
    
    x     = ones(sims)
    alive = trues(sims)
    
    for t ∈ 1:steps
        @inbounds for i ∈ 1:sims
            alive[i] || continue
            x_old = x[i]
            x[i] += randn() * σrΔ
            x_new = x[i]
            
            # direct exit
            if !(L < x_new < H)
                alive[i] = false
                continue
            end
            
            # Brownian bridge: probability of crossing barrier between steps
            p_low  = exp(-2(x_old - L) * (x_new - L) / (σ^2 * Δ))
            p_high = exp(-2(H - x_old) * (H - x_new) / (σ^2 * Δ))
            if rand() < p_low + p_high
                alive[i] = false
            end
        end
    end
    
    mean(alive)
end

@time barrier_sim_bb(sims=10_000, T=1, σ=0.1, Δ=1e-3, L=0.9, H=1.1)

quite a bit faster


julia> @time barrier_sim_bb(sims=10_000, T=1, σ=0.1, Δ=1e-3, L=0.9, H=1.1)
  0.093072 seconds (5 allocations: 81.422 KiB)
0.3762
1 Like

I was thinking the same but figured that OP might have more complicated dynamics in mind in the long run. If not, directly simulating hitting times of fixed barriers is also an approach: first simulate the first hitting time of either barrier; then simulate the state x at the first of {hitting time, T} using a conditional distribution. If you also need the terminal state and hitting occurred before time T, simulate x_T using one normal random variate.

1 Like

thank you.
I’ll combine this with JADekker’s approach.

Combining, I get

function barrier_sim_bb(sims=10_000; T=1, σ=0.1, Δ=1e-3, L=0.9, H=1.1)
    σrΔ   = σ * √Δ
    steps = round(Int, T / Δ)
    
    x     = ones(sims)
    alive = trues(sims)
    
    for t ∈ 1:steps
        @inbounds for i ∈ 1:sims
            alive[i] || continue
            x_old = x[i]
            x[i] += randn() * σrΔ
            x_new = x[i]
            
            # direct exit
            if !(L < x_new < H)
                alive[i] = false
                continue
            end
            
            # Brownian bridge: probability of crossing barrier between steps
            p_low  = exp(-2(x_old - L) * (x_new - L) / (σ^2 * Δ))
            p_high = exp(-2(H - x_old) * (H - x_new) / (σ^2 * Δ))
            if rand() < p_low + p_high
                alive[i] = false
            end
        end
    end
    
    mean(alive)
end

function barrier_sim_bb_quick(sims=10_000; T=1, σ=0.1, Δ=1e-3, L=0.9, H=1.1)
    σrΔ   = σ * √Δ
    steps = round(Int, T / Δ)
    
    x     = 1.
    n_alive = sims
    
    fr_inv = 2 / (σ^2 * Δ)
    for i ∈ 1:sims
        x_old = x
        x_new = x_old
        for t ∈ 1:steps
            x_old = x_new
            x_new += randn() * σrΔ
            
            # direct exit
            if !(L < x_new < H)
                n_alive -= 1
                break
            end
            
            # Brownian bridge: probability of crossing barrier between steps
            p_low  = exp(-(x_old - L) * (x_new - L) * fr_inv)
            p_high = exp(-(H - x_old) * (H - x_new) * fr_inv)
            if rand() < p_low + p_high
                n_alive -= 1
                break
            end
        end
    end
    
    n_alive / sims
end

gives with 1000 simulations

  0.010199 seconds (3 allocations: 8.219 KiB) # barrier_sim_bb
  0.006918 seconds # barrier_sim_bb_quick

and 10.000 simulations

  0.126836 seconds (4 allocations: 79.531 KiB)
  0.070636 seconds
2 Likes