Skip to content

Commit d6bad13

Browse files
committed
Add Liénard-Wiechert potential evaluation
1 parent 4969dc1 commit d6bad13

6 files changed

Lines changed: 312 additions & 5 deletions

File tree

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ version = "0.1.0-DEV"
44
authors = ["Sebastian Micluța-Câmpeanu <sebastian.mc95@proton.me> and contributors"]
55

66
[deps]
7+
DataInterpolations = "82cc6244-b520-54b8-b5a6-8a565e85f1d0"
78
HypergeometricFunctions = "34004b35-14d8-5ef3-9330-4cdb6864b03a"
89
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
910
ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78"
@@ -19,6 +20,7 @@ UnitfulAtomic = "a7773ee8-282e-5fa2-be4e-bd808c38a91a"
1920
[compat]
2021
Aqua = "0.8"
2122
CSV = "0.10"
23+
DataInterpolations = "8.9.0"
2224
HypergeometricFunctions = "0.3.28"
2325
JET = "0.9, 0.10, 0.11"
2426
LaserTypes = "0.2"

scripts/Project.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
[deps]
2+
CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"
23
ElectronDynamicsModels = "acecdaf2-97b2-47e1-90eb-3efa7bb274e5"
4+
FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341"
35
GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a"
46
LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f"
57
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
68
ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78"
9+
OrdinaryDiffEqNonlinearSolve = "127b3ac7-2247-4354-8eb6-78cf4e7c58e8"
10+
OrdinaryDiffEqTsit5 = "b1df2697-797e-41e3-8120-5422d3b24e4a"
711
OrdinaryDiffEqVerner = "79d7bb75-1356-48c1-b8c0-6832512096c2"
12+
ProfileCanvas = "efd6af41-a80b-495e-886c-e51b0c7d77a3"
13+
ProfileView = "c46f51b8-102a-5cf2-8d2c-8597cb0e0da7"
814
SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462"
915
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
1016
SymbolicIndexingInterface = "2efcf032-c050-4f8e-a9bb-153293bab1f5"

