Repository: henry2004y/TestParticle.jl
Pull request #236: refactor: unify `prepare` and `trace` interface
Author: login: Beforerr
Created: 2025-05-19T03:00:09Z | Merged: 2025-05-20T00:40:00Z

Julia compiler is sooooo smart that when `F` is `ZeroField()` the typed codes is exactly the same with previous `TPTuple` codes so now unification comes with no runtime cost.

Now `prepare(args...; kwargs...)` always return `(q2m, m, E, B, F)`


Diff:
diff --git a/docs/examples/advanced/demo_fermi_foreshock.jl b/docs/examples/advanced/demo_fermi_foreshock.jl
index 2ea5a84c7..f9a65afc9 100644
--- a/docs/examples/advanced/demo_fermi_foreshock.jl
+++ b/docs/examples/advanced/demo_fermi_foreshock.jl
@@ -32,6 +32,7 @@ import DisplayAs #hide
 using TestParticle, OrdinaryDiffEqVerner, StaticArrays
 import TestParticle as TP
 using TestParticle: mₑ, Rₑ, qₑ
+using TestParticle: get_EField, get_BField
 using Random
 using FHist
 using CairoMakie, Printf
@@ -122,9 +123,11 @@ function plot_multiple(sol)
    energy = map(x -> get_kinetic_energy(x[4:6]...), sol.u) .* mₑ ./ abs(qₑ)
    ## Obtain the EM fields along the particle trajectory
    ## [mV/m]
-   E = [sol.prob.p[2](sol[:,istep], sol.t[istep]).*1e3 for istep in eachindex(sol)]
+   Efield = get_EField(sol)
+   Bfield = get_BField(sol)
+   E = [Efield(sol[:,istep], sol.t[istep]).*1e3 for istep in eachindex(sol)]
    ## [nT]
-   B = [sol.prob.p[3](sol[:,istep], sol.t[istep]).*1e9 for istep in eachindex(sol)]
+   B = [Bfield(sol[:,istep], sol.t[istep]).*1e9 for istep in eachindex(sol)]
 
    Ex = [e[1] for e in E]
    Ey = [e[2] for e in E]
diff --git a/docs/examples/advanced/demo_output_func.jl b/docs/examples/advanced/demo_output_func.jl
index d934fc2a1..07b9890e5 100644
--- a/docs/examples/advanced/demo_output_func.jl
+++ b/docs/examples/advanced/demo_output_func.jl
@@ -17,6 +17,7 @@
 
 import DisplayAs #hide
 using TestParticle, OrdinaryDiffEqVerner, StaticArrays
+using TestParticle: get_BField
 using Statistics
 using LinearAlgebra: normalize, ×, ⋅
 using Random
@@ -28,7 +29,7 @@ Random.seed!(seed)
 
 "Set initial state for EnsembleProblem."
 function prob_func(prob, i, repeat)
-   B0 = prob.p[3](prob.u0)
+   B0 = get_BField(prob)(prob.u0)
    B0 = normalize(B0)
 
    Bperp1 = SA[0.0, -B0[3], B0[2]] |> normalize
@@ -102,7 +103,7 @@ prob = ODEProblem(trace_normalized!, stateinit, tspan, param)
 
 "Set customized outputs for the ensemble problem."
 function output_func(sol, i)
-   getB = sol.prob.p[3]
+   getB = get_BField(sol)
    b = getB.(sol.u)
 
    μ = [@views b[i] ⋅ sol[4:6, i] / sqrt(sum(x -> x^2, b[i])) for i in eachindex(sol)]   
diff --git a/docs/examples/advanced/demo_savingcallback.jl b/docs/examples/advanced/demo_savingcallback.jl
index dc8ce4816..d047922d6 100644
--- a/docs/examples/advanced/demo_savingcallback.jl
+++ b/docs/examples/advanced/demo_savingcallback.jl
@@ -22,6 +22,7 @@
 
 using TestParticle, OrdinaryDiffEqVerner, StaticArrays
 using TestParticle: qᵢ, mᵢ
+using TestParticle: get_BField
 using Statistics
 using LinearAlgebra: normalize, ×, ⋅
 using DiffEqCallbacks
@@ -75,7 +76,7 @@ stateinit = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
 prob = ODEProblem(trace_normalized!, stateinit, tspan, param)
 
 prob.u0[4:6] = let
-   B0 = prob.p[3](prob.u0)
+   B0 = get_BField(prob)(prob.u0)
    B0 = normalize(B0)
 
    Bperp1 = SA[0.0, -B0[3], B0[2]] |> normalize
@@ -93,7 +94,7 @@ end
 saved_values = SavedValues(Float64, Tuple{SVector{3, Float64}, Float64})
 
 function save_B_mu(u, t, integrator)
-   b = integrator.p[3](u)
+   b = get_BField(integrator.p)(u)
    μ = @views b ⋅ u[4:6] / √(b[1]^2 + b[2]^2 + b[3]^2)
 
    b, μ
diff --git a/docs/examples/basics/demo_Buniform_Ezero.jl b/docs/examples/basics/demo_Buniform_Ezero.jl
index 54089192c..6bccfd51a 100644
--- a/docs/examples/basics/demo_Buniform_Ezero.jl
+++ b/docs/examples/basics/demo_Buniform_Ezero.jl
@@ -12,11 +12,12 @@
 
 import DisplayAs #hide
 using TestParticle, OrdinaryDiffEqVerner, StaticArrays
+using TestParticle: ZeroField
 using CairoMakie
 CairoMakie.activate!(type = "png") #hide
 
 uniform_B(x) = SA[0.0, 0.0, 1e-8]
-uniform_E(x) = SA[0.0, 0.0, 0.0]
+zero_E = ZeroField()
 
 ## Initial condition
 stateinit = let x0 = [1.0, 0, 0], v0 = [0.0, 1.0, 0.1]
@@ -25,7 +26,7 @@ end
 ## Time span
 tspan = (0, 18)
 
-param = prepare(uniform_E, uniform_B, species=Proton)
+param = prepare(zero_E, uniform_B, species=Proton)
 prob = ODEProblem(trace!, stateinit, tspan, param)
 sol = solve(prob, Vern9())
 
diff --git a/docs/examples/basics/demo_ExB_drift.jl b/docs/examples/basics/demo_ExB_drift.jl
index afef7e1bf..877418b7f 100644
--- a/docs/examples/basics/demo_ExB_drift.jl
+++ b/docs/examples/basics/demo_ExB_drift.jl
@@ -15,6 +15,7 @@
 import DisplayAs #hide
 using TestParticle, OrdinaryDiffEqVerner, StaticArrays
 using LinearAlgebra: ⋅, ×, normalize
+using TestParticle: get_EField, get_BField
 using CairoMakie
 CairoMakie.activate!(type = "png") #hide
 
@@ -24,13 +25,15 @@ uniform_E(x) = SA[1e-9, 0, 0]
 
 ## Trace the orbit of the guiding center
 function trace_gc_ExB!(dx, x, p, t)
-   _, E, B, sol = p
+   sol = p[end]
+   Bfunc = get_BField(p)
+   Efunc = get_EField(p)
    xu = sol(t)
-   Bv = B(x)
+   Bv = Bfunc(x)
    b = normalize(Bv)
    v_par = @views (xu[4:6] ⋅ b) .* b
    B2 = sum(Bv.^2)
-   dx[1:3] = (E(x) × Bv) / B2 + v_par
+   dx[1:3] = (Efunc(x) × Bv) / B2 + v_par
 end
 ## Initial condition
 stateinit = let x0 = [1.0, 0.0, 0.0], v0 = [0.0, 1.0, 0.1]
diff --git a/docs/examples/basics/demo_FLR.jl b/docs/examples/basics/demo_FLR.jl
index 774cdce56..a0fc1f002 100644
--- a/docs/examples/basics/demo_FLR.jl
+++ b/docs/examples/basics/demo_FLR.jl
@@ -29,7 +29,7 @@ end
 
 ## trace the orbit of the guiding center
 function trace_gc!(dx, x, p, t)
-    q2m, E, B, sol = p
+    q2m, _, E, B, _, sol = p
     xu = sol(t)
     xp = @view xu[1:3]
     Bv = B(xp)
diff --git a/docs/examples/basics/demo_boris.jl b/docs/examples/basics/demo_boris.jl
index 2d3fbf23a..4c7974cc3 100644
--- a/docs/examples/basics/demo_boris.jl
+++ b/docs/examples/basics/demo_boris.jl
@@ -13,6 +13,7 @@
 
 import DisplayAs #hide
 using TestParticle, OrdinaryDiffEqTsit5, StaticArrays
+using TestParticle: ZeroField, get_BField
 import TestParticle as TP
 using CairoMakie
 CairoMakie.activate!(type = "png") #hide
@@ -38,16 +39,16 @@ end
 
 const Bmag = 0.01
 uniform_B(x) = SA[0.0, 0.0, Bmag]
-uniform_E(x) = SA[0.0, 0.0, 0.0]
+zero_E = ZeroField()
 
 x0 = [0.0, 0.0, 0.0]
 v0 = [0.0, 1e5, 0.0]
 stateinit = [x0..., v0...]
 
-param = prepare(uniform_E, uniform_B, species=Electron)
+param = prepare(zero_E, uniform_B, species=Electron)
 
 ## Reference parameters
-const tperiod = 2π / (abs(param[1]) * sqrt(sum(x -> x^2, param[3]([0.0,0.0,0.0], 0.0))))
+const tperiod = 2π / (abs(param[1]) * sqrt(sum(x -> x^2, get_BField(param)([0.0,0.0,0.0], 0.0))))
 const rL = sqrt(v0[1]^2 + v0[2]^2 + v0[3]^2) / (abs(param[1]) * Bmag)
 const invrL = 1 / rL;
 
diff --git a/docs/examples/basics/demo_energy_conservation.jl b/docs/examples/basics/demo_energy_conservation.jl
index b9dcd6118..9324e200c 100644
--- a/docs/examples/basics/demo_energy_conservation.jl
+++ b/docs/examples/basics/demo_energy_conservation.jl
@@ -11,6 +11,7 @@
 
 import DisplayAs #hide
 using TestParticle, OrdinaryDiffEq, StaticArrays
+using TestParticle: ZeroField
 import TestParticle as TP
 using LinearAlgebra: ×
 using CairoMakie
@@ -20,13 +21,13 @@ const B₀ = 1e-8 # [T]
 const E₀ = 3e-2 # [V/m]
 
 "f2"
-function location!(dx, v, x, p::TP.TPTuple, t)
+function location!(dx, v, x, p, t)
    dx .= v
 end
 
 "f1"
-function lorentz!(dv, v, x, p::TP.TPTuple, t)
-   q2m, E, B = p
+function lorentz!(dv, v, x, p, t)
+   q2m, _, E, B = p
    dv .= q2m*(E(x, t) + v × (B(x, t)))
 end
 
@@ -40,13 +41,8 @@ function uniform_E(x)
    return SA[E₀, 0.0, 0.0]
 end
 
-function zero_B(x)
-   return SA[0.0, 0.0, 0.0]
-end
-
-function zero_E(x)
-   return SA[0.0, 0.0, 0.0]
-end
+zero_B = ZeroField()
+zero_E = ZeroField()
 
 "Check energy conservation."
 E(dx, dy, dz) = 1 // 2 * (dx^2 + dy^2 + dz^2)
