Source code for wake_t.physics_models.plasma_wakefields.qs_cold_fluid_1x3p

from typing import Optional, Callable

import numpy as np
import scipy.constants as ct
import aptools.plasma_accel.general_equations as ge

from wake_t.particles.deposition import deposit_3d_distribution
from wake_t.particles.interpolation import gather_field_cyl_linear
from wake_t.fields.rz_wakefield import RZWakefield
from wake_t.physics_models.laser.laser_pulse import LaserPulse


[docs] class NonLinearColdFluidWakefield(RZWakefield): """ This class computes the plasma wakefields using a nonlinear cold fluid theory in one spatial dimension with a three-component fluid momentum, as described in [1]_. This implies that only longitudinal plasma waves are modeled, i.e., it assumes infinitely broad laser pulses and particle beams. This 1D model is in Wake-T extended to 2D in r-z geometry by computing the 1D plasma response at each radial slice. Given the assumptions of the model, it is only accurate for broad drivers where the radial plasma waves can be neglected. For a laser driver, it is typically accurate up to :math:`a_0 \\lesssim 1`. For electron beams it has not been yet fully tested, but given the typically narrow width of the beams, only very low charges can be accurately modeled. For a much more general model, see :class:`Quasistatic2DWakefield`. Parameters ---------- density_function : callable Function that returns the density value at the given position z. This parameter is given by the `PlasmaStage` and does not need to be specified by the user. r_max : float Maximum radial position up to which plasma wakefield will be calculated. Required only if mode='cold_fluid_1d'. xi_min : float Minimum longitudinal (speed of light frame) position up to which plasma wakefield will be calculated. xi_max : float Maximum longitudinal (speed of light frame) position up to which plasma wakefield will be calculated. n_r : int Number of grid elements along r to calculate the wakefields. n_xi : int Number of grid elements along xi to calculate the wakefields. dz_fields : float, optional Determines how often the plasma wakefields should be updated. If dz_fields=0 (default value), the wakefields are calculated at every step of the Runge-Kutta solver for the beam particle evolution (most expensive option). If specified, the wakefields are only updated in steps determined by dz_fields. For example, if dz_fields=10e-6, the plasma wakefields are only updated every time the simulation window advances by 10 micron. By default, if not specified, the value of `dz_fields` will be `xi_max-xi_min`, i.e., the length the simulation box. beam_wakefields : bool, optional Whether to take into account beam-driven wakefields (False by default). This should be set to True for any beam-driven case or in order to take into account the beam-loading of the witness in a laser-driven case. p_shape : str, optional Particle shape to be used for the beam charge deposition. Possible values are 'linear' or 'cubic'. laser : LaserPulse, optional Laser driver of the plasma stage. laser_evolution : bool, optional If True (default), the laser pulse is evolved using a laser envelope model. If False, the pulse envelope stays unchanged throughout the computation. laser_envelope_substeps : int, optional Number of substeps of the laser envelope solver per `dz_fields`. The time step of the envelope solver is therefore `dz_fields / c / laser_envelope_substeps`. laser_envelope_nxi, laser_envelope_nr : int, optional If given, the laser envelope will run in a grid of size (`laser_envelope_nxi`, `laser_envelope_nr`) instead of (`n_xi`, `n_r`). This allows the laser to run in a finer (or coarser) grid than the plasma wake. It is not necessary to specify both parameters. If one of them is not given, the resolution of the plasma grid with be used for that direction. laser_envelope_use_phase : bool, optional Determines whether to take into account the terms related to the longitudinal derivative of the complex phase in the envelope solver. See Also -------- Quasistatic2DWakefield References ---------- .. [1] T. Mehrling, "Theoretical and numerical studies on the transport of transverse beam quality in plasma-based accelerators," PhD thesis (2014), http://dx.doi.org/10.3204/DESY-THESIS-2014-040 """ def __init__( self, density_function: Callable[[float], float], r_max: float, xi_min: float, xi_max: float, n_r: int, n_xi: int, dz_fields: Optional[float] = None, beam_wakefields: Optional[bool] = False, p_shape: Optional[str] = "linear", laser: Optional[LaserPulse] = None, laser_evolution: Optional[bool] = True, laser_envelope_substeps: Optional[int] = 1, laser_envelope_nxi: Optional[int] = None, laser_envelope_nr: Optional[int] = None, laser_envelope_use_phase: Optional[bool] = True, ) -> None: self.beam_wakefields = beam_wakefields self.p_shape = p_shape super().__init__( density_function=density_function, r_max=r_max, xi_min=xi_min, xi_max=xi_max, n_r=n_r, n_xi=n_xi, dz_fields=dz_fields, laser=laser, laser_evolution=laser_evolution, laser_envelope_substeps=laser_envelope_substeps, laser_envelope_nxi=laser_envelope_nxi, laser_envelope_nr=laser_envelope_nr, laser_envelope_use_phase=laser_envelope_use_phase, model_name="cold_fluid_1d", ) def __wakefield_ode_system(self, u_1, u_2, laser_a0, n_beam): if self.beam_wakefields: return np.array( [u_2, (1 + laser_a0**2) / (2 * (1 + u_1) ** 2) - n_beam - 1 / 2] ) else: return np.array([u_2, (1 + laser_a0**2) / (2 * (1 + u_1) ** 2) - 1 / 2]) def _calculate_wakefield(self, bunches): # Get laser envelope if self.laser is not None: a_env = np.abs(self.laser.get_envelope()) # If linearly polarized, divide by sqrt(2) so that the # ponderomotive force on the plasma particles is correct. if self.laser.polarization == "linear": a_env /= np.sqrt(2) else: a_env = np.zeros((self.n_xi, self.n_r)) # Calculate and allocate laser quantities, including guard cells. a_rz = np.zeros((self.n_xi + 4, self.n_r + 4)) a_rz[2:-2, 2:-2] = a_env s_d = ge.plasma_skin_depth(self.n_p * 1e-6) dz = self.dxi / s_d dr = self.dr / s_d r_fld = self.r_fld / s_d # Get charge distribution and remove guard cells. beam_hist = np.zeros((self.n_xi + 4, self.n_r + 4)) for bunch in bunches: x = bunch.x y = bunch.y xi = bunch.xi w = bunch.w * (bunch.q_species / ct.e) deposit_3d_distribution( xi / s_d, x / s_d, y / s_d, w, self.xi_min / s_d, r_fld[0], self.n_xi, self.n_r, dz, dr, beam_hist, p_shape=self.p_shape, use_ruyten=True, ) beam_hist = beam_hist[2:-2, 2:-2] n = np.arange(self.n_r) disc_area = np.pi * dr**2 * (1 + 2 * n) beam_hist *= 1 / (disc_area * dz * self.n_p) / s_d**3 n_iter = self.n_xi - 1 u_1 = np.zeros((n_iter + 1, len(r_fld))) u_2 = np.zeros((n_iter + 1, len(r_fld))) z_fld = self.xi_fld / s_d for i in np.arange(n_iter): z_i = z_fld[-1 - i] # get laser a0 at z, z+dz/2 and z+dz if self.laser is not None: x = r_fld y = np.zeros_like(r_fld) z = np.full_like(r_fld, z_i) a0_0 = gather_field_cyl_linear( a_rz, self.xi_min / s_d, self.xi_max / s_d, r_fld[0], r_fld[-1], dz, dr, x, y, z, ) a0_1 = gather_field_cyl_linear( a_rz, self.xi_min / s_d, self.xi_max / s_d, r_fld[0], r_fld[-1], dz, dr, x, y, z - dz / 2, ) a0_2 = gather_field_cyl_linear( a_rz, self.xi_min / s_d, self.xi_max / s_d, r_fld[0], r_fld[-1], dz, dr, x, y, z - dz, ) else: a0_0 = np.zeros(r_fld.shape[0]) a0_1 = np.zeros(r_fld.shape[0]) a0_2 = np.zeros(r_fld.shape[0]) # perform runge-kutta A = dz * self.__wakefield_ode_system( u_1[-1 - i], u_2[-1 - i], a0_0, beam_hist[-i - 1] ) B = dz * self.__wakefield_ode_system( u_1[-1 - i] + A[0] / 2, u_2[-1 - i] + A[1] / 2, a0_1, beam_hist[-i - 1] ) C = dz * self.__wakefield_ode_system( u_1[-1 - i] + B[0] / 2, u_2[-1 - i] + B[1] / 2, a0_1, beam_hist[-i - 1] ) D = dz * self.__wakefield_ode_system( u_1[-1 - i] + C[0], u_2[-1 - i] + C[1], a0_2, beam_hist[-i - 1] ) u_1[-2 - i] = u_1[-1 - i] + 1 / 6 * (A[0] + 2 * B[0] + 2 * C[0] + D[0]) u_2[-2 - i] = u_2[-1 - i] + 1 / 6 * (A[1] + 2 * B[1] + 2 * C[1] + D[1]) E_z = -np.gradient(u_1, dz, axis=0, edge_order=2) W_r = -np.gradient(u_1, dr, axis=1, edge_order=2) E_0 = ge.plasma_cold_non_relativisct_wave_breaking_field(self.n_p * 1e-6) # Calculate rho and chi. gamma_fl = (1 + a_env**2 + (1 + u_1) ** 2) / (2 * (1 + u_1)) rho_fl = gamma_fl / (1 + u_1) self.rho[2:-2, 2:-2] = rho_fl self.chi[2:-2, 2:-2] = rho_fl / gamma_fl # Calculate B_theta and E_r. u_z = (1 + a_env**2 - (1 + u_1) ** 2) / (2 * (1 + u_1)) dE_z = np.gradient(E_z, dz, axis=0, edge_order=2) v_z = u_z / gamma_fl nv_z = rho_fl * v_z integrand = (dE_z - nv_z + beam_hist) * r_fld subs = integrand / 2 B_theta = (np.cumsum(integrand, axis=1) - subs) * dr / np.abs(r_fld) E_r = W_r + B_theta # Store fields. self.b_t[2:-2, 2:-2] = B_theta * E_0 / ct.c self.e_r[2:-2, 2:-2] = E_r * E_0 self.e_z[2:-2, 2:-2] = E_z * E_0