scripts/thomson_scattering.jl

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
using ElectronDynamicsModels
2+
using ModelingToolkit
3+
using OrdinaryDiffEqVerner, OrdinaryDiffEqTsit5
4+
using OrdinaryDiffEqNonlinearSolve
5+
using SciMLBase
6+
using StaticArrays
7+
using SymbolicIndexingInterface
8+
using LinearAlgebra
9+
using FFTW
10+
using CairoMakie
11+
12+
# Atomic units
13+
const c = 137.03599908330932
14+
15+
# Laser parameters
16+
ω = 0.057
17+
τ = 150 / ω
18+
λ = 2π * c / ω
19+
w₀ = 75λ
20+
Rmax = 3.25w₀
21+
22+
a₀ = 10.0
23+
24+
@named ref_frame = ProperFrame(:atomic)
25+
26+
@named laser = LaguerreGaussLaser(;
27+
wavelength = λ,
28+
a0 = a₀,
29+
beam_waist = w₀,
30+
radial_index = 2,
31+
azimuthal_index = -2,
32+
ref_frame,
33+
temporal_profile = :gaussian,
34+
temporal_width = τ,
35+
focus_position = 0.0,
36+
polarization = :circular
37+
)
38+
@named elec = ClassicalElectron(; laser)
39+
sys = mtkcompile(elec)
40+
41+
# Time span
42+
τi = -8τ
43+
τf = 8τ
44+
tspan = (τi, τf)
45+
46+
# Single electron solve (for parameter access)
47+
x⁰ = [τi * c, 0.0, 0.0, 0.0]
48+
u⁰ = [c, 0.0, 0.0, 0.0]
49+
50+
u0 = [
51+
sys.x => x⁰,
52+
sys.u => u⁰,
53+
]
54+
55+
prob = ODEProblem{false, SciMLBase.FullSpecialize}(
56+
sys, u0, tspan, u0_constructor = SVector{8}, fully_determined = true
57+
)
58+
sol0 = solve(prob, Vern9(), reltol = 1.0e-15, abstol = 1.0e-12)
59+
60+
# Sunflower distribution for electron positions
61+
const ϕ = (1 + 5) / 2
62+
63+
function radius(k, n, b)
64+
if k > n - b
65+
return 1.0
66+
else
67+
return sqrt(k - 0.5) / sqrt(n - (b + 1) / 2)
68+
end
69+
end
70+
71+
function sunflower(n, α)
72+
points = Vector{Vector{Float64}}()
73+
angle_stride = 2π / ϕ^2
74+
b = round(Int, α * sqrt(n))
75+
for k in 1:n
76+
r = radius(k, n, b)
77+
θ = k * angle_stride
78+
push!(points, [r * cos(θ), r * sin(θ)])
79+
end
80+
return points
81+
end
82+
83+
# Ensemble solve
84+
N = 300
85+
R₀ = Rmax * sunflower(N, 2)
86+
= [[τi * c, r..., 0.0] for r in R₀]
87+
88+
set_x = setsym_oop(prob, [Initial(sys.x); Initial(sys.u)])
89+
90+
function prob_func(prob, i, repeat)
91+
x_new = SVector{4}(xμ[i]...)
92+
u_new = SVector{4}(c, 0.0, 0.0, 0.0)
93+
u0, p = set_x(prob, SVector{8}(x_new..., u_new...))
94+
return remake(prob; u0, p)
95+
end
96+
97+
function abserr(a₀)
98+
amp = log10(a₀)
99+
expo = -amp^2 / 27 + 32amp / 27 - 220 / 27
100+
return 10^expo
101+
end
102+
103+
ensemble = EnsembleProblem(prob; prob_func, safetycopy = false)
104+
solution = solve(
105+
ensemble, Vern9(), EnsembleThreads();
106+
reltol = 1.0e-12, abstol = abserr(a₀), trajectories = N
107+
)
108+
109+
# Radiation computation
110+
111+
trajs = ElectronDynamicsModels.trajectories(solution)
112+
113+
# Screen parameters
114+
const Z = 2.0e5λ
115+
const δt = 2π / ω / 4
116+
const N_samples = floor(Int, (τf - τi) / δt)
117+
const x⁰_start = c * τi + hypot(Z, 25w₀ + Rmax)
118+
119+
Nx = 50
120+
Ny = 50
121+
122+
x⁰_samples = range(start = x⁰_start, step = c * δt, length = N_samples)
123+
124+
screen = ElectronDynamicsModels.ObserverScreen(
125+
LinRange(-25w₀, 25w₀, Nx),
126+
LinRange(-25w₀, 25w₀, Ny),
127+
Z,
128+
x⁰_samples
129+
)
130+
131+
K = sol0.ps[ref_frame.q_e / (4π * ref_frame.ε₀ * c)]
132+
133+
A_s = ElectronDynamicsModels.accumulate_potential(trajs, screen, K, alg = Tsit5())
134+
A_ω = rfft(A_s, 1)
135+
136+
# Find fundamental frequency bin
137+
freqs = rfftfreq(N_samples, 1 / δt)
138+
idx_f1 = findmin(x -> abs(x - ω / 2π), freqs)[2]
139+
140+
# Plot the y-component of the potential at the fundamental frequency
141+
fig = Figure()
142+
ax = Axis(fig[1, 1], aspect = 1, xlabel = "x", ylabel = "y", title = "Thomson scattering (ω₁)")
143+
field = real.(A_ω[idx_f1, 3, :, :])
144+
heatmap!(
145+
ax, collect(screen.x_grid), collect(screen.y_grid), field,
146+
colorrange = maximum(abs, field) .* (-1, 1), colormap = :seismic
147+
)
148+
fig

src/ElectronDynamicsModels.jl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ module ElectronDynamicsModels
22

33
using ModelingToolkit
44
using ModelingToolkitBase: AbstractSystem, SymbolicT, build_explicit_observed_function, get_systems
5-
using SymbolicIndexingInterface: getname, setsym_oop
5+
using SymbolicIndexingInterface: getname, setsym_oop, variable_index
66
using PhysicalConstants, Unitful, UnitfulAtomic
77
using PhysicalConstants.CODATA2018: c_0, e, m_e, ε_0
88
using LinearAlgebra
99
using Symbolics
1010
using HypergeometricFunctions: HypergeometricFunctions, _₁F₁, pochhammer
1111
using StaticArrays
12+
using SciMLBase
13+
using DataInterpolations
1214

1315
m_dot(x, y) = x[1] * y[1] - x[2] * y[2] - x[3] * y[3] - x[4] * y[4]
1416

@@ -28,6 +30,7 @@ export ReferenceFrame, ProperFrame, LabFrame,
2830
include("base.jl")
2931
include("dynamics.jl")
3032
include("fields.jl")
33+
include("radiation.jl")
3134
include("radiation_reaction.jl")
3235
include("external_fields.jl")
3336
include("systems.jl")