diff --git a/docs/examples/basics/demo_gradient_B.jl b/docs/examples/basics/demo_gradient_B.jl
index 2196770a4..745083261 100644
--- a/docs/examples/basics/demo_gradient_B.jl
+++ b/docs/examples/basics/demo_gradient_B.jl
@@ -31,7 +31,7 @@ abs_B(x) = norm(grad_B(x))
 
 ## Trace the orbit of the guiding center using analytical drifts
 function trace_gc!(dx, x, p, t)
-    q2m, E, B, sol = p
+    q2m, _, E, B, _, sol = p
     xu = sol(t)
     gradient_B = gradient(abs_B, x)
     Bv = B(x)
diff --git a/src/equations.jl b/src/equations.jl
index 2d6dc4f9c..3bc7f7375 100644
--- a/src/equations.jl
+++ b/src/equations.jl
@@ -1,187 +1,125 @@
 # Tracing equations.
 
-"""
-	 trace!(dy, y, p::TPTuple, t)
-	 trace!(dy, y, p::FullTPTuple, t)
-
-ODE equations for charged particle moving in static EM field with in-place form.
+get_x(u) = @inbounds SA[u[1], u[2], u[3]]
+get_v(u) = @inbounds SA[u[4], u[5], u[6]]
 
-ODE equations for charged particle moving in static EM field and external force field with in-place form.
-"""
-function trace!(dy, y, p::TPTuple, t)
-	q2m, Efunc, Bfunc = p
+function get_dv(x, v, p, t)
+	q2m, m, Efunc, Bfunc, Ffunc = p
+	E = Efunc(x, t)
+	B = Bfunc(x, t)
+	F = Ffunc(x, t)
 
-	v = y[SA[4:6...]]
-	E = Efunc(y, t)
-	B = Bfunc(y, t)
-
-	dy[1:3] = v
-	dy[4:6] = SVector{3}(q2m * (v × B + E))
+	SVector{3}(q2m * (v × B + E) + F / m)
+end
 
-	return
+function get_relativistic_v(γv; c=c)
+	γ²v² = γv[1]^2 + γv[2]^2 + γv[3]^2
+	if γ²v² > eps(eltype(γv))
+		v̂ = normalize(γv)
+	else # no velocity
+		v̂ = SVector{3,eltype(γv)}(0, 0, 0)
+	end
+	√(γ²v² / (1 + γ²v² / c^2)) * v̂
 end
 
-function trace!(dy, y, p::FullTPTuple, t)
-	q, m, Efunc, Bfunc, Ffunc = p
 
-	v = y[SA[4:6...]]
-	E = Efunc(y, t)
-	B = Bfunc(y, t)
-	F = Ffunc(y, t)
+"""
+	trace!(dy, y, p, t)
 
-	dy[1:3] = v
-	dy[4:6] = SVector{3}((q * (v × B + E) + F) / m)
+ODE equations for charged particle moving in static EM field and external force field with in-place form.
+"""
+function trace!(dy, y, p, t)
+	v = get_v(y)
+	@inbounds dy[1:3] = v
+	@inbounds dy[4:6] = get_dv(y, v, p, t)
 
-	return
+	return nothing
 end
 
 """
-	 trace(y, p::TPTuple, t) -> SVector{6, Float64}
-	 trace(y, p::FullTPTuple, t) -> SVector{6, Float64}
-
-ODE equations for charged particle moving in static EM field with out-of-place form.
+	trace(y, p, t) -> SVector{6, Float64}
 
 ODE equations for charged particle moving in static EM field and external force field with out-of-place form.
 """
-function trace(y, p::TPTuple, t)
-	q2m, Efunc, Bfunc = p
-	v = y[SA[4:6...]]
-	E = Efunc(y, t)
-	B = Bfunc(y, t)
-
-	dv = SVector{3}(q2m * (v × B + E))
-
-	vcat(v, dv)
-end
-
-function trace(y, p::FullTPTuple, t)
-	q, m, Efunc, Bfunc, Ffunc = p
-
+function trace(y, p, t)
 	v = y[SA[4:6...]]
-	E = Efunc(y, t)
-	B = Bfunc(y, t)
-	F = Ffunc(y, t)
-
-	dv = SVector{3}((q * (v × B + E) + F) / m)
-
+	dv = get_dv(y, v, p, t)
 	vcat(v, dv)
 end
 
 """
-	 trace_relativistic!(dy, y, p::TPTuple, t)
+	 trace_relativistic!(dy, y, p, t)
 
 ODE equations for relativistic charged particle (x, γv) moving in static EM field with in-place form.
 """
-function trace_relativistic!(dy, y, p::TPTuple, t)
-	q2m, Efunc, Bfunc = p
-	E = Efunc(y, t)
-	B = Bfunc(y, t)
-
-	γv = y[SA[4:6...]]
-	γ²v² = γv[1]^2 + γv[2]^2 + γv[3]^2
-	if γ²v² > eps(eltype(dy))
-		v̂ = normalize(γv)
-	else # no velocity
-		v̂ = SVector{3, eltype(dy)}(0, 0, 0)
-	end
-	vmag = √(γ²v² / (1 + γ²v²/c^2))
-	v = vmag * v̂
-
-	dy[1:3] = v
-	dy[4:6] = SVector{3}(q2m * (v × B + E))
+function trace_relativistic!(dy, y, p, t)
+	γv = get_v(y)
+	v = get_relativistic_v(γv)
+	@inbounds dy[1:3] = v
+	@inbounds dy[4:6] = get_dv(y, v, p, t)
 
 	return
 end
 
 """
-	 trace_relativistic(y, p::TPTuple, t) -> SVector{6}
+	 trace_relativistic(y, p, t) -> SVector{6}
 
 ODE equations for relativistic charged particle (x, γv) moving in static EM field with out-of-place form.
 """
-function trace_relativistic(y, p::TPTuple, t)
-	q2m, Efunc, Bfunc = p
-	E = Efunc(y, t)
-	B = Bfunc(y, t)
-
-	γv = y[SA[4:6...]]
-	γ²v² = γv[1]^2 + γv[2]^2 + γv[3]^2
-	if γ²v² > eps(eltype(y))
-		v̂ = normalize(γv)
-	else # no velocity
-		v̂ = SVector{3, eltype(y)}(0, 0, 0)
-	end
-	vmag = √(γ²v² / (1 + γ²v²/c^2))
-	v = vmag * v̂
-	dv = SVector{3}(q2m * (v × B + E))
+function trace_relativistic(y, p, t)
+	γv = get_v(y)
+	v = get_relativistic_v(γv)
+	dv = get_dv(y, v, p, t)
 
 	vcat(v, dv)
 end
 
 """
-	 trace_normalized!(dy, y, p::TPNormalizedTuple, t)
+	 trace_normalized!(dy, y, p, t)
 
 Normalized ODE equations for charged particle moving in static EM field with in-place form.
 If the field is in 2D X-Y plane, periodic boundary should be applied for the field in z via
 the extrapolation function provided by Interpolations.jl.
 """
-function trace_normalized!(dy, y, p::TPNormalizedTuple, t)
-	_, Efunc, Bfunc = p
+function trace_normalized!(dy, y, p, t)
+	v = get_v(y)
+	E = get_EField(p)(y, t)
+	B = get_BField(p)(y, t)
 
-	v = y[SA[4:6...]]
-	E = Efunc(y, t)
-	B = Bfunc(y, t)
-
-	dy[1:3] = v
-	dy[4:6] = SVector{3}(v × B + E)
+	@inbounds dy[1:3] = v
+	@inbounds dy[4:6] = SVector{3}(v × B + E)
 
 	return
 end
 
 """
-	 trace_relativistic_normalized!(dy, y, p::TPNormalizedTuple, t)
+	 trace_relativistic_normalized!(dy, y, p, t)
 
 Normalized ODE equations for relativistic charged particle (x, γv) moving in static EM field with in-place form.
 """
-function trace_relativistic_normalized!(dy, y, p::TPNormalizedTuple, t)
-	_, Efunc, Bfunc = p
-	E = Efunc(y, t)
-	B = Bfunc(y, t)
-	γv = y[SA[4:6...]]
-
-	γ²v² = γv[1]^2 + γv[2]^2 + γv[3]^2
-	if γ²v² > eps(eltype(dy))
-		v̂ = normalize(γv)
-	else # no velocity
-		v̂ = SVector{3, eltype(dy)}(0, 0, 0)
-	end
-	vmag = √(γ²v² / (1 + γ²v²))
-	v = vmag * v̂
+function trace_relativistic_normalized!(dy, y, p, t)
+	E = get_EField(p)(y, t)
+	B = get_BField(p)(y, t)
+	γv = get_v(y)
 
-	dy[1:3] = v
-	dy[4:6] = SVector{3}(v × B + E)
+	v = get_relativistic_v(γv; c=1)
+	@inbounds dy[1:3] = v
+	@inbounds dy[4:6] = SVector{3}(v × B + E)
 
 	return
 end
 
 """
-	 trace_relativistic_normalized(y, p::TPNormalizedTuple, t)
+	 trace_relativistic_normalized(y, p, t)
 
 Normalized ODE equations for relativistic charged particle (x, γv) moving in static EM field with out-of-place form.
 """
-function trace_relativistic_normalized(y, p::TPNormalizedTuple, t)
-	_, Efunc, Bfunc = p
-	E = Efunc(y, t)
-	B = Bfunc(y, t)
-	γv = y[SA[4:6...]]
+function trace_relativistic_normalized(y, p, t)
+	E = get_EField(p)(y, t)
+	B = get_BField(p)(y, t)
+	γv = get_v(y)
 
-	γ²v² = γv[1]^2 + γv[2]^2 + γv[3]^2
-	if γ²v² > eps(eltype(y))
-		v̂ = normalize(γv)
-	else # no velocity
-		v̂ = SVector{3,eltype(y)}(0, 0, 0)
-	end
-	vmag = √(γ²v² / (1 + γ²v²))
-	v = vmag * v̂
+	v = get_relativistic_v(γv; c=1)
 	dv = SVector{3}(v × B + E)
 
 	vcat(v, dv)
@@ -194,9 +132,9 @@ Equations for tracing the guiding center using analytical drifts, including the
 Parallel velocity is also added. This expression requires the full particle trajectory `p.sol`.
 """
 function trace_gc_drifts!(dx, x, p, t)
-	q2m, Efunc, Bfunc, sol = p
+	q2m, _, Efunc, Bfunc, _, sol = p
 	xu = sol(t)
-	v = xu[SA[4:6...]]
+	v = get_v(xu)
 	E = Efunc(x)
 	B = Bfunc(x)
 
@@ -205,12 +143,14 @@ function trace_gc_drifts!(dx, x, p, t)
 	b = normalize(B)
 	v_par = (v ⋅ b) .* b
 	v_perp = v - v_par
-	Ω = q2m*norm(B)
+	Ω = q2m * norm(B)
 	κ = ForwardDiff.jacobian(Bfunc, x) * B  # B⋅∇B
 	## v⟂^2*(B×∇|B|)/(2*Ω*B^2) + v∥^2*(B×(B⋅∇B))/(Ω*B^3) + (E×B)/B^2 + v∥
-	dx[1:3] =
-		norm(v_perp)^2*(B × ∇B)/(2*Ω*norm(B)^2) +
-		norm(v_par)^2*(B × κ)/Ω/norm(B)^3 + (E × B)/(B ⋅ B) + v_par
+	@inbounds dx[1:3] =
+		norm(v_perp)^2 * (B × ∇B) / (2 * Ω * norm(B)^2) +
+		norm(v_par)^2 * (B × κ) / Ω / norm(B)^3 + (E × B) / (B ⋅ B) + v_par
+
+   return
 end
 
 """
@@ -222,7 +162,7 @@ Variable `y = (x, y, z, u)`, where `u` is the velocity along the magnetic field
 function trace_gc!(dy, y, p::GCTuple, t)
 	q, m, μ, Efunc, Bfunc = p
 	q2m = q / m
-	X = y[SA[1:3...]]
+	X = get_x(y)
 	E = Efunc(X, t)
 	B = Bfunc(X, t)
 	b̂ = normalize(B) # unit B field at X
@@ -243,13 +183,13 @@ function trace_gc!(dy, y, p::GCTuple, t)
 	# ∇ × b̂
 	curlb = SVector{3}(∇b̂[3, 2] - ∇b̂[2, 3], ∇b̂[1, 3] - ∇b̂[3, 1], ∇b̂[2, 1] - ∇b̂[1, 2])
 	# effective EM fields
-	Eᵉ = @. E - (μ*∇B - 0.5*m*∇vE²) / q
+	Eᵉ = @. E - (μ * ∇B - 0.5 * m * ∇vE²) / q
 	Bᵉ = @. B + y[4] / q2m * curlb
 	Bparᵉ = b̂ ⋅ Bᵉ # effective B field parallel to B
 
-	dy[1] = (y[4] * Bᵉ[1] + Eᵉ[2]*b̂[3] - Eᵉ[3]*b̂[2]) / Bparᵉ
-	dy[2] = (y[4] * Bᵉ[2] + Eᵉ[3]*b̂[1] - Eᵉ[1]*b̂[3]) / Bparᵉ
-	dy[3] = (y[4] * Bᵉ[3] + Eᵉ[1]*b̂[2] - Eᵉ[2]*b̂[1]) / Bparᵉ
+	dy[1] = (y[4] * Bᵉ[1] + Eᵉ[2] * b̂[3] - Eᵉ[3] * b̂[2]) / Bparᵉ
+	dy[2] = (y[4] * Bᵉ[2] + Eᵉ[3] * b̂[1] - Eᵉ[1] * b̂[3]) / Bparᵉ
+	dy[3] = (y[4] * Bᵉ[3] + Eᵉ[1] * b̂[2] - Eᵉ[2] * b̂[1]) / Bparᵉ
 	dy[4] = q2m / Bparᵉ * Bᵉ ⋅ Eᵉ
 
 	return
@@ -259,7 +199,7 @@ end
 function trace_gc_1st!(dy, y, p::GCTuple, t)
 	q, m, μ, Efunc, Bfunc = p
 	q2m = q / m
-	X = y[SA[1:3...]]
+	X = get_x(y)
 	E = Efunc(X, t)
 	B = Bfunc(X, t)
 	b̂ = normalize(B) # unit B field at X
@@ -272,14 +212,14 @@ function trace_gc_1st!(dy, y, p::GCTuple, t)
 	bfunc(x) = normalize(Bfunc(x, t))
 	∇b̂ = ForwardDiff.jacobian(bfunc, X)
 	# effective EM fields
-	Eᵉ = @. E - (μ*∇B) / q
+	Eᵉ = @. E - (μ * ∇B) / q
 	κ = ∇b̂ * b̂  # curvature
-	vX = u * b̂ + b̂ × (κ*u^2 - q2m*Eᵉ) / Ω
+	vX = u * b̂ + b̂ × (κ * u^2 - q2m * Eᵉ) / Ω
 
 	dy[1] = vX[1]
 	dy[2] = vX[2]
 	dy[3] = vX[3]
-	dy[4] = q2m * (b̂ + u*(b̂ × κ)/Ω) ⋅ Eᵉ
+	dy[4] = q2m * (b̂ + u * (b̂ × κ) / Ω) ⋅ Eᵉ
 
 	return
 end
