Usage

To use prepic in a project, we first import the necessary Python modules and then declare the parameters of the laser system we want to model. For this example, let’s consider the parameters from [CABC]:

>>> from collections import namedtuple  # optional, for grouping input parameters

>>> import numpy as np
>>> import unyt as u  # for physical units support

>>> Flame = namedtuple(
...     "Flame",
...     [
...         "npe",  # electron plasma density
...         "w0",  # laser beam waist (Gaussian beam assumed)
...         "ɛL",  # laser energy on target (focused into the FWHM@intensity spot)
...         "τL",  # laser pulse duration (FWHM@intensity)
...         "prop_dist",  # laser propagation distance (acceleration length)
...     ],
... )
>>> param = Flame(
...     npe=6.14e18 / u.cm ** 3,
...     w0=6.94 * u.micrometer,
...     ɛL=1.0 * u.joule,
...     τL=30 * u.femtosecond,
...     prop_dist=1.18 * u.mm,
... )

Notice the use of physical quantities with associated units throughout. See unyt for further info.

We start by constructing a GaussianBeam from the given beam waist w0. Note the the beam can also be constructed by giving its FWHM size instead. Alternatively, one can use GaussianBeam.from_f_number or GaussianBeam.from_focal_distance:

>>> from prepic import GaussianBeam
>>> flame_beam = GaussianBeam(w0=param.w0)

We see prepic computed the FWHM spot size and Rayleigh length of our beam:

>>> print(flame_beam)
beam with w0=6.9 µm (FWHM=8.2 µm), zᵣ=0.19 mm, λL=0.80 µm

We now initialize a Laser instance using its default constructor. We could have used Laser.from_a0, Laser.from_intensity or Laser.from_power, depending on which laser parameters are known:

>>> from prepic import Laser
>>> flame_laser = Laser(ɛL=param.ɛL, τL=param.τL, beam=flame_beam)
>>> print(flame_laser)
laser with kL=7.854 1/µm, ωL=2.355 1/fs, ɛL=1.0 J, τL=30.0 fs, P₀=31.3 TW
I₀=4.1e+19 W/cm**2, a₀=4.4, E₀=1.8e+04 MV/mm
Helium ionization state: 2+

The various attributes, such as the critical density ncrit or peak laser electric field E0 can be easily accessed:

>>> print(f"critical density for this laser is {flame_laser.ncrit:.1e}")
critical density for this laser is 1.7e+21 cm**(-3)
>>> print(flame_laser.E0)  
17659.733275104507 MV/mm

Also notice that flame_laser contains the flame_beam instance from before. For example, we can access its Rayleigh length via:

>>> print(flame_laser.beam.zR)  
0.18913801491304671 mm

We now build the Plasma for our parameters via:

>>> from prepic import Plasma
>>> flame_plasma = Plasma(
...     n_pe=param.npe, laser=flame_laser, propagation_distance=param.prop_dist
... )
>>> print(flame_plasma)
Plasma with nₚ=6.1e+18 cm**(-3) (3.52e-03 × nc), ωₚ=0.140 1/fs, kₚ=0.466 1/µm, λₚ=13.5 µm, Ewb=238.3 MV/mm
Pc=4.8 TW, Ldeph=1.70 mm, Ldepl=2.55 mm, ΔE=294.9 MeV over Lacc=1.18 mm

This is the top-level class, which contains all the computed parameters. If, as before, we would like to access the Rayleigh length, we can do so via:

>>> print(flame_plasma.laser.beam.zR)  
0.18913801491304671 mm

All the computed parameters are stored as attributes. See Plasma for their description:

>>> print(f"\nThe dephasing length is {flame_plasma.dephasing:.1f}.")
The dephasing length is 1.7 mm.

If propagation_distance is passed, this is used to evaluate the electron energy gain ΔE. If not given, the code assumes that the electrons are accelerated for a distance equal to the dephasing length.

The Plasma can also be constructed by passing the (optional) bubble_radius, if known from experiments or numerical simulations. For now, we can estimate the bubble size from the scaling laws of [LTJT]: \(R = 2 \sqrt{a_0} / k_p\). This allows computing the total accelerated charge Q and laser-to-electron energy transfer efficiency η:

>>> bubble_r = 2 * np.sqrt(flame_plasma.laser.a0) / flame_plasma.kp
>>> print(f"The bubble radius is {bubble_r.to('micrometer'):.1f}.\n")
The bubble radius is 9.0 µm.
>>> plasma_with_bubble = Plasma(
...     n_pe=param.npe,
...     laser=flame_laser,
...     bubble_radius=bubble_r,
...     propagation_distance=param.prop_dist,
... )
>>> print(plasma_with_bubble.Q)  
300.1260542601371 pC
>>> print(plasma_with_bubble.η.to_value('dimensionless'))  
0.08850500992541349

The Plasma parameters can also be automagically computed by matched_laser_plasma, based on the scaling laws of [LTJT]. The only input parameter in this case is the laser normalized vector potential \(a_0\):

>>> from prepic import matched_laser_plasma
>>> matched_plasma_flame = matched_laser_plasma(a0=flame_laser.a0)
>>> print(matched_plasma_flame)  # notice density, spot size, etc. changed!
Plasma with nₚ=1.1e+18 cm**(-3) (6.06e-04 × nc), ωₚ=0.058 1/fs, kₚ=0.193 1/µm, λₚ=32.5 µm, Ewb=98.8 MV/mm
Pc=28.0 TW, Ldeph=23.86 mm, Ldepl=23.86 mm, ΔE=2472.7 MeV over Lacc=23.86 mm
N=4.5e+09 electrons, Q=723.7 pC, η=0.114
>>> print(matched_plasma_flame.Q)  
723.6933108198331 pC

We see that now the total accelerated charge, final energy, as well as efficiency are all improved compared to their previous values. The acceleration distance is now longer, and equal to the dephasing and depletion lengths. This is possible due to the better matching between laser and plasma parameters.

Finally, the prepic package also includes a Simulation convenience class for estimating the recommended parameters for a PIC simulation, based on a particular Plasma:

>>> from prepic import Simulation
>>> sim_flame = Simulation(matched_plasma_flame)
>>> print(sim_flame)
3D simulation with box size (130.0 µm)³, Δx=0.517 µm, Δy=0.517 µm, Δz=0.040 µm, nx=251, ny=251, nz=3249, 1.637522e+09 macro-particles, 5.997110e+05 time steps

These values can then be used as inputs for specialized codes such as PIConGPU or fbpic.

CABC

Curcio, A., et al. Physical Review Accelerators and Beams 20.1 (2017): 012801.