Skip to content

Commit 7e32b65

Browse files
authored
Merge pull request #4 from budjensen/lee/integrators
Add 1D3V Boris integrator and gyromotion test
2 parents 9432ab8 + 07201dd commit 7e32b65

File tree

2 files changed

+154
-0
lines changed

2 files changed

+154
-0
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"""
2+
integrators.py
3+
4+
Boris integrator for 1D spatial / 3D velocity (1D3V) particle motion.
5+
6+
Particles move along z but have full 3D velocity.
7+
The electric and magnetic fields depend only on z.
8+
9+
This module advances particle positions and velocities according to the
10+
Lorentz force law:
11+
12+
m dv/dt = q (E + v x B)
13+
dz/dt = v_z
14+
15+
The Boris algorithm is a second-order, time-centered scheme that splits the
16+
electric and magnetic effects into separate half steps:
17+
- Half electric acceleration
18+
- Magnetic rotation
19+
- Second half electric acceleration
20+
21+
References:
22+
J. P. Boris, "Relativistic Plasma Simulation—Optimization of a Hybrid Code",
23+
Proceedings of the Fourth Conference on Numerical Simulation of Plasmas, 1970.
24+
Qin, H. et al. (2013). "Why is Boris algorithm so good?"
25+
"""
26+
27+
from __future__ import annotations
28+
29+
import numpy as np
30+
31+
32+
def boris_1d3v_z(
33+
z: np.ndarray,
34+
v: np.ndarray,
35+
q: np.ndarray,
36+
m: np.ndarray,
37+
E_func,
38+
B_func,
39+
dt: float,
40+
n_steps: int,
41+
):
42+
"""
43+
Advance 1D-in-z, 3D-in-velocity particles using the Boris algorithm.
44+
45+
Args:
46+
z (np.ndarray): Particle positions along z [m], shape (N,)
47+
v (np.ndarray): Particle velocities [m/s], shape (N, 3)
48+
q (np.ndarray): Charges [C], shape (N,)
49+
m (np.ndarray): Masses [kg], shape (N,)
50+
E_func (callable): E(z) -> (N, 3) electric field [V/m]
51+
B_func (callable): B(z) -> (N, 3) magnetic field [T]
52+
dt (float): Time step [s]
53+
n_steps (int): Number of steps
54+
55+
Returns:
56+
tuple[np.ndarray, np.ndarray]: Final (z, v)
57+
"""
58+
q_over_m = (q / m)[:, None]
59+
60+
for _ in range(n_steps):
61+
# 1. Fields at current positions
62+
E = E_func(z)
63+
B = B_func(z)
64+
65+
# 2. Half-step electric acceleration
66+
v_minus = v + 0.5 * dt * q_over_m * E
67+
68+
# 3. Magnetic rotation
69+
t = q_over_m * B * (0.5 * dt)
70+
t_mag2 = np.sum(t * t, axis=1, keepdims=True)
71+
s = 2 * t / (1 + t_mag2)
72+
73+
v_prime = v_minus + np.cross(v_minus, t)
74+
v_plus = v_minus + np.cross(v_prime, s)
75+
76+
# 4. Second half-step electric acceleration
77+
v = v_plus + 0.5 * dt * q_over_m * E
78+
79+
# 5. Position update using vz
80+
z += v[:, 2] * dt
81+
82+
return z, v

tests/test_gyromotion.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""
2+
test_gyromotion.py
3+
4+
Test that the Boris 1D3V integrator produces correct gyromotion in a uniform B field.
5+
6+
Physical expectation:
7+
---------------------
8+
In a uniform magnetic field (B = Bz * e_z) and zero electric field,
9+
a charged particle with initial velocity perpendicular to B
10+
undergoes circular motion with constant |v|.
11+
12+
The cyclotron frequency is:
13+
ω_c = |q| * B / m
14+
15+
After one cyclotron period:
16+
T = 2π / ω_c
17+
the velocity vector should return to its initial direction (within numerical tolerance).
18+
"""
19+
20+
from __future__ import annotations
21+
22+
import numpy as np
23+
24+
from test_particle_sim_1d.integrators import boris_1d3v_z
25+
26+
27+
def test_gyromotion_conservation():
28+
# --- Physical constants ---
29+
q = np.array([-1.602e-19]) # electron charge [C]
30+
m = np.array([9.109e-31]) # electron mass [kg]
31+
B0 = 1.0 # magnetic field [T]
32+
E0 = 0.0 # no electric field
33+
34+
# --- Initial conditions ---
35+
z = np.zeros(1)
36+
v0 = np.array([[1e6, 0.0, 0.0]]) # velocity purely in x (perpendicular to Bz)
37+
38+
# --- Field definitions ---
39+
def E_field(z):
40+
return np.stack(
41+
[np.full_like(z, E0), np.full_like(z, 0.0), np.full_like(z, 0.0)], axis=1
42+
)
43+
44+
def B_field(z):
45+
return np.stack(
46+
[np.full_like(z, 0.0), np.full_like(z, 0.0), np.full_like(z, B0)], axis=1
47+
)
48+
49+
# --- Cyclotron frequency and time step ---
50+
omega_c = np.abs(q[0]) * B0 / m[0] # cyclotron frequency (rad/s)
51+
T_c = 2 * np.pi / omega_c # cyclotron period (s)
52+
n_steps = 1000 # number of integration steps to use for one full gyro orbit
53+
dt = T_c / n_steps # time step so that one period is divided into n_steps steps
54+
55+
# --- Run one full gyro orbit ---
56+
_z_final, v_final = boris_1d3v_z(z, v0.copy(), q, m, E_field, B_field, dt, n_steps)
57+
58+
# --- Expected: velocity magnitude conserved ---
59+
v_mag_initial = np.linalg.norm(v0)
60+
v_mag_final = np.linalg.norm(v_final)
61+
np.testing.assert_allclose(v_mag_final, v_mag_initial, rtol=1e-6)
62+
63+
# --- Expected: velocity returns to (vx, vy) ≈ (v0, 0) after one full period ---
64+
np.testing.assert_allclose(
65+
v_final[0, 0], v0[0, 0], rtol=1e-4
66+
) # vx close to initial
67+
np.testing.assert_allclose(v_final[0, 1], 0.0, atol=1e-4 * v_mag_initial) # vy ≈ 0
68+
np.testing.assert_allclose(v_final[0, 2], 0.0, atol=1e-12) # vz stays constant
69+
70+
71+
if __name__ == "__main__":
72+
test_gyromotion_conservation()

0 commit comments

Comments
 (0)