Skip to content

Features Walkthrough ​

TestParticle.jl provides a range of features to simplify and enhance your particle tracing simulations. This section provides a walkthrough of these capabilities with links to specific demos.

Summary of Features ​

The table below summarizes the key features and tracing capabilities in TestParticle.jl:

CategoryCapabilityDescriptionReference
Solvers & DynamicsBoris PusherSymplectic energy-conserving pushers (Standard, Multistep, Hyper, Adaptive)Tutorial, Demo
SciML SolversHigh-order adaptive and symplectic ODE solvers from OrdinaryDiffEq.jlNumerical Methods
Guiding Center (GC)Reduced-order drift dynamics averaging over gyromotionGuiding Center, Guiding Center Approximation
Hybrid SolverAdaptive runtime switching between full orbit and guiding centerHybrid GC-FO Solver
Canonical FormRelativistic/non-relativistic canonical (x,p) phase space with auto-diffCanonical Tracing Form
Fieldline TracingTraces magnetic field lines in 3D fieldsTracing Magnetic Field Lines
Relativistic DynamicsRelativistic equations of motion for high-energy particlesRelativistic vs Non-relativistic Tracing
Backward TracingReverse-time integration for source identificationBackwards in Time
Callbacks & Boundaryisoutside PredicateDomain boundary check with step rollback (CPU & GPU)Boundary Checks
TerminateOutsideSciML discrete callback wrapper for boundary terminationBoundary Checks
SciML CallbacksContinuous/discrete event handling and step size limitersNumerical Methods
Parallel & HardwareMultithreadingCPU shared-memory parallelism (EnsembleThreads)Multithreaded Parallelization
Distributed & HybridMulti-process and multi-node clusters (EnsembleDistributed, EnsembleSplitThreads)Distributed Parallelization
Native GPU KernelsBackend-agnostic GPU execution (CUDA, AMDGPU, oneAPI, Metal)GPU Ensemble Tracing
Output & Diagnosticssave_fields & save_workInline trajectory logging of fields (E,B) and work ratesExtra Diagnostics
saveat & Dense OutputExplicit output times and linear step interpolationBoris Pusher
raw_outputHigh-throughput dense array output bypassing solution wrapping on GPUGPU Ensemble Tracing
Fields & UnitsGridded Fields1D/2D/3D regular, rectilinear, and structured grid interpolationField Interpolation
Spherical Device GridsGPU-optimized trilinear spherical grid interpolator (GPUSphericalGrid)GPU Ensemble Tracing
Dimensionless UnitsNormalized scaling by gyrofrequency, gyroradius, etc.Dimensionless Units
Unitful SupportPhysical units tracking via Unitful.jlUnitful Integration
Velocity DistributionsParticle sampling (Maxwellian, Kappa) from distribution functionsVelocity Sampling

Unit Conversions and Scaling ​

By default, SI units are used. However, you can define your own unit systems by setting reference scales for mass, charge, magnetic field, length, and time.

Tracing Backwards in Time ​

Tracing a particle backwards is as simple as setting a time span where the end time is less than the start time, e.g., tspan = (0.0, -1.0). For fixed time step methods, you also need to set dt to be negative (e.g. standard Boris pusher). This is particularly useful for source identification in plasma simulations.

Callbacks and Boundary Checks ​

In particle tracing simulations, it is frequently necessary to stop integration when a particle leaves the simulation box, collides with a boundary or body, or encounters a specific physical condition. TestParticle.jl provides two primary mechanisms for handling boundary checks and callbacks:

1. The isoutside Predicate ​

Used by the Boris family (both host CPU and native GPU KernelAbstractions backends), Guiding Center (TraceGCProblem), and Hybrid (TraceHybridProblem) solvers.

  • Signature: isoutside(u, p, t) -> Bool, where u is the phase-space state vector, p is the parameter container, and t is current time.

  • Behavior: When isoutside evaluates to true, the solver rolls back the step that crossed the boundary so the trajectory ends at the last state inside the domain, and marks the solution with ReturnCode.Terminated.

  • GPU Portability: On device backends (e.g., CUDA.CUDABackend()), isoutside is passed as an argument to the device kernel and executed directly on the GPU. It must therefore be an isbits function that avoids dynamic allocations.

  • High-throughput ensembles: In raw_output = true mode, the terminated boolean array marks which particles exited early, and unused time slots repeat the final inside state.

julia
using TestParticle, StaticArrays

# Stop once the particle exits the region |x| <= 10 m
isoutside(u, p, t) = abs(u[1]) > 10.0

# Native Boris solver on CPU
sol = solve(prob, Boris(); dt = 1.0e-9, isoutside)

# Native Boris solver on GPU
# using CUDA
# sol = solve(prob, Boris(), CUDABackend(); dt = 1.0e-9, trajectories = 100, isoutside)

2. TerminateOutside and SciML Callbacks ​