src/external_fields.jl

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ Parameters:
1313
- t₀: pulse center time (= n_cycles × 2π/ω)
1414
- z₀: focus position along z-axis
1515
"""
16-
@component function GaussLaser(; name, wavelength = nothing, frequency = nothing,
16+
@component function GaussLaser(;
17+
name, wavelength = nothing, frequency = nothing,
1718
a0 = 10.0, beam_waist = nothing, polarization = :linear,
18-
n_cycles = 5, focus_position = nothing, ref_frame)
19+
n_cycles = 5, focus_position = nothing, ref_frame
20+
)
1921
if wavelength === nothing && frequency === nothing
2022
wavelength = 1.0
2123
end
@@ -82,8 +84,7 @@ Parameters:
8284
E[1] ~ real(ξx * E_g)
8385
E[2] ~ real(ξy * E_g)
8486
E[3] ~ real(
85-
2im / (k * wz^2) *
86-
(1 + im * (z / z_R)) *
87+
2im / (k * wz^2) * (1 + im * (z / z_R)) *
8788
(x[2] * (ξx * E_g) + x[3] * (ξy * E_g)),
8889
)
8990

src/radiation.jl

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# Trajectory access (thread-safe interpolation wrapper)
2+
struct TrajectoryInterpolant{I, R, U}
3+
itp::I # DataInterpolations interpolant (SVector{8} → SVector{8})
4+
r_idxs::R # SVector{4, Int} for x⁰, x¹, x², x³
5+
u_idxs::U # SVector{4, Int} for u⁰, u¹, u², u³
6+
end
7+
8+
function TrajectoryInterpolant(sol::SciMLBase.AbstractODESolution, x_syms, u_syms)
9+
r_idxs = SVector{4, Int}(variable_index.((sol,), collect(x_syms)))
10+
u_idxs = SVector{4, Int}(variable_index.((sol,), collect(u_syms)))
11+
itp = CubicSpline(sol.u, sol.t; extrapolation = ExtrapolationType.Extension)
12+
return TrajectoryInterpolant(itp, r_idxs, u_idxs)
13+
end
14+
15+
function (t::TrajectoryInterpolant)(τ)
16+
v = t.itp(τ)
17+
= v[t.r_idxs]
18+
= v[t.u_idxs]
19+
return (rμ, uμ)
20+
end
21+
22+
# Convenience: extract all trajectories from ensemble solution
23+
function trajectories(ensemble_sol::EnsembleSolution)
24+
sys = ensemble_sol.u[1].prob.f.sys
25+
return [TrajectoryInterpolant(ensemble_sol.u[i], sys.x, sys.u) for i in axes(ensemble_sol.u, 1)]
26+
end
27+
28+
# Observer geometry + temporal sampling
29+
struct ObserverScreen{G, T, R}
30+
x_grid::G # e.g., LinRange for x
31+
y_grid::G # e.g., LinRange for y
32+
z::T # screen distance
33+
x⁰_samples::R # uniform observer-time sampling grid
34+
end
35+
36+
# dτᵣ/dt = 1/(u⁰(τᵣ) - u⃗(τᵣ)·n̂(τᵣ, r_obs))
37+
function retarded_time_rhs(τᵣ, p, t)
38+
traj, r_obs = p
39+
rμ, uμ = traj(τᵣ)
40+
rⁱ = rμ[SA[2, 3, 4]]
41+
= (r_obs - rⁱ) * inv(norm(r_obs - rⁱ))
42+
return inv(uμ[1] - uμ[SA[2, 3, 4]] n̂)
43+
end
44+
45+
function advanced_time(traj, τ, x_obs)
46+
rμ, _ = traj(τ)
47+
return rμ[1] + norm(x_obs - rμ[SA[2, 3, 4]])
48+
end
49+
50+
function retarded_time_problem(traj::TrajectoryInterpolant, screen::ObserverScreen)
51+
Nx, Ny = length(screen.x_grid), length(screen.y_grid)
52+
CI = CartesianIndices((Nx, Ny))
53+
54+
r_obs_0 = SVector{3}(screen.x_grid[1], screen.y_grid[1], screen.z)
55+
τi = first(traj.itp.t)
56+
τf = last(traj.itp.t)
57+
(x⁰_i_0, x⁰_f_0) = advanced_time(traj, τi, r_obs_0), advanced_time(traj, τf, r_obs_0)
58+
59+
prob = ODEProblem{false, SciMLBase.FullSpecialize}(
60+
retarded_time_rhs,
61+
τi,
62+
(x⁰_i_0, x⁰_f_0),
63+
(traj, r_obs_0),
64+
)
65+
66+
function set_pixel(prob, i, repeat)
67+
ix, iy = Tuple(CI[i])
68+
r_obs = SVector{3}(screen.x_grid[ix], screen.y_grid[iy], screen.z)
69+
(x⁰_i, x⁰_f) = advanced_time(traj, τi, r_obs), advanced_time(traj, τf, r_obs)
70+
return remake(prob; p = (traj, r_obs), tspan = (x⁰_i, x⁰_f))
71+
end
72+
73+
return EnsembleProblem(prob; prob_func = set_pixel, safetycopy = false)
74+
end
75+
76+
function accumulate_potential(trajs, screen, K; alg, ensemblealg = nothing, solve_kwargs...)
77+
x⁰_samples = screen.x⁰_samples
78+
N_samples = length(x⁰_samples)
79+
Nx, Ny = length(screen.x_grid), length(screen.y_grid)
80+
81+
A = zeros(N_samples, 4, Nx, Ny)
82+
83+
for (j, traj) in enumerate(trajs)
84+
τi = first(traj.itp.t)
85+
τf = last(traj.itp.t)
86+
87+
if ensemblealg !== nothing
88+
# GPU / custom ensemble path
89+
rt_prob = retarded_time_problem(traj, screen)
90+
N_pixels = Nx * Ny
91+
rt_sol = solve(
92+
rt_prob, alg, ensemblealg;
93+
trajectories = N_pixels, saveat = x⁰_samples, solve_kwargs...
94+
)
95+
LI = LinearIndices((Nx, Ny))
96+
97+
Threads.@threads for ix in Base.OneTo(Nx)
98+
for iy in Base.OneTo(Ny)
99+
_accumulate_pixel!(A, traj, screen, K, rt_sol.u[LI[ix, iy]].u, ix, iy)
100+
end
101+
end
102+
else
103+
# CPU path: reinit! to avoid repeated solver initialization
104+
r_obs_0 = SVector{3}(screen.x_grid[1], screen.y_grid[1], screen.z)
105+
x⁰_i_0 = advanced_time(traj, τi, r_obs_0)
106+
x⁰_f_0 = advanced_time(traj, τf, r_obs_0)
107+
proto_prob = ODEProblem{false, SciMLBase.FullSpecialize}(
108+
retarded_time_rhs, τi, (x⁰_i_0, x⁰_f_0), (traj, r_obs_0)
109+
)
110+
111+
nworkers = Threads.nthreads()
112+
integ_pool = Channel{Any}(nworkers)
113+
for _ in 1:nworkers
114+
put!(integ_pool, init(proto_prob, alg; saveat = x⁰_samples, solve_kwargs...))
115+
end
116+
117+
Threads.@threads for ix in Base.OneTo(Nx)
118+
integ = take!(integ_pool)
119+
for iy in Base.OneTo(Ny)
120+
r_obs = SVector{3}(screen.x_grid[ix], screen.y_grid[iy], screen.z)
121+
x⁰_i = advanced_time(traj, τi, r_obs)
122+
x⁰_f = advanced_time(traj, τf, r_obs)
123+
124+
integ.p = (traj, r_obs)
125+
reinit!(integ, τi; t0 = x⁰_i, tf = x⁰_f)
126+
solve!(integ)
127+
128+
_accumulate_pixel!(A, traj, screen, K, integ.sol.u, ix, iy)
129+
end
130+
put!(integ_pool, integ)
131+
end
132+
end
133+
end
134+
135+
return A
136+
end
137+
138+
function _accumulate_pixel!(A, traj, screen, K, τ_samples, ix, iy)
139+
r_obs = SVector{3}(screen.x_grid[ix], screen.y_grid[iy], screen.z)
140+
for (k, τ) in enumerate(τ_samples)
141+
rμ, uμ = traj(τ)
142+
disp = r_obs - rμ[SA[2, 3, 4]]
143+
xR = SVector{4}(norm(disp), disp[1], disp[2], disp[3])
144+
@views A[k, :, ix, iy] .+= K *./ m_dot(xR, uμ)
145+
end
146+
return
147+
end

0 commit comments

Comments
 (0)