Open henry2004y opened 1 year ago
Another test involves tracing protons in a magnetic mirror:
```julia # Test for energy conservation between different schemes in a magnetic mirror. using TestParticle using TestParticle: getB_mirror using OrdinaryDiffEq using StaticArrays using TestParticleMakie using GLMakie using FieldTracer using Random using Statistics: mean using LinearAlgebra: × using RecursiveArrayTools function location!(dx, x, v, p::TestParticle.TPTuple, t) dx .= v end function lorentz!(dv, x, v, p::TestParticle.TPTuple, t) q, m, E, B = p dv .= q/m*(E(x, t) + v × (B(x, t))) end ## Obtain field # Magnetic bottle parameters in SI units const I1 = 1. # current in the solenoid const N1 = 1 # number of windings const distance = 10. # distance between solenoids const a = 2.5 # radius of each coil # Unfortunately there is no analytical differentiation to that special function! function getB(xu) SVector{3}(getB_mirror(xu[1], xu[2], xu[3], distance, a, I1*N1)) #SVector{3}(0.0, 0.0, 5.88e-8) end function getE(xu) SA[0.0, 0.0, 0.0] end function isoutofdomain(u, p, t) if abs(u[3]) > 6.0 return true else return false end end struct Trajectory{T<:AbstractFloat} x::Vector{T} y::Vector{T} z::Vector{T} vx::Vector{T} vy::Vector{T} vz::Vector{T} end "Uniform sampling on a sphere." function sample_unit_sphere(n::Int=1000, method::Symbol=:standard) x = zeros(n) y = zeros(n) z = zeros(n) if method == :spherical for i in 1:n theta = 2π * rand() phi = acos(1 - 2 * rand()) x[i] = sin(phi) * cos(theta) y[i] = sin(phi) * sin(theta) z[i] = cos(phi) end elseif method == :standard i = 0 while i < n x1, x2, x3 = rand(3) .* 2 .- 1 λ = hypot(x1, x2, x3) if λ ≤ 1 x[i+1] = x1 / λ y[i+1] = x2 / λ z[i+1] = x3 / λ i += 1 end end end x, y, z end function sample_equator(n::Int, rmin::Float64=0.1, rmax::Float64=1.0) x = zeros(n) y = zeros(n) i = 0 while i < n x1, x2 = (rand(2) .* 2 .- 1) .* rmax λ = hypot(x1, x2) if rmin ≤ λ ≤ rmax x[i+1] = x1 y[i+1] = x2 i += 1 end end z = zeros(n) x, y, z end function sample_particles(n::Int=10, energy::Float64=1e-7, rmin::Float64=0.0, rmax::Float64=0.1) x, y, z = sample_equator(n, rmin, rmax) vx, vy, vz = sample_unit_sphere(n) kB = 1.38064852e-23 T = energy * TestParticle.qᵢ / kB vth = √(2kB*T/TestParticle.mᵢ) vx .*= vth vy .*= vth vz .*= vth x, y, z, vx, vy, vz end function trace_particle(; species::TestParticle.Species=Proton, tspan=(0.0, 1.0), trajectories::Int=1, saveat::Float64=0.0) #stateinit = SA[0.0, 0.0, 0.0, 0.0, 0.0, 0.0] stateinit = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] param = prepare(getE, getB; species) #prob = ODEProblem(TestParticle.trace, stateinit, tspan, param) prob = DynamicalODEProblem(location!, lorentz!, stateinit[1:3], stateinit[4:6], tspan, param) sols = Vector{Trajectory}(undef, trajectories) particles = sample_particles(trajectories) for i in 1:trajectories #u0 = SVector{6}([p[i] for p in particles]) #prob = remake(prob; u0) u0 = [p[i] for p in particles] prob = remake(prob; u0=ArrayPartition(u0[1:3], u0[4:6])) if saveat == 0.0 #sol = solve(prob, Tsit5(); save_idxs=[1,2,3,4,5,6], isoutofdomain, verbose=false) #sol = solve(prob, KahanLi6(); dt=0.1, save_idxs=[1,2,3,4,5,6], isoutofdomain, verbose=false) #sol = solve(prob, Trapezoid(autodiff=false); save_idxs=[1,2,3,4,5,6], isoutofdomain, verbose=false) #sol = solve(prob, Vern6(); save_idxs=[1,2,3,4,5,6], isoutofdomain, verbose=true) sol = solve(prob, Vern9(); save_idxs=[1,2,3,4,5,6], isoutofdomain, verbose=true) else sol = solve(prob, Tsit5(); save_idxs=[1,2,3,4,5,6], isoutofdomain, saveat, verbose=false) end x = getindex.(sol.u, 1) y = getindex.(sol.u, 2) z = getindex.(sol.u, 3) vx = getindex.(sol.u, 4) vy = getindex.(sol.u, 5) vz = getindex.(sol.u, 6) sols[i] = Trajectory(x, y, z, vx, vy, vz) end sols end ##### xrange = range(-2, 2, length=20) yrange = range(-2, 2, length=20) zrange = range(-5, 5, length=20) Bx, By, Bz = let x=xrange, y=yrange, z=zrange Bx = zeros(length(x), length(y), length(z)) By = zeros(length(x), length(y), length(z)) Bz = zeros(length(x), length(y), length(z)) for k in eachindex(z), j in eachindex(y), i in eachindex(x) Bx[i,j,k], By[i,j,k], Bz[i,j,k] = getB([x[i], y[j], z[k]]) end Bx, By, Bz end f = Figure() ax = Axis3(f[1, 1]; aspect = :data ) for i in 0:8 if i == 0 xs, ys, zs = 0.0, 0.0, 0.0 else xs, ys, zs = 1.5*cos(2π*(i-1)/8), 1.5*sin(2π*(i-1)/8), 0.0 end x1, y1, z1 = FieldTracer.trace(Bx, By, Bz, xs, ys, zs, xrange, yrange, zrange; ds=0.1, maxstep=1000) lines!(ax, x1, y1, z1, color=:black) end f tspan = (0.0, 1000.) trajectories = 1 Random.seed!(1234) sols = trace_particle(; species=Proton, tspan, trajectories) KE1 = let vx = [s.vx[1] for s in sols] vy = [s.vy[1] for s in sols] vz = [s.vz[1] for s in sols] KE1 = mean(@. √(vx^2 + vy^2 + vz^2)) end KE2 = let vx = [s.vx[end] for s in sols] vy = [s.vy[end] for s in sols] vz = [s.vz[end] for s in sols] KE2 = mean(@. √(vx^2 + vy^2 + vz^2)) end println("Number of particles: ", trajectories) # energy conservation println("Avg energy: ", KE1, " to ", KE2) println("Change ratio: ", abs(KE2-KE1)/KE1) for s in sols lines!(ax, s.x, s.y, s.z) end f ```
I use DynamicalODEProblem
for all to include the test for symplectic integrators like KahanLi6
. The parameters chosen here correspond to a gyroperiod of approximately 1 s, so in a total simulation time of 1000 s each particle gyrates for about 1000 rounds.
Scheme | Tsit5 | Tsit5(dtmax=0.1) | Trapezoid | Vern6 | Vern9 | KahanLi6(dt=0.1) | SERK2 | ESERK5 |
---|---|---|---|---|---|---|---|---|
Initial energy | 4.375 | 4.375 | 4.375 | 4.375 | 4.375 | 4.375 | 4.375 | 4.375 |
Final energy | 3.743 | 4.366 | 4.006 | 4.334 | 4.403 | 7.300 | 4.400 | 5.677 |
change ratio | 14.5% | 0.2% | 8.4% | 0.9% | 0.6% | 66.8% | 0.5% | 29.7% |
The numbers in the table above have been updated after merging #76
A side finding from performing these tests is that auto-differentiation may fail for complicated functions like ellipke
that is used when generating the magnetic field from a coil. In this situation there are three options:
autodiff=false
.Sorry, I am very interested in this issue, and the results you have presented here seem worthy of further exploration. But recently I need to write a very important article. Therefore, it may take one to two weeks before I can conduct further testing on this topic. Before that, we may not be able to have an in-depth discussion. Please forgive me.
No worry, we are not in a rush @TCLiuu!
Now I have some better understandings of the issue. For some algorithms like Tsit5
and Vern6
, much better energy conservation can be achieved if we set dtmax=T/15
, which means the largest time step for an adaptive scheme should not exceed 1/15 of a gyroperiod. However, for many if not all fixed timestep methods, setting the time step to T/15 is still not enough to conserve energy to a decent level. So far, Tsit5
, Vern6/7/8/9
, and SERK2
are among the best options together with dtmax
; if no dtmax
is provided, SERK2
is still decent.
The gyroperiod should be estimated from the smallest value, i.e. largest B field area. These are inspired from the reply of Lubos here.
When we wish to generalize the above restriction, we need to modify the timesteps. This can be achieved by the DiscreteCallback methods provided in SciMLBase, or indirectly via the DiffEqCallbacks.jl
pkg:
using DiffEqCallbacks
# p = (q, m, E, B)
dtFE(u, p, t) = @views p[2] / (2π * p[1] * hypot(p[4](u, t)...))
cb = StepsizeLimiter(dtFE; safety_factor=1 // 15, max_step=true)
sol = solve(prob, Tsit5(); callback=cb, adaptive=false, dt=0.1, isoutofdomain)
This should be included in TestParticle.jl, at least partly.
Now the most important remaining question is: why do all symplectic integrators for the DynamicalODEProblem
not working properly?
The classical way of performing an energy conserved PIC simulation is to adopt the Boris method (see here and here). Mathmatically, this belongs to the family of symplectic integrators, where energy is conserved.
In OrdinaryDiffEq.jl, most of the symplectic integrators are defined only for DynamicalODEs, as can be found in this page. These integrators require the definition of
DynamicalODEProblem
only, so we need to split the ODE equation for test particle into two parts. In the general integrator list, only a few symplectic choices are listed:ImplicitMidpoint()
,Trapezoid()
, andSSPSDIRK2()
. As explained by Chris in this thread, most symplectic integrators require a fixed timestep. We cannot use the symplectic integrators designed forDynamicalODEProblem
for the generalODEProblem
.Let us test this using a single proton in a uniform magnetic field without the electric field:
Static B field
```julia # Tracing charged particle in a static EM field. # TestParticle.jl v0.8.2 using TestParticle using OrdinaryDiffEq using StaticArrays using LinearAlgebra: × const B₀ = 1e-8 # [T] "f2" function location!(dx, v, x, p::TestParticle.TPTuple, t) dx .= v end "f1" function lorentz!(dv, v, x, p::TestParticle.TPTuple, t) q2m, E, B = p dv .= q2m*(E(x, t) + v × (B(x, t))) end ## Initialize field function uniform_B(x) return SA[0, 0, B₀] end function uniform_E(x) return SA[0.0, 0.0, 0.0] end "Check energy conservation." E(dx, dy, dz) = 1 // 2 * (dx^2 + dy^2 + dz^2) ## Initialize particles x0 = [0.0, 0, 0] v0 = [0.0, 1e2, 0.0] stateinit = [x0..., v0...] tspan_proton = (0.0, 2000.0) param_proton = prepare(uniform_E, uniform_B, species=Proton) ## Solve for the trajectories prob_p = ODEProblem(trace!, stateinit, tspan_proton, param_proton) #prob_p = DynamicalODEProblem(lorentz!, location!, v0, x0, tspan_proton, param_proton) Ωᵢ = TestParticle.qᵢ * B₀ / TestParticle.mᵢ Tᵢ = 2π / Ωᵢ println("Number of gyrations: ", tspan_proton[2] / Tᵢ) function main(prob_p, Tᵢ) schemes = [ (ImplicitMidpoint(), true), (SSPSDIRK2(), true), (RosenbrockW6S4OS(), true), (Vern9(), false), (Vern8(), false), (Trapezoid(), false), (Vern6(), false), (Tsit5(), false), (BS3(), false), #(Nystrom4(), true), # only can be used for DynamicalODEProblem ] for i in eachindex(schemes) @info schemes[i][1] if schemes[i][2] # fixed timestep @time sol = solve(prob_p, schemes[i][1], dt = Tᵢ/15) else @time sol = solve(prob_p, schemes[i][1]) end #energy = map(x -> E(x[1:3]...), sol.u) # for DynamicalODEProblem defined above energy = map(x -> E(x[4:6]...), sol.u) # for ODEProblem energy_diff = (energy[end] - energy[1]) / energy[1] println("Energy diff ratio: ", round(energy_diff, digits=4)) end @info "Boris:" prob = TraceProblem(stateinit, tspan_proton, Tᵢ/15, prob_p.p) @time sol = TestParticle.solve(prob; savestepinterval=10); energy_diff = (E(sol[1].u[4:6,end]...) - E(sol[1].u[4:6,1]...)) / E(sol[1].u[4:6,1]...) println("Energy diff ratio: ", round(energy_diff, digits=4)) end main(prob_p, Tᵢ) println("-------------------------------------------------------") main(prob_p, Tᵢ) ```
First time execution:
Second time:
dt = Tᵢ/15
:KahanLi8(), DPRKN6(), DPRKN12(), IRKN4(), VelocityVerlet(), McAte5(), McAte2(), SymplecticEuler(), VerletLeapfrog(), Ruth3()
, etc. Based on the description in here, myf1
does not satisfy the independency condition.Vern9
has much longer compilation time than the others, probably due to the lack of precompilation. However, it is actually pretty fast.x
andv
.Vern8
orVern9
is probably the best here. This is also consistent with the SciML Benchmarks for nonstiff problems.@TCLiuu