Used by standard SciML ODE solvers (OrdinaryDiffEq.jl, e.g., Tsit5(), Vern9()).

  • TerminateOutside(isoutside): Convenience wrapper that constructs a SciML DiscreteCallback triggering terminate!(integrator) when isoutside(u, p, t) holds.

  • SciML Callback Interface: Standard ContinuousCallback, DiscreteCallback, or CallbackSet can be passed via callback = ... for generic event handling, root-finding, or enforcing maximum step size limits based on gyroperiods.

julia
using TestParticle, OrdinaryDiffEq

# Domain termination for SciML ODE solvers
isoutside(u, p, t) = hypot(u[1], u[2], u[3]) > 10.0
callback = TerminateOutside(isoutside)

sol = solve(prob, Tsit5(); callback)

Comparison Matrix ​

FeatureSolversHardwareBehavior
isoutsideBoris(), MultistepBoris, GC, HybridCPU & GPUStep rollback to boundary; sets ReturnCode.Terminated
TerminateOutsideSciML ODE solvers (Tsit5, Vern9, etc.)CPUHalts integrator via terminate!
SciML CallbacksSciML ODE solversCPUArbitrary event handling, root-finding, stepsize limiters

For detailed tutorials and examples, see:

Multiple Particles (Ensemble Simulations) ​

Tracing multiple particles simultaneously is efficiently handled through ensemble simulations. TestParticle.jl supports several parallelization strategies to leverage available hardware (following Ensemble Algorithms):

  • EnsembleSerial: Sequential execution on a single core.

  • EnsembleThreads: Shared-memory parallelization using multiple threads. This is the most efficient choice for local multi-core machines.

  • EnsembleDistributed: Distributed-memory parallelization across multiple processes or nodes. Useful for large-scale simulations on a cluster.

  • EnsembleSplitThreads: A hybrid strategy that combines distributed processes with multi-threading on each process. This is ideal for HPC clusters where you want to use multiple nodes and all cores within each node.

Work Distribution (batch_size) ​

When using distributed parallelization (EnsembleDistributed or EnsembleSplitThreads), you can control the granularity of task distribution using the batch_size keyword argument in solve. This parameter determines how many trajectories are sent to a worker in a single chunk.

By default, batch_size is automatically optimized for the current environment:

  • For distributed solvers, it defaults to max(1, trajectories ÷ nworkers()).

  • For serial and threaded solvers, it defaults to 1.

julia
# Explicitly setting batch_size for distributed simulation
sols = solve(prob, EnsembleSplitThreads(); trajectories=10000, batch_size=1000)

Efficiency Considerations ​

When tracing in numeric fields, memory efficiency is critical. By default, ensure that large field data is shared as a reference rather than being copied for each trajectory.

Warning

When defining a prob_func for an EnsembleProblem, avoid in-place mutation of the ODEProblem (e.g., prob.u0 = ...), especially when using StaticArrays. SciMLBase v2.0+ may issue a warning about "Mutation of ODEProblem detected". Instead, use remake(prob; u0=...) to create a new problem instance with updated parameters.

Parallelization ​

Tracing many independent particles is an embarrassingly parallel workload: each trajectory is computed without communication with the others. TestParticle.jl exposes the same ensemble parallelization interface for both the SciML solvers (via OrdinaryDiffEq) and the native Boris Method, so switching from a serial run to a parallel one only requires changing a single argument.

AlgorithmDescription
EnsembleSerial()Trajectories are solved one after another (no parallelism).
EnsembleThreads()Trajectories are split across the available Julia threads within a single process.
EnsembleDistributed()Trajectories are distributed across worker processes with pmap (multiple machines / NUMA nodes).
EnsembleSplitThreads()Hybrid: work is first split across worker processes, then multithreaded within each process.

The two execution models differ enough that they are documented separately:

  • Multithreaded Parallelization (EnsembleSerial / EnsembleThreads) runs entirely inside one Julia session — no extra setup is needed, and the achievable speedup depends on Threads.nthreads().

  • Distributed Parallelization (EnsembleDistributed / EnsembleSplitThreads) requires adding worker processes up front and loading the required packages on every worker, but scales beyond a single machine.

For GPU-based massively parallel tracing, see the GPU Ensemble Tracing example.

Boris Pusher ​

The native Boris pusher is optimized for performance and energy conservation in magnetic fields. TestParticle.jl provides a whole family of Boris solvers — standard, Multistep, Hyper, and Adaptive — described in Boris Pusher. MultistepBoris6(; n = 4) is the recommended default for most work.

Numerical Field Interpolations ​

TestParticle.jl supports tracing in numerical fields defined on various grids (Cartesian, Rectilinear, Structured).

Real-world Applications ​

Explore how TestParticle.jl can be applied to complex physical systems:

More Demos ​

For a full list of capabilities, browse the Examples section in the sidebar: