Skip to content
57 changes: 53 additions & 4 deletions Examples/Tests/particle_boundary_interaction/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@

import sys

sys.path.append("../../../Tools/Parser/")

import numpy as np
import yt
from input_file_parser import parse_input_file
from openpmd_viewer import OpenPMDTimeSeries
from scipy.constants import c

yt.funcs.mylog.setLevel(0)

Expand All @@ -22,19 +26,64 @@
it = ts.iterations
x, y, z = ts.get_particle(["x", "y", "z"], species="electrons", iteration=it[-1])

# Analytical results calculated
# Analytical results
# ------------------
# Parameters read from warpx_used_inputs:
# the sphere (embedded boundary) is centered at the origin and has radius R;
# a single electron starts at (x0, 0, z0) with relativistic proper velocity
# (gamma*v) (ux0, 0, uz0). Since uy0 = 0 and y0 = 0, the motion is confined
# to the (x, z) plane.
# Note: WarpX stores proper velocities normalized by c in warpx_used_inputs.
input_dict = parse_input_file("./warpx_used_inputs")
R = float(input_dict["my_constants.radius"][0])
x0 = float(input_dict["electrons.multiple_particles_pos_x"][0])
z0 = float(input_dict["electrons.multiple_particles_pos_z"][0])
ux0 = float(input_dict["electrons.multiple_particles_ux"][0]) * c
uz0 = float(input_dict["electrons.multiple_particles_uz"][0]) * c

# The electron is relativistic, so we first compute the Lorentz factor and
# the corresponding (constant) 3-velocity from the proper velocity.
gamma = np.sqrt(1.0 + (ux0**2 + uz0**2) / c**2)
vx0, vz0 = ux0 / gamma, uz0 / gamma

# Step 1: find the time at which the electron hits the sphere.
# Before the impact, the trajectory is a straight line:
# (x(t), z(t)) = (x0 + vx0*t, z0 + vz0*t).
# The impact condition x(t)^2 + z(t)^2 = R^2 yields the quadratic
# (vx0^2 + vz0^2) * t^2 + 2*(x0*vx0 + z0*vz0) * t + (x0^2 + z0^2 - R^2) = 0.
# The first impact corresponds to the smaller root.
a_q = vx0**2 + vz0**2
b_q = 2.0 * (x0 * vx0 + z0 * vz0)
c_q = x0**2 + z0**2 - R**2
t_impact = (-b_q - np.sqrt(b_q**2 - 4 * a_q * c_q)) / (2 * a_q)

# Position of the point of impact on the sphere.
x_impact = x0 + vx0 * t_impact
z_impact = z0 + vz0 * t_impact

# Step 2: mirror-reflect the velocity at the impact point.
# The outward normal to the sphere at the impact point is r_impact / R.
# A mirror reflection gives v_reflected = v - 2 (v . n) n.
nx, nz = x_impact / R, z_impact / R
v_dot_n = vx0 * nx + vz0 * nz
vx_r = vx0 - 2 * v_dot_n * nx
vz_r = vz0 - 2 * v_dot_n * nz

x_analytic = 0.03307855709632465
# Step 3: propagate the reflected electron in a straight line until the end
# of the simulation. The remaining time is taken from the diagnostics so that
# the formula automatically adapts if the simulation duration changes.
t_remain = ts.t[-1] - t_impact
x_analytic = x_impact + vx_r * t_remain
y_analytic = 0.0
z_analytic = -0.2029948949556386
z_analytic = z_impact + vz_r * t_remain

print("NUMERICAL coordinates of the point of contact:")
print(f"x={x[0]:5.5f}, y={y[0]:5.5f}, z={z[0]:5.5f}")
print("\n")
print("ANALYTICAL coordinates of the point of contact:")
print(f"x={x_analytic:5.5f}, y={y_analytic:5.5f}, z={z_analytic:5.5f}")

tolerance = 1e-5
tolerance = 0.02

rel_err_x = np.abs((x[0] - x_analytic) / x_analytic)
rel_err_z = np.abs((z[0] - z_analytic) / z_analytic)
Expand Down
116 changes: 94 additions & 22 deletions Examples/Tests/secondary_ion_emission/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@

import sys

sys.path.append("../../../Tools/Parser/")

import numpy as np
import yt
from input_file_parser import parse_input_file
from openpmd_viewer import OpenPMDTimeSeries
from scipy.constants import c

yt.funcs.mylog.setLevel(0)

Expand All @@ -21,38 +25,106 @@
ts = OpenPMDTimeSeries(filename)

it = ts.iterations
x, y, z = ts.get_particle(["x", "y", "z"], species="electrons", iteration=it[-1])

x_analytic = [-0.091696, 0.011599]
y_analytic = [-0.002282, -0.0111624]
z_analytic = [-0.200242, -0.201728]
x, y, z, ux_e, uy_e, uz_e = ts.get_particle(
["x", "y", "z", "ux", "uy", "uz"], species="electrons", iteration=it[-1]
)

N_sec_e = np.size(z) # number of the secondary electrons

assert N_sec_e == 2, (
"Test did not pass: for this set up we expect 2 secondary electrons emitted"
)

tolerance = 1e-3
# Analytical results
# ------------------
# Parameters read from warpx_used_inputs:
# the sphere (embedded boundary) is centered at the origin and has radius R;
# four ions start at (xi, 0, zi) with proper velocity (gamma*v) (uxi, 0, uzi).
# Since uy0 = 0 and yi = 0, each ion travels in the (x, z) plane.
# When an ion reaches the sphere, it can emit a secondary electron at the
# impact point. The emitted electron then receives a small thermal kick
# (random direction, magnitude ~ sqrt(k Te / m_e)) and is propagated for
# the remaining fraction of the time step. Therefore the deterministic
# (analytical) part of the emitted electron position is the impact point
# of the ion on the sphere; the residual displacement comes from the
# thermal kick and from the embedded-boundary discretization.
# Note: WarpX stores proper velocities normalized by c in warpx_used_inputs.
input_dict = parse_input_file("./warpx_used_inputs")
R = float(input_dict["my_constants.radius"][0])
ion_x0 = np.array([float(v) for v in input_dict["ions.multiple_particles_pos_x"]])
ion_z0 = np.array([float(v) for v in input_dict["ions.multiple_particles_pos_z"]])
ion_ux0 = np.array([float(v) for v in input_dict["ions.multiple_particles_ux"]]) * c
ion_uz0 = np.array([float(v) for v in input_dict["ions.multiple_particles_uz"]]) * c

# The ions are non-relativistic (gamma ~ 1), but use the general formula
# to convert the proper velocity to the 3-velocity for consistency.
gamma = np.sqrt(1.0 + (ion_ux0**2 + ion_uz0**2) / c**2)
vx0 = ion_ux0 / gamma
vz0 = ion_uz0 / gamma

# For each ion, find the time at which it hits the sphere by solving
# (vx0^2 + vz0^2) * t^2 + 2*(x0*vx0 + z0*vz0) * t + (x0^2 + z0^2 - R^2) = 0
# (the ray-sphere intersection in the (x, z) plane). The first impact
# corresponds to the smaller root.
a_q = vx0**2 + vz0**2
b_q = 2.0 * (ion_x0 * vx0 + ion_z0 * vz0)
c_q = ion_x0**2 + ion_z0**2 - R**2
t_impact = (-b_q - np.sqrt(b_q**2 - 4 * a_q * c_q)) / (2 * a_q)

# Coordinates of the four ion impact points on the sphere.
x_impact = ion_x0 + vx0 * t_impact
y_impact = np.zeros_like(x_impact)
z_impact = ion_z0 + vz0 * t_impact

# Each emitted electron is back-propagated to the analytical ion impact time
# and then matched to the closest analytical impact point. Back-propagating
# removes the thermal-kick displacement, leaving only the EB discretization
# error in the comparison.
#
# OpenPMDTimeSeries returns proper velocities normalized by c (u/c), so
# gamma = sqrt(1 + ux^2 + uy^2 + uz^2) and the 3-velocity is v = (u/c)*c/gamma.
gamma_e = np.sqrt(1.0 + ux_e**2 + uy_e**2 + uz_e**2)
vx_e = ux_e * c / gamma_e
vy_e = uy_e * c / gamma_e
vz_e = uz_e * c / gamma_e

# The tolerance is expressed as a fraction of the sphere radius R, and covers
# only the EB discretization error (the thermal kick is removed by back-propagation).
tolerance = 0.05

for i in range(0, N_sec_e):
# For each candidate parent ion j, back-propagate the electron from ts.t[-1]
# to t_impact[j] and compute the distance to the analytical impact point.
bp_distances = np.array(
[
np.sqrt(
(x[i] - vx_e[i] * (ts.t[-1] - t_impact[j]) - x_impact[j]) ** 2
+ (y[i] - vy_e[i] * (ts.t[-1] - t_impact[j]) - y_impact[j]) ** 2
+ (z[i] - vz_e[i] * (ts.t[-1] - t_impact[j]) - z_impact[j]) ** 2
)
for j in range(len(x_impact))
]
)
j = int(np.argmin(bp_distances))
x_bp = x[i] - vx_e[i] * (ts.t[-1] - t_impact[j])
y_bp = y[i] - vy_e[i] * (ts.t[-1] - t_impact[j])
z_bp = z[i] - vz_e[i] * (ts.t[-1] - t_impact[j])
rel_dist = bp_distances[j] / R
print("\n")
print(f"Electron # {i}:")
print("NUMERICAL coordinates of the emitted electrons:")
print(f"x={x[i]:5.5f}, y={y[i]:5.5f}, z={z[i]:5.5f}")
print("\n")
print("ANALYTICAL coordinates of the point of contact:")
print(f"x={x_analytic[i]:5.5f}, y={y_analytic[i]:5.5f}, z={z_analytic[i]:5.5f}")

rel_err_x = np.abs((x[i] - x_analytic[i]) / x_analytic[i])
rel_err_y = np.abs((y[i] - y_analytic[i]) / y_analytic[i])
rel_err_z = np.abs((z[i] - z_analytic[i]) / z_analytic[i])

print(
"NUMERICAL coordinates of the emitted electron (back-propagated to impact time):"
)
print(f"x={x_bp:5.5f}, y={y_bp:5.5f}, z={z_bp:5.5f}")
print("\n")
print(f"Relative percentage error for x = {rel_err_x * 100:5.4f} %")
print(f"Relative percentage error for y = {rel_err_y * 100:5.4f} %")
print(f"Relative percentage error for z = {rel_err_z * 100:5.4f} %")
print("ANALYTICAL coordinates of the closest ion impact point on the sphere:")
print(
f"(ion # {j}) x={x_impact[j]:5.5f}, y={y_impact[j]:5.5f}, z={z_impact[j]:5.5f}"
)
print(
f"Relative distance (back-propagated) to impact point = {rel_dist * 100:5.4f} % (tolerance: {tolerance * 100} %)"
)

assert (
(rel_err_x < tolerance) and (rel_err_y < tolerance) and (rel_err_z < tolerance)
), "Test particle_boundary_interaction did not pass"
assert rel_dist < tolerance, (
f"Electron {i} is too far from any ion impact point on the sphere"
)
Loading