diff --git a/src/gc.jl b/src/gc.jl
index c525a6ad0..ada4b1219 100644
--- a/src/gc.jl
+++ b/src/gc.jl
@@ -73,7 +73,7 @@ function prepare_gc(xv, E, B; species::Species = Proton, q::AbstractFloat = 1.0,
 end
 
 """
-	 get_gc(xu, param::Union{TPTuple, FullTPTuple})
+	 get_gc(xu, param)
 	 get_gc(x, y, z, vx, vy, vz, bx, by, bz, q2m)
 
 Calculate the coordinates of the guiding center according to the phase space coordinates of a particle.
@@ -84,8 +84,9 @@ Nonrelativistic definition:
 \\mathbf{X}=\\mathbf{x}-m\\frac{\\mathbf{b}\\times\\mathbf{v}}{qB}
 ```
 """
-function get_gc(xu, param::TPTuple)
-	q2m, _, B_field = param
+function get_gc(xu, param)
+	q2m = get_q2m(param)
+	B_field = get_BField(param)
 	t = xu[end]
 	v = xu[SA[4:6...]]
 	B = B_field(xu, t)
@@ -96,18 +97,6 @@ function get_gc(xu, param::TPTuple)
 	X = @views xu[1:3] - ρ
 end
 
-function get_gc(xu, param::FullTPTuple)
-	q, m, _, B_field = param
-	t = xu[end]
-	v = xu[SA[4:6...]]
-	B = B_field(xu, t)
-	B² = B[1]^2 + B[2]^2 + B[3]^2
-	# vector of Larmor radius
-	ρ = B × v ./ (q*B²/m)
-
-	X = @views xu[1:3] - ρ
-end
-
 function get_gc(x, y, z, vx, vy, vz, bx, by, bz, q2m)
 	l = SVector{3}(x, y, z)
 	B = SVector{3}(bx, by, bz)
diff --git a/src/prepare.jl b/src/prepare.jl
index 0346e3e7b..a4a14ece8 100644
--- a/src/prepare.jl
+++ b/src/prepare.jl
@@ -58,12 +58,21 @@ TPNormalizedTuple = Tuple{AbstractFloat, AbstractField, AbstractField}
 "The type of parameter tuple for guiding center problem."
 GCTuple = Tuple{Float64, Float64, Float64, AbstractField, AbstractField}
 
+get_q2m(param) = param[1]
+get_BField(param::TPTuple) = param[3]
+get_BField(param) = param[4]
+get_EField(param::TPTuple) = param[2]
+get_EField(param) = param[3]
 
 """
-	 prepare(grid::CartesianGrid, E, B; kwargs...) -> (q2m, E, B)
+	prepare(args...; kwargs...) -> (q2m, m, E, B, F)
+	prepare(E, B, F = ZeroField(); kwargs...)
 
-Return a tuple consists of particle charge-mass ratio for a prescribed `species` and
-interpolated EM field functions.
+Return a tuple consists of particle charge-mass ratio for a prescribed `species` of charge `q` and mass `m`,
+	mass `m` for a prescribed `species`, analytic/interpolated EM field functions, and external force `F`.
+
+Prescribed `species` are `Electron` and `Proton`; 
+	other species can be manually specified with `species=Ion/User`, `q` and `m`.
 
 # Keywords
 - `order::Int=1`: order of interpolation in [1,2,3].
@@ -72,30 +81,20 @@ interpolated EM field functions.
 - `q::AbstractFloat=1.0`: particle charge. Only works when `Species=User`.
 - `m::AbstractFloat=1.0`: particle mass. Only works when `Species=User`.
 
-	 prepare(grid::CartesianGrid, E, B, F; species=Proton, q=1.0, m=1.0) -> (q, m, E, B, F)
+	 prepare(grid::CartesianGrid, E, B; kwargs...)
+	 prepare(grid::CartesianGrid, E, B, F; species=Proton, q=1.0, m=1.0)
 
 Return a tuple consists of particle charge, mass for a prescribed `species` of charge `q`
 and mass `m`, interpolated EM field functions, and external force `F`.
 
-	 prepare(x::AbstractRange, y::AbstractRange, z::AbstractRange, E, B; kwargs...) -> (q2m, E, B)
-	 prepare(x, y, E, B; kwargs...) -> (q2m, E, B)
+	 prepare(x::AbstractRange, y::AbstractRange, z::AbstractRange, E, B; kwargs...)
+	 prepare(x, y, E, B; kwargs...)
 
-	 prepare(x::AbstractRange, E, B; kwargs...) -> (q2m, E, B)
+	 prepare(x::AbstractRange, E, B; kwargs...)
 
 1D grid. An additional keyword `dir` is used for specifying the spatial direction, 1 -> x, 2 -> y, 3 -> z.
 
 Direct range input for uniform grid in 2/3D is also accepted.
-
-	 prepare(E, B; kwargs...) -> (q2m, E, B)
-
-Return a tuple consists of particle charge-mass ratio for a prescribed `species` of charge
-`q` and mass `m` and analytic EM field functions. Prescribed `species` are `Electron` and
-`Proton`; other species can be manually specified with `species=Ion/User`, `q` and `m`.
-
-	 prepare(E, B, F; kwargs...) -> (q, m, E, B, F)
-
-Return a tuple consists of particle charge, mass for a prescribed `species` of charge `q`
-and mass `m`, analytic EM field functions, and external force `F`.
 """
 function prepare(grid::CartesianGrid, E::TE, B::TB; species::Species = Proton,
 	q::AbstractFloat = 1.0, m::AbstractFloat = 1.0, order::Int = 1, bc::Int = 1,
@@ -113,7 +112,7 @@ function prepare(grid::CartesianGrid, E::TE, B::TB; species::Species = Proton,
 		B = TB <: AbstractArray ? getinterp(B, gridx, gridy, order, bc) : B
 	end
 
-	q/m, Field(E), Field(B)
+	q/m, m, Field(E), Field(B), ZeroField()
 end
 
 function prepare(grid::CartesianGrid, E::TE, B::TB, F::TF; species::Species = Proton,
@@ -128,7 +127,7 @@ function prepare(grid::CartesianGrid, E::TE, B::TB, F::TF; species::Species = Pr
 	B = TB <: AbstractArray ? getinterp(B, gridx, gridy, gridz, order, bc) : B
 	F = TF <: AbstractArray ? getinterp(F, gridx, gridy, gridz, order, bc) : F
 
-	q, m, Field(E), Field(B), Field(F)
+	q/m, m, Field(E), Field(B), Field(F)
 end
 
 function prepare(x::T, y::T, E::TE, B::TB; species::Species = Proton,
@@ -140,7 +139,7 @@ function prepare(x::T, y::T, E::TE, B::TB; species::Species = Proton,
 	E = TE <: AbstractArray ? getinterp(E, x, y, order, bc) : E
 	B = TB <: AbstractArray ? getinterp(B, x, y, order, bc) : B
 
-	q/m, Field(E), Field(B)
+	q/m, m, Field(E), Field(B), ZeroField()
 end
 
 function prepare(x::T, E::TE, B::TB; species::Species = Proton, q::AbstractFloat = 1.0,
@@ -152,7 +151,7 @@ function prepare(x::T, E::TE, B::TB; species::Species = Proton, q::AbstractFloat
 	E = TE <: AbstractArray ? getinterp(E, x, order, bc; dir) : E
 	B = TB <: AbstractArray ? getinterp(B, x, order, bc; dir) : B
 
-	q/m, Field(E), Field(B)
+	q/m, m, Field(E), Field(B), ZeroField()
 end
 
 function prepare(x::T, y::T, z::T, E::TE, B::TB;
@@ -165,30 +164,18 @@ function prepare(x::T, y::T, z::T, E::TE, B::TB;
 	E = TE <: AbstractArray ? getinterp(E, x, y, z, order, bc) : E
 	B = TB <: AbstractArray ? getinterp(B, x, y, z, order, bc) : B
 
-	q/m, Field(E), Field(B)
-end
-
-function prepare(
-	E,
-	B;
-	species::Species = Proton,
-	q::AbstractFloat = 1.0,
-	m::AbstractFloat = 1.0,
-)
-	q, m = getchargemass(species, q, m)
-
-	q/m, Field(E), Field(B)
+	q/m, m, Field(E), Field(B), ZeroField()
 end
 
-function prepare(E, B, F; species::Species = Proton, q::AbstractFloat = 1.0,
+function prepare(E, B, F = ZeroField(); species::Species = Proton, q::AbstractFloat = 1.0,
 	m::AbstractFloat = 1.0)
 	q, m = getchargemass(species, q, m)
 
-	q, m, Field(E), Field(B), Field(F)
+	q/m, m, Field(E), Field(B), Field(F)
 end
 
 
-import Base: (+), (*), (/), setindex!
+import Base: (+), (*), (/), setindex!, getindex
 import LinearAlgebra: ×
 import StaticArrays: StaticArray
 
@@ -211,7 +198,9 @@ struct ZeroVector end
 
 # Make ZeroVector work with array assignment
 Base.setindex!(A::AbstractArray, ::ZeroVector, I...) = fill!(view(A, I...), 0)
+Base.getindex(::ZeroVector, I...) = 0
 
 # Field interface
 Field(x::ZeroField) = x
-(::ZeroField)(y, t) = ZeroVector()
\ No newline at end of file
+(::ZeroField)(y, t) = ZeroVector()
+(::ZeroField)(_) = ZeroVector()
\ No newline at end of file
diff --git a/src/pusher.jl b/src/pusher.jl
index 2b2f5e8c1..c2b24082d 100644
--- a/src/pusher.jl
+++ b/src/pusher.jl
@@ -13,6 +13,10 @@ struct TraceProblem{uType, tType, isinplace, P, F <: AbstractODEFunction, PF} <:
 	prob_func::PF
 end
 
+
+get_EField(p::AbstractODEProblem) = get_EField(p.p)
+get_BField(p::AbstractODEProblem) = get_BField(p.p)
+
 struct TraceSolution{T, N, uType, uType2, DType, tType, rateType, P, A, IType, S, AC} <:
 		 AbstractODESolution{T, N, uType}
 	"positions and velocities"
@@ -41,6 +45,9 @@ function TraceSolution{T, N}(u, u_analytic, errors, t, k, prob, alg, interp, den
 		dense, tslocation, stats, alg_choice, retcode)
 end
 
+get_BField(sol::AbstractODESolution) = get_BField(sol.prob)
+get_EField(sol::AbstractODESolution) = get_EField(sol.prob)
+
 Base.length(ts::TraceSolution) = length(ts.t)
 
 "Interpolate solution at time `x`. Forward tracing only."
@@ -111,7 +118,9 @@ Reference: [DTIC](https://apps.dtic.mil/sti/citations/ADA023511)
 """
 function update_velocity!(xv, paramBoris, param, dt, t)
 	(; v⁻, v′, v⁺, t_rotate, s_rotate, v⁻_cross_t, v′_cross_s) = paramBoris
-	q2m, E, B = param[1], param[2](xv, t), param[3](xv, t)
+	q2m, _, Efunc, Bfunc = param
+	E = Efunc(xv, t)
+	B = Bfunc(xv, t)
 	# t vector
 	for dim in 1:3
 		t_rotate[dim] = q2m*B[dim]*0.5*dt


Reviews:
user: login: henry2004y
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-19T14:11:58Z
user: login: henry2004y
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-19T14:13:09Z
user: login: Beforerr
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-19T14:21:07Z
user: login: henry2004y
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-19T14:32:09Z
user: login: henry2004y
body: The other tiny formatting thing I noticed is that you are probably using whitespaces while the current master uses tabs (which is why there are so many extra diffs in comparison). I was using whitespaces as well before. However, the Julia formatter by default uses tabs. It is probably a good switch since I prefer 3 space tabs while the standard Julia guidance suggests 4-space separation. VScode has the option to set tabs with either 3 or 4 spaces, so it's no longer an annoying issue for collaboration.

---

Now my attempt is to introduce a format configuration file for the package #239. It mostly follows the SciMLStyle. To avoid affecting this PR, the formatting setup will be merged afterwards.

state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-19T14:37:56Z

Review comments:
diff_hunk: @@ -1,190 +1,128 @@
 # Tracing equations.
 
-"""
-	 trace!(dy, y, p::TPTuple, t)
-	 trace!(dy, y, p::FullTPTuple, t)
-
-ODE equations for charged particle moving in static EM field with in-place form.
-
-ODE equations for charged particle moving in static EM field and external force field with in-place form.
-"""
-function trace!(dy, y, p::TPTuple, t)
-	q2m, Efunc, Bfunc = p
-
-	v = y[SA[4:6...]]
-	E = Efunc(y, t)
-	B = Bfunc(y, t)
-
-	dy[1:3] = v
-	dy[4:6] = SVector{3}(q2m * (v × B + E))
+function get_dv(u, v, p, t)
+    q2m, m, Efunc, Bfunc, Ffunc = p
+    E = Efunc(u, t)
+    B = Bfunc(u, t)
+    F = Ffunc(u, t)
 
-	return
+    SVector{3}(q2m * (v × B + E) + F / m)
 end
 
-function trace!(dy, y, p::FullTPTuple, t)
-	q, m, Efunc, Bfunc, Ffunc = p
+function get_relativistic_v(γv; c=c)
+    γ²v² = γv[1]^2 + γv[2]^2 + γv[3]^2
+    if γ²v² > eps(eltype(γv))
+        v̂ = normalize(γv)
+    else # no velocity
+        v̂ = SVector{3,eltype(γv)}(0, 0, 0)
+    end
+    √(γ²v² / (1 + γ²v² / c^2)) * v̂
+end
 
-	v = y[SA[4:6...]]
-	E = Efunc(y, t)
-	B = Bfunc(y, t)
-	F = Ffunc(y, t)
 
-	dy[1:3] = v
-	dy[4:6] = SVector{3}((q * (v × B + E) + F) / m)
+"""
+	trace!(dy, y, p, t)
 
-	return
+ODE equations for charged particle moving in static EM field and external force field with in-place form.
+"""
+function trace!(dy, y, p, t)
+    v = y[SA[4:6...]]
+    dy[1:3] = v
+    dy[4:6] = get_dv(y, v, p, t)
+    return
 end
 
 """
-	 trace(y, p::TPTuple, t) -> SVector{6, Float64}
-	 trace(y, p::FullTPTuple, t) -> SVector{6, Float64}
-
-ODE equations for charged particle moving in static EM field with out-of-place form.
+	trace(y, p, t) -> SVector{6, Float64}
 
 ODE equations for charged particle moving in static EM field and external force field with out-of-place form.
 """
-function trace(y, p::TPTuple, t)
-	q2m, Efunc, Bfunc = p
-	v = y[SA[4:6...]]
-	E = Efunc(y, t)
-	B = Bfunc(y, t)
-
-	dv = SVector{3}(q2m * (v × B + E))
-
-	vcat(v, dv)
-end
-
-function trace(y, p::FullTPTuple, t)
-	q, m, Efunc, Bfunc, Ffunc = p
-
-	v = y[SA[4:6...]]
-	E = Efunc(y, t)
-	B = Bfunc(y, t)
-	F = Ffunc(y, t)
-
-	dv = SVector{3}((q * (v × B + E) + F) / m)
-
-	vcat(v, dv)
+function trace(y, p, t)
+    v = y[SA[4:6...]]
+    dv = get_dv(y, v, p, t)
+    vcat(v, dv)
 end
 
 """
-	 trace_relativistic!(dy, y, p::TPTuple, t)
+	 trace_relativistic!(dy, y, p, t)
 
 ODE equations for relativistic charged particle (x, γv) moving in static EM field with in-place form.
 """
-function trace_relativistic!(dy, y, p::TPTuple, t)
-	q2m, Efunc, Bfunc = p
-	E = Efunc(y, t)
-	B = Bfunc(y, t)
-
-	γv = y[SA[4:6...]]
-	γ²v² = γv[1]^2 + γv[2]^2 + γv[3]^2
-	if γ²v² > eps(eltype(dy))
-		v̂ = normalize(γv)
-	else # no velocity
-		v̂ = SVector{3, eltype(dy)}(0, 0, 0)
-	end
-	vmag = √(γ²v² / (1 + γ²v²/c^2))
-	v = vmag * v̂
-
-	dy[1:3] = v
-	dy[4:6] = SVector{3}(q2m * (v × B + E))
-
-	return
+function trace_relativistic!(dy, y, p, t)
+    γv = y[SA[4:6...]]
+    v = get_relativistic_v(γv)
+    dy[1:3] = v
+    dy[4:6] = get_dv(y, v, p, t)
+
+    return
 end
 
 """
-	 trace_relativistic(y, p::TPTuple, t) -> SVector{6}
+	 trace_relativistic(y, p, t) -> SVector{6}
 
 ODE equations for relativistic charged particle (x, γv) moving in static EM field with out-of-place form.
 """
-function trace_relativistic(y, p::TPTuple, t)
-	q2m, Efunc, Bfunc = p
-	E = Efunc(y, t)
-	B = Bfunc(y, t)
-
-	γv = y[SA[4:6...]]
-	γ²v² = γv[1]^2 + γv[2]^2 + γv[3]^2
-	if γ²v² > eps(eltype(y))
-		v̂ = normalize(γv)
-	else # no velocity
-		v̂ = SVector{3, eltype(y)}(0, 0, 0)
-	end
-	vmag = √(γ²v² / (1 + γ²v²/c^2))
-	v = vmag * v̂
-	dv = SVector{3}(q2m * (v × B + E))
-
-	vcat(v, dv)
+function trace_relativistic(y, p, t)
+    γv = y[SA[4:6...]]
+    v = get_relativistic_v(γv)
+    dv = get_dv(y, v, p, t)
+
+    vcat(v, dv)
 end
 
 """
-	 trace_normalized!(dy, y, p::TPNormalizedTuple, t)
+	 trace_normalized!(dy, y, p, t)
 
 Normalized ODE equations for charged particle moving in static EM field with in-place form.
 If the field is in 2D X-Y plane, periodic boundary should be applied for the field in z via
 the extrapolation function provided by Interpolations.jl.
 """
-function trace_normalized!(dy, y, p::TPNormalizedTuple, t)
-	_, Efunc, Bfunc = p
+function trace_normalized!(dy, y, p, t)
+    _, m, Efunc, Bfunc = p
path: src/equations.jl
user: login: henry2004y
body: I guess this `m` is not used here.
created_at: 2025-05-19T14:11:58Z
author_association: OWNER
diff_hunk: @@ -1,190 +1,128 @@
 # Tracing equations.
 
-"""
-	 trace!(dy, y, p::TPTuple, t)
-	 trace!(dy, y, p::FullTPTuple, t)
-
-ODE equations for charged particle moving in static EM field with in-place form.
-
-ODE equations for charged particle moving in static EM field and external force field with in-place form.
-"""
-function trace!(dy, y, p::TPTuple, t)
-	q2m, Efunc, Bfunc = p
-
-	v = y[SA[4:6...]]
-	E = Efunc(y, t)
-	B = Bfunc(y, t)
-
-	dy[1:3] = v
-	dy[4:6] = SVector{3}(q2m * (v × B + E))
+function get_dv(u, v, p, t)
+    q2m, m, Efunc, Bfunc, Ffunc = p
+    E = Efunc(u, t)
+    B = Bfunc(u, t)
+    F = Ffunc(u, t)
 
-	return
+    SVector{3}(q2m * (v × B + E) + F / m)
 end
 
-function trace!(dy, y, p::FullTPTuple, t)
-	q, m, Efunc, Bfunc, Ffunc = p
+function get_relativistic_v(γv; c=c)
+    γ²v² = γv[1]^2 + γv[2]^2 + γv[3]^2
+    if γ²v² > eps(eltype(γv))
+        v̂ = normalize(γv)
+    else # no velocity
+        v̂ = SVector{3,eltype(γv)}(0, 0, 0)
+    end
+    √(γ²v² / (1 + γ²v² / c^2)) * v̂
+end
 
-	v = y[SA[4:6...]]
-	E = Efunc(y, t)
-	B = Bfunc(y, t)
-	F = Ffunc(y, t)
 
-	dy[1:3] = v
-	dy[4:6] = SVector{3}((q * (v × B + E) + F) / m)
+"""
+	trace!(dy, y, p, t)
 
-	return
+ODE equations for charged particle moving in static EM field and external force field with in-place form.
+"""
+function trace!(dy, y, p, t)
+    v = y[SA[4:6...]]
+    dy[1:3] = v
+    dy[4:6] = get_dv(y, v, p, t)
+    return
 end
 
 """
-	 trace(y, p::TPTuple, t) -> SVector{6, Float64}
-	 trace(y, p::FullTPTuple, t) -> SVector{6, Float64}
-
-ODE equations for charged particle moving in static EM field with out-of-place form.
+	trace(y, p, t) -> SVector{6, Float64}
 
 ODE equations for charged particle moving in static EM field and external force field with out-of-place form.
 """
-function trace(y, p::TPTuple, t)
-	q2m, Efunc, Bfunc = p
-	v = y[SA[4:6...]]
-	E = Efunc(y, t)
-	B = Bfunc(y, t)
-
-	dv = SVector{3}(q2m * (v × B + E))
-
-	vcat(v, dv)
-end
-
-function trace(y, p::FullTPTuple, t)
-	q, m, Efunc, Bfunc, Ffunc = p
-
-	v = y[SA[4:6...]]
-	E = Efunc(y, t)
-	B = Bfunc(y, t)
-	F = Ffunc(y, t)
-
-	dv = SVector{3}((q * (v × B + E) + F) / m)
-
-	vcat(v, dv)
+function trace(y, p, t)
+    v = y[SA[4:6...]]
+    dv = get_dv(y, v, p, t)
+    vcat(v, dv)
 end
 
 """
-	 trace_relativistic!(dy, y, p::TPTuple, t)
+	 trace_relativistic!(dy, y, p, t)
 
 ODE equations for relativistic charged particle (x, γv) moving in static EM field with in-place form.
 """
-function trace_relativistic!(dy, y, p::TPTuple, t)
-	q2m, Efunc, Bfunc = p
-	E = Efunc(y, t)
-	B = Bfunc(y, t)
-
-	γv = y[SA[4:6...]]
-	γ²v² = γv[1]^2 + γv[2]^2 + γv[3]^2
-	if γ²v² > eps(eltype(dy))
-		v̂ = normalize(γv)
-	else # no velocity
-		v̂ = SVector{3, eltype(dy)}(0, 0, 0)
-	end
-	vmag = √(γ²v² / (1 + γ²v²/c^2))
-	v = vmag * v̂
-
-	dy[1:3] = v
-	dy[4:6] = SVector{3}(q2m * (v × B + E))
-
-	return
+function trace_relativistic!(dy, y, p, t)
+    γv = y[SA[4:6...]]
+    v = get_relativistic_v(γv)
+    dy[1:3] = v
+    dy[4:6] = get_dv(y, v, p, t)
+
+    return
 end
 
 """
-	 trace_relativistic(y, p::TPTuple, t) -> SVector{6}
+	 trace_relativistic(y, p, t) -> SVector{6}
 
 ODE equations for relativistic charged particle (x, γv) moving in static EM field with out-of-place form.
 """
-function trace_relativistic(y, p::TPTuple, t)
-	q2m, Efunc, Bfunc = p
-	E = Efunc(y, t)
-	B = Bfunc(y, t)
-
-	γv = y[SA[4:6...]]
-	γ²v² = γv[1]^2 + γv[2]^2 + γv[3]^2
-	if γ²v² > eps(eltype(y))
-		v̂ = normalize(γv)
-	else # no velocity
-		v̂ = SVector{3, eltype(y)}(0, 0, 0)
-	end
-	vmag = √(γ²v² / (1 + γ²v²/c^2))
-	v = vmag * v̂
-	dv = SVector{3}(q2m * (v × B + E))
-
-	vcat(v, dv)
+function trace_relativistic(y, p, t)
+    γv = y[SA[4:6...]]
+    v = get_relativistic_v(γv)
+    dv = get_dv(y, v, p, t)
+
+    vcat(v, dv)
 end
 
 """
-	 trace_normalized!(dy, y, p::TPNormalizedTuple, t)
+	 trace_normalized!(dy, y, p, t)
 
 Normalized ODE equations for charged particle moving in static EM field with in-place form.
 If the field is in 2D X-Y plane, periodic boundary should be applied for the field in z via
 the extrapolation function provided by Interpolations.jl.
 """
-function trace_normalized!(dy, y, p::TPNormalizedTuple, t)
-	_, Efunc, Bfunc = p
+function trace_normalized!(dy, y, p, t)
+    _, m, Efunc, Bfunc = p
 
-	v = y[SA[4:6...]]
-	E = Efunc(y, t)
-	B = Bfunc(y, t)
+    v = y[SA[4:6...]]
+    E = Efunc(y, t)
+    B = Bfunc(y, t)
 
-	dy[1:3] = v
-	dy[4:6] = SVector{3}(v × B + E)
+    dy[1:3] = v
+    dy[4:6] = SVector{3}(v × B + E)
 
-	return
+    return
 end
 
 """
-	 trace_relativistic_normalized!(dy, y, p::TPNormalizedTuple, t)
+	 trace_relativistic_normalized!(dy, y, p, t)
 
 Normalized ODE equations for relativistic charged particle (x, γv) moving in static EM field with in-place form.
 """
-function trace_relativistic_normalized!(dy, y, p::TPNormalizedTuple, t)
-	_, Efunc, Bfunc = p
-	E = Efunc(y, t)
-	B = Bfunc(y, t)
-	γv = y[SA[4:6...]]
-
-	γ²v² = γv[1]^2 + γv[2]^2 + γv[3]^2
-	if γ²v² > eps(eltype(dy))
-		v̂ = normalize(γv)
-	else # no velocity
-		v̂ = SVector{3, eltype(dy)}(0, 0, 0)
-	end
-	vmag = √(γ²v² / (1 + γ²v²))
-	v = vmag * v̂
-
-	dy[1:3] = v
-	dy[4:6] = SVector{3}(v × B + E)
-
-	return
+function trace_relativistic_normalized!(dy, y, p, t)
+    _, m, Efunc, Bfunc = p
path: src/equations.jl
user: login: henry2004y
body: This `m` is also not used
created_at: 2025-05-19T14:13:09Z
author_association: OWNER
diff_hunk: @@ -1,190 +1,128 @@
 # Tracing equations.
 
-"""
-	 trace!(dy, y, p::TPTuple, t)
-	 trace!(dy, y, p::FullTPTuple, t)
-
-ODE equations for charged particle moving in static EM field with in-place form.
-
-ODE equations for charged particle moving in static EM field and external force field with in-place form.
-"""
-function trace!(dy, y, p::TPTuple, t)
-	q2m, Efunc, Bfunc = p
-
-	v = y[SA[4:6...]]
-	E = Efunc(y, t)
-	B = Bfunc(y, t)
-
-	dy[1:3] = v
-	dy[4:6] = SVector{3}(q2m * (v × B + E))
+function get_dv(u, v, p, t)
+    q2m, m, Efunc, Bfunc, Ffunc = p
+    E = Efunc(u, t)
+    B = Bfunc(u, t)
+    F = Ffunc(u, t)
 
-	return
+    SVector{3}(q2m * (v × B + E) + F / m)
 end
 
-function trace!(dy, y, p::FullTPTuple, t)
-	q, m, Efunc, Bfunc, Ffunc = p
+function get_relativistic_v(γv; c=c)
+    γ²v² = γv[1]^2 + γv[2]^2 + γv[3]^2
+    if γ²v² > eps(eltype(γv))
+        v̂ = normalize(γv)
+    else # no velocity
+        v̂ = SVector{3,eltype(γv)}(0, 0, 0)
+    end
+    √(γ²v² / (1 + γ²v² / c^2)) * v̂
+end
 
-	v = y[SA[4:6...]]
-	E = Efunc(y, t)
-	B = Bfunc(y, t)
-	F = Ffunc(y, t)
 
-	dy[1:3] = v
-	dy[4:6] = SVector{3}((q * (v × B + E) + F) / m)
+"""
+	trace!(dy, y, p, t)
 
-	return
+ODE equations for charged particle moving in static EM field and external force field with in-place form.
+"""
+function trace!(dy, y, p, t)
+    v = y[SA[4:6...]]
+    dy[1:3] = v
+    dy[4:6] = get_dv(y, v, p, t)
+    return
 end
 
 """
-	 trace(y, p::TPTuple, t) -> SVector{6, Float64}
-	 trace(y, p::FullTPTuple, t) -> SVector{6, Float64}
-
-ODE equations for charged particle moving in static EM field with out-of-place form.
+	trace(y, p, t) -> SVector{6, Float64}
 
 ODE equations for charged particle moving in static EM field and external force field with out-of-place form.
 """
-function trace(y, p::TPTuple, t)
-	q2m, Efunc, Bfunc = p
-	v = y[SA[4:6...]]
-	E = Efunc(y, t)
-	B = Bfunc(y, t)
-
-	dv = SVector{3}(q2m * (v × B + E))
-
-	vcat(v, dv)
-end
-
-function trace(y, p::FullTPTuple, t)
-	q, m, Efunc, Bfunc, Ffunc = p
-
-	v = y[SA[4:6...]]
-	E = Efunc(y, t)
-	B = Bfunc(y, t)
-	F = Ffunc(y, t)
-
-	dv = SVector{3}((q * (v × B + E) + F) / m)
-
-	vcat(v, dv)
+function trace(y, p, t)
+    v = y[SA[4:6...]]
+    dv = get_dv(y, v, p, t)
+    vcat(v, dv)
 end
 
 """
-	 trace_relativistic!(dy, y, p::TPTuple, t)
+	 trace_relativistic!(dy, y, p, t)
 
 ODE equations for relativistic charged particle (x, γv) moving in static EM field with in-place form.
 """
-function trace_relativistic!(dy, y, p::TPTuple, t)
-	q2m, Efunc, Bfunc = p
-	E = Efunc(y, t)
-	B = Bfunc(y, t)
-
-	γv = y[SA[4:6...]]
-	γ²v² = γv[1]^2 + γv[2]^2 + γv[3]^2
-	if γ²v² > eps(eltype(dy))
-		v̂ = normalize(γv)
-	else # no velocity
-		v̂ = SVector{3, eltype(dy)}(0, 0, 0)
-	end
-	vmag = √(γ²v² / (1 + γ²v²/c^2))
-	v = vmag * v̂
-
-	dy[1:3] = v
-	dy[4:6] = SVector{3}(q2m * (v × B + E))
-
-	return
+function trace_relativistic!(dy, y, p, t)
+    γv = y[SA[4:6...]]
+    v = get_relativistic_v(γv)
+    dy[1:3] = v
+    dy[4:6] = get_dv(y, v, p, t)
+
+    return
 end
 
 """
-	 trace_relativistic(y, p::TPTuple, t) -> SVector{6}
+	 trace_relativistic(y, p, t) -> SVector{6}
 
 ODE equations for relativistic charged particle (x, γv) moving in static EM field with out-of-place form.
 """
-function trace_relativistic(y, p::TPTuple, t)
-	q2m, Efunc, Bfunc = p
-	E = Efunc(y, t)
-	B = Bfunc(y, t)
-
-	γv = y[SA[4:6...]]
-	γ²v² = γv[1]^2 + γv[2]^2 + γv[3]^2
-	if γ²v² > eps(eltype(y))
-		v̂ = normalize(γv)
-	else # no velocity
-		v̂ = SVector{3, eltype(y)}(0, 0, 0)
-	end
-	vmag = √(γ²v² / (1 + γ²v²/c^2))
-	v = vmag * v̂
-	dv = SVector{3}(q2m * (v × B + E))
-
-	vcat(v, dv)
+function trace_relativistic(y, p, t)
+    γv = y[SA[4:6...]]
+    v = get_relativistic_v(γv)
+    dv = get_dv(y, v, p, t)
+
+    vcat(v, dv)
 end
 
 """
-	 trace_normalized!(dy, y, p::TPNormalizedTuple, t)
+	 trace_normalized!(dy, y, p, t)
 
 Normalized ODE equations for charged particle moving in static EM field with in-place form.
 If the field is in 2D X-Y plane, periodic boundary should be applied for the field in z via
 the extrapolation function provided by Interpolations.jl.
 """
-function trace_normalized!(dy, y, p::TPNormalizedTuple, t)
-	_, Efunc, Bfunc = p
+function trace_normalized!(dy, y, p, t)
+    _, m, Efunc, Bfunc = p
path: src/equations.jl
user: login: Beforerr
body: Right. (The typed code ignores it anyway:)

P.S.: I think for normalized trace we could introduce `m` and `Ffunc`, there is no reason not to do that.
But the normalized equation including F (especially for relativistic) may need to be discussed.
created_at: 2025-05-19T14:21:07Z
author_association: COLLABORATOR
diff_hunk: @@ -1,190 +1,128 @@
 # Tracing equations.
 
-"""
-	 trace!(dy, y, p::TPTuple, t)
-	 trace!(dy, y, p::FullTPTuple, t)
-
-ODE equations for charged particle moving in static EM field with in-place form.
-
-ODE equations for charged particle moving in static EM field and external force field with in-place form.
-"""
-function trace!(dy, y, p::TPTuple, t)
-	q2m, Efunc, Bfunc = p
-
-	v = y[SA[4:6...]]
-	E = Efunc(y, t)
-	B = Bfunc(y, t)
-
-	dy[1:3] = v
-	dy[4:6] = SVector{3}(q2m * (v × B + E))
+function get_dv(u, v, p, t)
path: src/equations.jl
user: login: henry2004y
body: Maybe rename `u` to `x` here to make it clear that the first argument is ordinary space coordinate?
created_at: 2025-05-19T14:32:09Z
author_association: OWNER

PR comments:
user: login: Beforerr
created_at: 2025-05-19T03:05:26Z
body: This is just refactor: I do not change `trace_relativistic` to include `F` (which could be included easily as a feature)
author_association: COLLABORATOR
user: login: codecov[bot]
created_at: 2025-05-19T05:04:54Z
body: ## [Codecov](https://app.codecov.io/gh/henry2004y/TestParticle.jl/pull/236?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Hongyang+Zhou) Report
Attention: Patch coverage is `90.80460%` with `8 lines` in your changes missing coverage. Please review.
> Project coverage is 91.45%. Comparing base [(`1affc4f`)](https://app.codecov.io/gh/henry2004y/TestParticle.jl/commit/1affc4f020a21cd80af4652672f4974e2565a90e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Hongyang+Zhou) to head [(`0bb7a30`)](https://app.codecov.io/gh/henry2004y/TestParticle.jl/commit/0bb7a309e7d4ac22fe7bbfba6f29b00aa4321c9b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Hongyang+Zhou).
> Report is 3 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/henry2004y/TestParticle.jl/pull/236?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Hongyang+Zhou) | Patch % | Lines |
|---|---|---|
| [src/prepare.jl](https://app.codecov.io/gh/henry2004y/TestParticle.jl/pull/236?src=pr&el=tree&filepath=src%2Fprepare.jl&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Hongyang+Zhou#diff-c3JjL3ByZXBhcmUuamw=) | 73.33% | [4 Missing :warning: ](https://app.codecov.io/gh/henry2004y/TestParticle.jl/pull/236?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Hongyang+Zhou) |
| [src/pusher.jl](https://app.codecov.io/gh/henry2004y/TestParticle.jl/pull/236?src=pr&el=tree&filepath=src%2Fpusher.jl&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Hongyang+Zhou#diff-c3JjL3B1c2hlci5qbA==) | 42.85% | [4 Missing :warning: ](https://app.codecov.io/gh/henry2004y/TestParticle.jl/pull/236?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Hongyang+Zhou) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #236      +/-   ##
==========================================
- Coverage   92.35%   91.45%   -0.91%     
==========================================
  Files          10       10              
  Lines         746      714      -32     
==========================================
- Hits          689      653      -36     
- Misses         57       61       +4     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/henry2004y/TestParticle.jl/pull/236?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Hongyang+Zhou).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Hongyang+Zhou).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: Beforerr
created_at: 2025-05-19T05:43:25Z
body: BTW, is there any reason to seperate these two interfaces?

```
	 prepare(grid::CartesianGrid, E, B; kwargs...)
	 prepare(grid::CartesianGrid, E, B, F; species=Proton, q=1.0, m=1.0)
```

Can we just make it `prepare(grid::CartesianGrid, E, B, F=ZeroField(); species=Proton, q=1.0, m=1.0)`
author_association: COLLABORATOR
user: login: henry2004y
created_at: 2025-05-19T13:50:25Z
body: > BTW, is there any reason to seperate these two interfaces?
> 
> ```
> 	 prepare(grid::CartesianGrid, E, B; kwargs...)
> 	 prepare(grid::CartesianGrid, E, B, F; species=Proton, q=1.0, m=1.0)
> ```
> 
> Can we just make it `prepare(grid::CartesianGrid, E, B, F=ZeroField(); species=Proton, q=1.0, m=1.0)`

There is no special reason. As long as the changes cause no conflict with current tests, it's fine.

---

One small suggestion for PRs: you don't necessarily need to force push every time you want to update it. In the end I will use squash so all the individual commits will be merged into one.
author_association: OWNER
user: login: Beforerr
created_at: 2025-05-19T14:03:08Z
body: Sorry about this. Most force push is dealing with updating trace equation in documentation, I thought it might be easier for you to review.
author_association: COLLABORATOR
user: login: henry2004y
created_at: 2025-05-19T14:16:13Z
body: One question here: does this PR contain #237? I am trying to see why that one failed while this one passed.
author_association: OWNER
user: login: Beforerr
created_at: 2025-05-19T14:21:45Z
body: Oh for #237 I do not rebase it to include the force push here.
author_association: COLLABORATOR
user: login: henry2004y
created_at: 2025-05-19T14:28:03Z
body: So I guess the right ordering is to first merge this PR and then #237?
author_association: OWNER
user: login: Beforerr
created_at: 2025-05-19T14:30:29Z
body: Right. After merging this, I would need to rebase #237 .
author_association: COLLABORATOR
user: login: Beforerr
created_at: 2025-05-19T14:46:20Z
body: Sounds good. I tried so hard to avoid auto-formatting, but it still happened wwww:). Maybe we could have something like https://editorconfig.org/ to enlighten VScode?
author_association: COLLABORATOR
user: login: github-actions[bot]
created_at: 2025-05-19T15:10:11Z
body: ### Benchmark Results


|                                            | master             | 0bb7a309e7d4ac...  | master / 0bb7a309e7d4ac... |
|:-------------------------------------------|:------------------:|:------------------:|:--------------------------:|
| trace/GC/in place                          | 10.9 ± 3.8 μs      | 10.6 ± 3.6 μs      | 1.03 ± 0.5                 |
| trace/analytic field/in place              | 14.3 ± 6.3 μs      | 13.9 ± 6 μs        | 1.02 ± 0.63                |
| trace/analytic field/in place relativistic | 15.7 ± 5.8 μs      | 18.2 ± 5.9 μs      | 0.861 ± 0.42               |
| trace/analytic field/out of place          | 10.4 ± 6 μs        | 10.1 ± 5.9 μs      | 1.03 ± 0.84                |
| trace/numerical field/Boris                | 13.2 ± 0.41 μs     | 13.1 ± 0.42 μs     | 1.01 ± 0.045               |
| trace/numerical field/Boris ensemble       | 26.5 ± 0.5 μs      | 26.1 ± 0.37 μs     | 1.02 ± 0.024               |
| trace/numerical field/in place             | 0.0515 ± 0.0076 ms | 0.0523 ± 0.0071 ms | 0.984 ± 0.2                |
| trace/numerical field/out of place         | 0.04 ± 0.0067 ms   | 0.0398 ± 0.0063 ms | 1.01 ± 0.23                |
| trace/time-dependent field/in place        | 22.4 ± 9.7 μs      | 23.1 ± 9.6 μs      | 0.966 ± 0.58               |
| trace/time-dependent field/out of place    | 22.6 ± 8.8 μs      | 22.3 ± 8.2 μs      | 1.02 ± 0.54                |
| time_to_load                               | 2.04 ± 0.013 s     | 2.03 ± 0.0084 s    | 1 ± 0.0075                 |

|                                            | master                  | 0bb7a309e7d4ac...       | master / 0bb7a309e7d4ac... |
|:-------------------------------------------|:-----------------------:|:-----------------------:|:--------------------------:|
| trace/GC/in place                          | 0.198 k allocs: 11.5 kB | 0.198 k allocs: 11.5 kB | 1                          |
| trace/analytic field/in place              | 0.433 k allocs: 20 kB   | 0.433 k allocs: 20 kB   | 0.999                      |
| trace/analytic field/in place relativistic | 0.433 k allocs: 20 kB   | 0.433 k allocs: 20 kB   | 0.999                      |
| trace/analytic field/out of place          | 0.393 k allocs: 18.2 kB | 0.393 k allocs: 18.2 kB | 0.997                      |
| trace/numerical field/Boris                | 26  allocs: 2.5 kB      | 26  allocs: 2.5 kB      | 1                          |
| trace/numerical field/Boris ensemble       | 0.039 k allocs: 4.48 kB | 0.039 k allocs: 4.48 kB | 1                          |
| trace/numerical field/in place             | 0.434 k allocs: 22.3 kB | 0.434 k allocs: 22.3 kB | 1                          |
| trace/numerical field/out of place         | 0.393 k allocs: 19.4 kB | 0.393 k allocs: 19.4 kB | 1                          |
| trace/time-dependent field/in place        | 0.616 k allocs: 27.5 kB | 0.616 k allocs: 27.5 kB | 1                          |
| trace/time-dependent field/out of place    | 0.573 k allocs: 25.5 kB | 0.573 k allocs: 25.5 kB | 1                          |
| time_to_load                               | 0.159 k allocs: 11.2 kB | 0.159 k allocs: 11.2 kB | 1                          |




author_association: CONTRIBUTOR
user: login: Beforerr
created_at: 2025-05-19T15:45:52Z
body: I do not understand the regression, why it only happed to `analytic field - in place`. The typed code indeeds ignore `m` and `Ffunc`.
author_association: COLLABORATOR
user: login: henry2004y
created_at: 2025-05-19T16:01:03Z
body: I am re-triggering the benchmark tests to confirm. However, we do see some more allocations for all the analytic field tracing, which worths attention.
author_association: OWNER
user: login: Beforerr
created_at: 2025-05-19T19:54:16Z
body: Somehow adding `@inbounds` improve the performance by 10% on my local machine, but this does not show up in the CI.
author_association: COLLABORATOR
user: login: henry2004y
created_at: 2025-05-19T20:17:36Z
body: Are the extra allocations in the `trace/analytic field/` tests coming from `m` and `Ffunc` call in `get_dv`?
author_association: OWNER
user: login: Beforerr
created_at: 2025-05-19T20:23:43Z
body: No, the typed code for `@code_typed trace!(dy, stateinit, param_analytic, 0)` totally eliminate `m` and `Ffunc` totally (attached below. I felt like it may come from `ODEProblem`. Where do you see the extra allocation? I could not find it out.

```julia
CodeInfo(
1 ── %1   = $(Expr(:boundscheck, false))::Bool
└───        goto #5 if not %1
2 ── %3   = Base.sub_int(4, 1)::Int64
│    %4   = Base.bitcast(Base.UInt, %3)::UInt64
│    %5   = Base.getfield(y, :size)::Tuple{Int64}
│    %6   = $(Expr(:boundscheck, true))::Bool
│    %7   = Base.getfield(%5, 1, %6)::Int64
│    %8   = Base.bitcast(Base.UInt, %7)::UInt64
│    %9   = Base.ult_int(%4, %8)::Bool
└───        goto #4 if not %9
3 ──        goto #5
4 ── %12  = Core.tuple(4)::Tuple{Int64}
│           invoke Base.throw_boundserror(y::Vector{Float64}, %12::Tuple{Int64})::Union{}
└───        unreachable
5 ┄─ %15  = Base.getfield(y, :ref)::MemoryRef{Float64}
│    %16  = Base.memoryrefnew(%15, 4, false)::MemoryRef{Float64}
│    %17  = Base.memoryrefget(%16, :not_atomic, false)::Float64
└───        goto #6
6 ── %19  = $(Expr(:boundscheck, false))::Bool
└───        goto #10 if not %19
7 ── %21  = Base.sub_int(5, 1)::Int64
│    %22  = Base.bitcast(Base.UInt, %21)::UInt64
│    %23  = Base.getfield(y, :size)::Tuple{Int64}
│    %24  = $(Expr(:boundscheck, true))::Bool
│    %25  = Base.getfield(%23, 1, %24)::Int64
│    %26  = Base.bitcast(Base.UInt, %25)::UInt64
│    %27  = Base.ult_int(%22, %26)::Bool
└───        goto #9 if not %27
8 ──        goto #10
9 ── %30  = Core.tuple(5)::Tuple{Int64}
│           invoke Base.throw_boundserror(y::Vector{Float64}, %30::Tuple{Int64})::Union{}
└───        unreachable
10 ┄ %33  = Base.getfield(y, :ref)::MemoryRef{Float64}
│    %34  = Base.memoryrefnew(%33, 5, false)::MemoryRef{Float64}
│    %35  = Base.memoryrefget(%34, :not_atomic, false)::Float64
└───        goto #11
11 ─ %37  = $(Expr(:boundscheck, false))::Bool
└───        goto #15 if not %37
12 ─ %39  = Base.sub_int(6, 1)::Int64
│    %40  = Base.bitcast(Base.UInt, %39)::UInt64
│    %41  = Base.getfield(y, :size)::Tuple{Int64}
│    %42  = $(Expr(:boundscheck, true))::Bool
│    %43  = Base.getfield(%41, 1, %42)::Int64
│    %44  = Base.bitcast(Base.UInt, %43)::UInt64
│    %45  = Base.ult_int(%40, %44)::Bool
└───        goto #14 if not %45
13 ─        goto #15
14 ─ %48  = Core.tuple(6)::Tuple{Int64}
│           invoke Base.throw_boundserror(y::Vector{Float64}, %48::Tuple{Int64})::Union{}
└───        unreachable
15 ┄ %51  = Base.getfield(y, :ref)::MemoryRef{Float64}
│    %52  = Base.memoryrefnew(%51, 6, false)::MemoryRef{Float64}
│    %53  = Base.memoryrefget(%52, :not_atomic, false)::Float64
└───        goto #16
16 ─ %55  = Core.tuple(%17, %35, %53)::Tuple{Float64, Float64, Float64}
│    %56  = %new(SVector{3, Float64}, %55)::SVector{3, Float64}
└───        goto #17
17 ─ %58  = $(Expr(:boundscheck, false))::Bool
└───        goto #22 if not %58
18 ─ %60  = Base.sub_int(3, 1)::Int64
│    %61  = Base.add_int(1, %60)::Int64
│    %62  = (3 === %61)::Bool
└───        goto #20 if not %62
19 ─        goto #21
20 ─ %65  = Core.tuple(%61)::Tuple{Int64}
│           invoke Base.throw_setindex_mismatch(%56::SVector{3, Float64}, %65::Tuple{Int64})::Union{}
└───        unreachable
21 ─        nothing::Nothing
22 ┄ %69  = $(Expr(:boundscheck, false))::Bool
└───        goto #27 if not %69
23 ─ %71  = Core.tuple($(QuoteNode(1:3)))::Tuple{UnitRange{Int64}}
│    %72  = Base.getfield(dy, :size)::Tuple{Int64}
│    %73  = $(Expr(:boundscheck, true))::Bool
│    %74  = Base.getfield(%72, 1, %73)::Int64
│    %75  = Base.slt_int(3, 1)::Bool
│    %76  = Base.sub_int(1, 1)::Int64
│    %77  = Base.bitcast(UInt64, %76)::UInt64
│    %78  = Base.bitcast(UInt64, %74)::UInt64
│    %79  = Base.ult_int(%77, %78)::Bool
│    %80  = Base.sub_int(3, 1)::Int64
│    %81  = Base.bitcast(UInt64, %80)::UInt64
│    %82  = Base.bitcast(UInt64, %74)::UInt64
│    %83  = Base.ult_int(%81, %82)::Bool
│    %84  = Base.and_int(%79, %83)::Bool
│    %85  = Base.or_int(%75, %84)::Bool
└───        goto #25 if not %85
24 ─        goto #26
25 ─        invoke Base.throw_boundserror(dy::Vector{Float64}, %71::Tuple{UnitRange{Int64}})::Union{}
└───        unreachable
26 ─        nothing::Nothing
27 ┄ %91  = Base.slt_int(3, 1)::Bool
└───        goto #29 if not %91
28 ─        goto #30
29 ─        goto #30
30 ┄ %95  = φ (#28 => true, #29 => false)::Bool
│    %96  = φ (#29 => 1)::Int64
│    %97  = φ (#29 => 1)::Int64
│    %98  = Base.not_int(%95)::Bool
└───        goto #41 if not %98
31 ┄ %100 = φ (#30 => %96, #40 => %129)::Int64
│    %101 = φ (#30 => %97, #40 => %130)::Int64
│    %102 = φ (#30 => 1, #40 => %123)::Int64
│    %103 = $(Expr(:boundscheck, false))::Bool
│    %104 = Base.getfield(%55, %102, %103)::Float64
│    %105 = $(Expr(:boundscheck, false))::Bool
└───        goto #35 if not %105
32 ─ %107 = Base.sub_int(%100, 1)::Int64
│    %108 = Base.bitcast(UInt64, %107)::UInt64
│    %109 = Base.getfield(dy, :size)::Tuple{Int64}
│    %110 = $(Expr(:boundscheck, true))::Bool
│    %111 = Base.getfield(%109, 1, %110)::Int64
│    %112 = Base.bitcast(UInt64, %111)::UInt64
│    %113 = Base.ult_int(%108, %112)::Bool
└───        goto #34 if not %113
33 ─        goto #35
34 ─ %116 = Core.tuple(%100)::Tuple{Int64}
│           invoke Base.throw_boundserror(dy::Vector{Float64}, %116::Tuple{Int64})::Union{}
└───        unreachable
35 ┄ %119 = Base.getfield(dy, :ref)::MemoryRef{Float64}
│    %120 = Base.memoryrefnew(%119, %100, false)::MemoryRef{Float64}
│           Base.memoryrefset!(%120, %104, :not_atomic, false)::Float64
└───        goto #36
36 ─ %123 = Base.add_int(%102, 1)::Int64
│    %124 = (%101 === 3)::Bool
└───        goto #38 if not %124
37 ─        goto #39
38 ─ %127 = Base.add_int(%101, 1)::Int64
└───        goto #39
39 ┄ %129 = φ (#38 => %127)::Int64
│    %130 = φ (#38 => %127)::Int64
│    %131 = φ (#37 => true, #38 => false)::Bool
│    %132 = Base.not_int(%131)::Bool
└───        goto #41 if not %132
40 ─        goto #31
41 ┄        goto #42
42 ─ %136 = Base.getfield(p, 1)::Float64
│    %137 = Base.mul_float(%35, 1.0e-8)::Float64
│    %138 = Base.mul_float(%53, 0.0)::Float64
│    %139 = Base.sub_float(%137, %138)::Float64
│    %140 = Base.mul_float(%53, 0.0)::Float64
│    %141 = Base.mul_float(%17, 1.0e-8)::Float64
│    %142 = Base.sub_float(%140, %141)::Float64
│    %143 = Base.mul_float(%17, 0.0)::Float64
│    %144 = Base.mul_float(%35, 0.0)::Float64
│    %145 = Base.sub_float(%143, %144)::Float64
│    %146 = Base.add_float(%139, 5.0e-10)::Float64
│    %147 = Base.add_float(%142, 5.0e-10)::Float64
│    %148 = Base.add_float(%145, 0.0)::Float64
│    %149 = Base.mul_float(%136, %146)::Float64
│    %150 = Base.mul_float(%136, %147)::Float64
│    %151 = Base.mul_float(%136, %148)::Float64
│    %152 = StaticArrays.tuple(%149, %150, %151)::Tuple{Float64, Float64, Float64}
│    %153 = %new(SVector{3, Float64}, %152)::SVector{3, Float64}
│    %154 = $(Expr(:boundscheck, false))::Bool
└───        goto #47 if not %154
43 ─ %156 = Base.sub_int(6, 4)::Int64
│    %157 = Base.add_int(1, %156)::Int64
│    %158 = (3 === %157)::Bool
└───        goto #45 if not %158
44 ─        goto #46
45 ─ %161 = Core.tuple(%157)::Tuple{Int64}
│           invoke Base.throw_setindex_mismatch(%153::SVector{3, Float64}, %161::Tuple{Int64})::Union{}
└───        unreachable
46 ─        nothing::Nothing
47 ┄ %165 = $(Expr(:boundscheck, false))::Bool
└───        goto #52 if not %165
48 ─ %167 = Core.tuple($(QuoteNode(4:6)))::Tuple{UnitRange{Int64}}
│    %168 = Base.getfield(dy, :size)::Tuple{Int64}
│    %169 = $(Expr(:boundscheck, true))::Bool
│    %170 = Base.getfield(%168, 1, %169)::Int64
│    %171 = Base.slt_int(6, 4)::Bool
│    %172 = Base.sub_int(4, 1)::Int64
│    %173 = Base.bitcast(UInt64, %172)::UInt64
│    %174 = Base.bitcast(UInt64, %170)::UInt64
│    %175 = Base.ult_int(%173, %174)::Bool
│    %176 = Base.sub_int(6, 1)::Int64
│    %177 = Base.bitcast(UInt64, %176)::UInt64
│    %178 = Base.bitcast(UInt64, %170)::UInt64
│    %179 = Base.ult_int(%177, %178)::Bool
│    %180 = Base.and_int(%175, %179)::Bool
│    %181 = Base.or_int(%171, %180)::Bool
└───        goto #50 if not %181
49 ─        goto #51
50 ─        invoke Base.throw_boundserror(dy::Vector{Float64}, %167::Tuple{UnitRange{Int64}})::Union{}
└───        unreachable
51 ─        nothing::Nothing
52 ┄ %187 = Base.slt_int(6, 4)::Bool
└───        goto #54 if not %187
53 ─        goto #55
54 ─        goto #55
55 ┄ %191 = φ (#53 => true, #54 => false)::Bool
│    %192 = φ (#54 => 4)::Int64
│    %193 = φ (#54 => 4)::Int64
│    %194 = Base.not_int(%191)::Bool
└───        goto #66 if not %194
56 ┄ %196 = φ (#55 => %192, #65 => %225)::Int64
│    %197 = φ (#55 => %193, #65 => %226)::Int64
│    %198 = φ (#55 => 1, #65 => %219)::Int64
│    %199 = $(Expr(:boundscheck, false))::Bool
│    %200 = Base.getfield(%152, %198, %199)::Float64
│    %201 = $(Expr(:boundscheck, false))::Bool
└───        goto #60 if not %201
57 ─ %203 = Base.sub_int(%196, 1)::Int64
│    %204 = Base.bitcast(UInt64, %203)::UInt64
│    %205 = Base.getfield(dy, :size)::Tuple{Int64}
│    %206 = $(Expr(:boundscheck, true))::Bool
│    %207 = Base.getfield(%205, 1, %206)::Int64
│    %208 = Base.bitcast(UInt64, %207)::UInt64
│    %209 = Base.ult_int(%204, %208)::Bool
└───        goto #59 if not %209
58 ─        goto #60
59 ─ %212 = Core.tuple(%196)::Tuple{Int64}
│           invoke Base.throw_boundserror(dy::Vector{Float64}, %212::Tuple{Int64})::Union{}
└───        unreachable
60 ┄ %215 = Base.getfield(dy, :ref)::MemoryRef{Float64}
│    %216 = Base.memoryrefnew(%215, %196, false)::MemoryRef{Float64}
│           Base.memoryrefset!(%216, %200, :not_atomic, false)::Float64
└───        goto #61
61 ─ %219 = Base.add_int(%198, 1)::Int64
│    %220 = (%197 === 6)::Bool
└───        goto #63 if not %220
62 ─        goto #64
63 ─ %223 = Base.add_int(%197, 1)::Int64
└───        goto #64
64 ┄ %225 = φ (#63 => %223)::Int64
│    %226 = φ (#63 => %223)::Int64
│    %227 = φ (#62 => true, #63 => false)::Bool
│    %228 = Base.not_int(%227)::Bool
└───        goto #66 if not %228
65 ─        goto #56
66 ┄        goto #67
67 ─        return TestParticle.nothing
) => Nothing
```
author_association: COLLABORATOR
user: login: henry2004y
created_at: 2025-05-19T20:33:08Z
body: > No, the typed code for `@code_typed trace!(dy, stateinit, param_analytic, 0)` totally eliminate `m` and `Ffunc` totally (attached below. I felt like it may come from `ODEProblem`. Where do you see the extra allocation? I could not find it out.

The third column of the second benchmark table reports the ratios of memory allocations. It's close to 1, but I guess 0.999 means that's not exactly the same, and the PR allocates a bit more. I haven't run this on my local machine though.

author_association: OWNER
user: login: Beforerr
created_at: 2025-05-19T21:56:15Z
body: Interestingly my local test does not show regression for `trace/analytic field/in place`. And I could not track down the allocation to any modified `trace`.

|                                            | 772d63adcb52d4...  | master             | 772d63adcb52d4... / master |
|:-------------------------------------------|:------------------:|:------------------:|:--------------------------:|
| trace/GC/in place                          | 6.49 ± 0.64 μs     | 7.1 ± 0.62 μs      | 0.915 ± 0.12               |
| trace/analytic field/in place              | 14.1 ± 1.4 μs      | 13.5 ± 1.2 μs      | 1.05 ± 0.14                |
| trace/analytic field/in place relativistic | 16.6 ± 1.3 μs      | 16.9 ± 1.2 μs      | 0.983 ± 0.1                |
| trace/analytic field/out of place          | 9.1 ± 1 μs         | 8.97 ± 1.1 μs      | 1.01 ± 0.17                |
| trace/numerical field/Boris                | 9.96 ± 0.46 μs     | 9.79 ± 0.38 μs     | 1.02 ± 0.061               |
| trace/numerical field/Boris ensemble       | 19.7 ± 0.96 μs     | 19.5 ± 0.54 μs     | 1.01 ± 0.057               |
| trace/numerical field/in place             | 0.0335 ± 0.0018 ms | 0.0327 ± 0.0017 ms | 1.02 ± 0.077               |
| trace/numerical field/out of place         | 27 ± 1.8 μs        | 26.1 ± 1.5 μs      | 1.04 ± 0.091               |
| trace/time-dependent field/in place        | 24 ± 2.1 μs        | 23.5 ± 2 μs        | 1.02 ± 0.13                |
| trace/time-dependent field/out of place    | 15.7 ± 1.4 μs      | 15.7 ± 1.6 μs      | 1 ± 0.14                   |
| time_to_load                               | 1.71 ± 0.15 s      | 1.51 ± 0.028 s     | 1.13 ± 0.1                 |
author_association: COLLABORATOR
user: login: henry2004y
created_at: 2025-05-20T00:14:11Z
body: On my local machine (Windows):

```cmd
D:\Computer\TestParticle.jl>benchpkgtable.cmd TestParticle --rev="master,dirty" --ratio --mode time,memory
[ Info: Loading results from .\results_TestParticle@master.json
[ Info: Flattening results.
[ Info: Loading results from .\results_TestParticle@dirty.json
[ Info: Flattening results.
|                                            | master             | dirty              | master / dirty |
|:-------------------------------------------|:------------------:|:------------------:|:--------------:|
| trace/GC/in place                          | 7.1 ± 2.8 μs       | 7.2 ± 2.9 μs       | 0.986 ± 0.56   |
| trace/analytic field/in place              | 14.1 ± 6 μs        | 12.7 ± 6 μs        | 1.11 ± 0.71    |
| trace/analytic field/in place relativistic | 15.2 ± 6 μs        | 14.8 ± 5.2 μs      | 1.03 ± 0.54    |
| trace/analytic field/out of place          | 12 ± 5.8 μs        | 11.8 ± 5.3 μs      | 1.02 ± 0.67    |
| trace/numerical field/Boris                | 14 ± 1.2 μs        | 14.2 ± 1.3 μs      | 0.986 ± 0.12   |
| trace/numerical field/Boris ensemble       | 29.4 ± 2.4 μs      | 29.7 ± 2.5 μs      | 0.99 ± 0.12    |
| trace/numerical field/in place             | 0.0454 ± 0.0079 ms | 0.0473 ± 0.0075 ms | 0.96 ± 0.23    |
| trace/numerical field/out of place         | 0.0321 ± 0.0058 ms | 0.0319 ± 0.0049 ms | 1.01 ± 0.24    |
| trace/time-dependent field/in place        | 23.1 ± 4.5 μs      | 22.3 ± 4.4 μs      | 1.04 ± 0.29    |
| trace/time-dependent field/out of place    | 16.8 ± 8.4 μs      | 15.6 ± 8.4 μs      | 1.08 ± 0.79    |
| time_to_load                               | 3.75 ± 0.0069 s    | 3.75 ± 0.045 s     | 1 ± 0.012      |

|                                            | master                  | dirty                   | master / dirty |
|:-------------------------------------------|:-----------------------:|:-----------------------:|:--------------:|
| trace/GC/in place                          | 0.198 k allocs: 11.5 kB | 0.198 k allocs: 11.5 kB | 1              |
| trace/analytic field/in place              | 0.433 k allocs: 20 kB   | 0.433 k allocs: 20 kB   | 0.999          |
| trace/analytic field/in place relativistic | 0.433 k allocs: 20 kB   | 0.433 k allocs: 20 kB   | 0.999          |
| trace/analytic field/out of place          | 0.393 k allocs: 18.2 kB | 0.393 k allocs: 18.2 kB | 0.997          |
| trace/numerical field/Boris                | 26  allocs: 2.5 kB      | 26  allocs: 2.5 kB      | 1              |
| trace/numerical field/Boris ensemble       | 0.039 k allocs: 4.48 kB | 0.039 k allocs: 4.48 kB | 1              |
| trace/numerical field/in place             | 0.434 k allocs: 22.3 kB | 0.434 k allocs: 22.3 kB | 1              |
| trace/numerical field/out of place         | 0.393 k allocs: 19.4 kB | 0.393 k allocs: 19.4 kB | 1              |
| trace/time-dependent field/in place        | 0.616 k allocs: 27.5 kB | 0.616 k allocs: 27.5 kB | 1              |
| trace/time-dependent field/out of place    | 0.573 k allocs: 25.5 kB | 0.573 k allocs: 25.5 kB | 1              |
| time_to_load                               | 0.502 k allocs: 26.8 kB | 0.502 k allocs: 26.8 kB | 1              |
```

It seems that there is no time regression, and almost negligible allocation regression.
author_association: OWNER