Пример #1
0
# Order of the stencil for z derivatives in the Maxwell solver.
# Use -1 for infinite order, i.e. for exact dispersion relation in
# all direction (adviced for single-GPU/single-CPU simulation).
# Use a positive number (and multiple of 2) for a finite-order stencil
# (required for multi-GPU/multi-CPU with MPI). A large `n_order` leads
# to more overhead in MPI communications, but also to a more accurate
# dispersion relation for electromagnetic waves. (Typically,
# `n_order = 32` is a good trade-off.)
# See https://arxiv.org/abs/1611.05712 for more information.
n_order = -1

# Boosted frame
gamma_boost = 10.
# Boosted frame converter
boost = BoostConverter(gamma_boost)

# The simulation box
Nz = 600  # Number of gridpoints along z
zmax = 0.e-6  # Length of the box along z (meters)
zmin = -30.e-6
Nr = 75  # Number of gridpoints along r
rmax = 150.e-6  # Length of the box along r (meters)
Nm = 2  # Number of modes used

# The simulation timestep
# (See the section Advanced use > Running boosted-frame simulation
# of the FBPIC documentation for an explanation of the calculation of dt)
dt = min(rmax / (2 * boost.gamma0 * Nr) / c,
         (zmax - zmin) / Nz / c)  # Timestep (seconds)
Пример #2
0
# Order of the stencil for z derivatives in the Maxwell solver.
# Use -1 for infinite order, i.e. for exact dispersion relation in
# all direction (adviced for single-GPU/single-CPU simulation).
# Use a positive number (and multiple of 2) for a finite-order stencil
# (required for multi-GPU/multi-CPU with MPI). A large `n_order` leads
# to more overhead in MPI communications, but also to a more accurate
# dispersion relation for electromagnetic waves. (Typically,
# `n_order = 32` is a good trade-off.)
# See https://arxiv.org/abs/1611.05712 for more information.
n_order = -1

# Boosted frame
gamma_boost = 10.
# Boosted frame converter
boost = BoostConverter(gamma_boost)

# The simulation box
Nz = 600  # Number of gridpoints along z
zmax = 0.e-6  # Length of the box along z (meters)
zmin = -30.e-6
Nr = 75  # Number of gridpoints along r
rmax = 150.e-6  # Length of the box along r (meters)
Nm = 2  # Number of modes used

# The simulation timestep
# (See the section Advanced use > Running boosted-frame simulation
# of the FBPIC documentation for an explanation of the calculation of dt)
dt = min(rmax / (2 * boost.gamma0 * Nr) / c,
         (zmax - zmin) / Nz / c)  # Timestep (seconds)
Пример #3
0
# (increase this number for a real simulation)

# Order of the stencil for z derivatives in the Maxwell solver.
# Use -1 for infinite order, i.e. for exact dispersion relation in
# all direction (adviced for single-GPU/single-CPU simulation).
# Use a positive number (and multiple of 2) for a finite-order stencil
# (required for multi-GPU/multi-CPU with MPI). A large `n_order` leads
# to more overhead in MPI communications, but also to a more accurate
# dispersion relation for electromagnetic waves. (Typically,
# `n_order = 32` is a good trade-off.)
# See https://arxiv.org/abs/1611.05712 for more information.
n_order = -1

# Boosted frame
gamma_boost = 15.
boost = BoostConverter(gamma_boost)

# The laser (conversion to boosted frame is done inside 'add_laser')
a0 = 2.  # Laser amplitude
w0 = 50.e-6  # Laser waist
ctau = 5.e-6  # Laser duration
z0 = -10.e-6  # Laser centroid
zfoc = 0.e-6  # Focal position
lambda0 = 0.8e-6  # Laser wavelength

# The density profile
w_matched = 50.e-6
ramp_up = 5.e-3
plateau = 8.e-2
ramp_down = 5.e-3
Пример #4
0
def run_simulation(gamma_boost, use_separate_electron_species):
    """
    Run a simulation with a laser pulse going through a gas jet of ionizable
    N5+ atoms, and check the fraction of atoms that are in the N5+ state.

    Parameters
    ----------
    gamma_boost: float
        The Lorentz factor of the frame in which the simulation is carried out.
    use_separate_electron_species: bool
        Whether to use separate electron species for each level, or
        a single electron species for all levels.
    """
    # The simulation box
    zmax_lab = 20.e-6  # Length of the box along z (meters)
    zmin_lab = 0.e-6
    Nr = 3  # Number of gridpoints along r
    rmax = 10.e-6  # Length of the box along r (meters)
    Nm = 2  # Number of modes used

    # The particles of the plasma
    p_zmin = 5.e-6  # Position of the beginning of the plasma (meters)
    p_zmax = 15.e-6
    p_rmin = 0.  # Minimal radial position of the plasma (meters)
    p_rmax = 100.e-6  # Maximal radial position of the plasma (meters)
    n_atoms = 0.2  # The atomic density is chosen very low,
    # to avoid collective effects
    p_nz = 2  # Number of particles per cell along z
    p_nr = 1  # Number of particles per cell along r
    p_nt = 4  # Number of particles per cell along theta

    # Boosted frame
    boost = BoostConverter(gamma_boost)
    # Boost the different quantities
    beta_boost = np.sqrt(1. - 1. / gamma_boost**2)
    zmin, zmax = boost.static_length([zmin_lab, zmax_lab])
    p_zmin, p_zmax = boost.static_length([p_zmin, p_zmax])
    n_atoms, = boost.static_density([n_atoms])
    # Increase the number of particles per cell in order to keep sufficient
    # statistics for the evaluation of the ionization fraction
    if gamma_boost > 1:
        p_nz = int(2 * gamma_boost * (1 + beta_boost) * p_nz)

    # The laser
    a0 = 1.8  # Laser amplitude
    lambda0_lab = 0.8e-6  # Laser wavelength
    # Boost the laser wavelength before calculating the laser amplitude
    lambda0, = boost.copropag_length([lambda0_lab], beta_object=1.)
    # Duration and initial position of the laser
    ctau = 10. * lambda0
    z0 = -2 * ctau
    # Calculate laser amplitude
    omega = 2 * np.pi * c / lambda0
    E0 = a0 * m_e * c * omega / e
    B0 = E0 / c

    def laser_func(F, x, y, z, t, amplitude, length_scale):
        """
        Function that describes a Gaussian laser with infinite waist
        """
        return( F + amplitude * math.cos( 2*np.pi*(z-c*t)/lambda0 ) * \
                math.exp( - (z - c*t - z0)**2/ctau**2 ) )

    # Resolution and number of timesteps
    dz = lambda0 / 16.
    dt = dz / c
    Nz = int((zmax - zmin) / dz) + 1
    N_step = int(
        (2. * 40. * lambda0 + zmax - zmin) / (dz * (1 + beta_boost))) + 1

    # Get the speed of the plasma
    uz_m, = boost.longitudinal_momentum([0.])
    v_plasma, = boost.velocity([0.])

    # The diagnostics
    diag_period = N_step - 1  # Period of the diagnostics in number of timesteps

    # Initial ionization level of the Nitrogen atoms
    level_start = 2
    # Initialize the simulation object, with the neutralizing electrons
    # No particles are created because we do not pass the density
    sim = Simulation(Nz,
                     zmax,
                     Nr,
                     rmax,
                     Nm,
                     dt,
                     zmin=zmin,
                     v_comoving=v_plasma,
                     use_galilean=False,
                     boundaries='open',
                     use_cuda=use_cuda)

    # Add the charge-neutralizing electrons
    elec = sim.add_new_species(q=-e,
                               m=m_e,
                               n=level_start * n_atoms,
                               p_nz=p_nz,
                               p_nr=p_nr,
                               p_nt=p_nt,
                               p_zmin=p_zmin,
                               p_zmax=p_zmax,
                               p_rmin=p_rmin,
                               p_rmax=p_rmax,
                               continuous_injection=False,
                               uz_m=uz_m)
    # Add the N atoms
    ions = sim.add_new_species(q=0,
                               m=14. * m_p,
                               n=n_atoms,
                               p_nz=p_nz,
                               p_nr=p_nr,
                               p_nt=p_nt,
                               p_zmin=p_zmin,
                               p_zmax=p_zmax,
                               p_rmin=p_rmin,
                               p_rmax=p_rmax,
                               continuous_injection=False,
                               uz_m=uz_m)
    # Add the target electrons
    if use_separate_electron_species:
        # Use a dictionary of electron species: one per ionizable level
        target_species = {}
        level_max = 6  # N can go up to N7+, but here we stop at N6+
        for i_level in range(level_start, level_max):
            target_species[i_level] = sim.add_new_species(q=-e, m=m_e)
    else:
        # Use the pre-existing, charge-neutralizing electrons
        target_species = elec
        level_max = None  # Default is going up to N7+
    # Define ionization
    ions.make_ionizable(element='N',
                        level_start=level_start,
                        level_max=level_max,
                        target_species=target_species)
    # Set the moving window
    sim.set_moving_window(v=v_plasma)

    # Add a laser to the fields of the simulation (external fields)
    sim.external_fields = [
        ExternalField(laser_func, 'Ex', E0, 0.),
        ExternalField(laser_func, 'By', B0, 0.)
    ]

    # Add a particle diagnostic
    sim.diags = [
        ParticleDiagnostic(
            diag_period,
            {"ions": ions},
            particle_data=["position", "gamma", "weighting", "E", "B"],
            # Test output of fields and gamma for standard
            # (non-boosted) particle diagnostics
            write_dir='tests/diags',
            comm=sim.comm)
    ]
    if gamma_boost > 1:
        T_sim_lab = (2. * 40. * lambda0_lab + zmax_lab - zmin_lab) / c
        sim.diags.append(
            BackTransformedParticleDiagnostic(zmin_lab,
                                              zmax_lab,
                                              v_lab=0.,
                                              dt_snapshots_lab=T_sim_lab / 2.,
                                              Ntot_snapshots_lab=3,
                                              gamma_boost=gamma_boost,
                                              period=diag_period,
                                              fldobject=sim.fld,
                                              species={"ions": ions},
                                              comm=sim.comm,
                                              write_dir='tests/lab_diags'))

    # Run the simulation
    sim.step(N_step, use_true_rho=True)

    # Check the fraction of N5+ ions at the end of the simulation
    w = ions.w
    ioniz_level = ions.ionizer.ionization_level
    # Get the total number of N atoms/ions (all ionization levels together)
    ntot = w.sum()
    # Get the total number of N5+ ions
    n_N5 = w[ioniz_level == 5].sum()
    # Get the fraction of N5+ ions, and check that it is close to 0.32
    N5_fraction = n_N5 / ntot
    print('N5+ fraction: %.4f' % N5_fraction)
    assert ((N5_fraction > 0.30) and (N5_fraction < 0.34))

    # When different electron species are created, check the fraction of
    # each electron species
    if use_separate_electron_species:
        for i_level in range(level_start, level_max):
            n_N = w[ioniz_level == i_level].sum()
            assert np.allclose(target_species[i_level].w.sum(), n_N)

    # Check consistency in the regular openPMD diagnostics
    ts = OpenPMDTimeSeries('./tests/diags/hdf5/')
    last_iteration = ts.iterations[-1]
    w, q = ts.get_particle(['w', 'charge'],
                           species="ions",
                           iteration=last_iteration)
    # Check that the openPMD file contains the same number of N5+ ions
    n_N5_openpmd = np.sum(w[(4.5 * e < q) & (q < 5.5 * e)])
    assert np.isclose(n_N5_openpmd, n_N5)
    # Remove openPMD files
    shutil.rmtree('./tests/diags/')

    # Check consistency of the back-transformed openPMD diagnostics
    if gamma_boost > 1.:
        ts = OpenPMDTimeSeries('./tests/lab_diags/hdf5/')
        last_iteration = ts.iterations[-1]
        w, q = ts.get_particle(['w', 'charge'],
                               species="ions",
                               iteration=last_iteration)
        # Check that the openPMD file contains the same number of N5+ ions
        n_N5_openpmd = np.sum(w[(4.5 * e < q) & (q < 5.5 * e)])
        assert np.isclose(n_N5_openpmd, n_N5)
        # Remove openPMD files
        shutil.rmtree('./tests/lab_diags/')
Пример #5
0
def check_fields(interp1_complex,
                 z,
                 r,
                 info_in_real_part,
                 z0,
                 gamma_b,
                 forward_propagating,
                 show_difference=False):
    """
    Check the real and imaginary part of the interpolation grid agree
    with the theory by:
    - Checking that the part (real or imaginary) that does not
        carry information is zero
    - Extracting the a0 from the other part and comparing it
        to the predicted value
    - Using the extracted value of a0 to compare the simulated
      profile with a gaussian profile
    """
    # Extract the part that has information
    if info_in_real_part:
        interp1 = interp1_complex.real
        zero_part = interp1_complex.imag
    else:
        interp1 = interp1_complex.imag
        zero_part = interp1_complex.real

    # Control that the part that has no information is 0
    assert np.allclose(0., zero_part, atol=1.e-6 * interp1.max())

    # Get the predicted properties of the laser in the boosted frame
    if gamma_b is None:
        boost = BoostConverter(1.)
    else:
        boost = BoostConverter(gamma_b)
    ctau_b, lambda0_b, Lprop_b, z0_b = \
        boost.copropag_length([ctau, 0.8e-6, Lprop, z0])
    # Take into account whether the pulse is propagating forward or backward
    if not forward_propagating:
        Lprop_b = -Lprop_b

    # Fit the on-axis profile to extract a0
    def fit_function(z, a0, z0_phase):
        return (gaussian_laser(z, r[0], a0, z0_phase, z0_b + Lprop_b, ctau_b,
                               lambda0_b))

    fit_result = curve_fit(fit_function,
                           z,
                           interp1[:, 0],
                           p0=np.array([a0, z0_b + Lprop_b]))
    a0_fit, z0_fit = fit_result[0]

    # Check that the a0 agrees within 5% of the predicted value
    assert abs(abs(a0_fit) - a0) / a0 < 0.05

    # Calculate predicted fields
    r2d, z2d = np.meshgrid(r, z)
    # Factor 0.5 due to the definition of the interpolation grid
    interp1_predicted = gaussian_laser(z2d, r2d, a0_fit, z0_fit,
                                       z0_b + Lprop_b, ctau_b, lambda0_b)
    # Plot the difference
    if show_difference:
        import matplotlib.pyplot as plt
        plt.subplot(311)
        plt.imshow(interp1.T)
        plt.colorbar()
        plt.subplot(312)
        plt.imshow(interp1_predicted.T)
        plt.colorbar()
        plt.subplot(313)
        plt.imshow((interp1_predicted - interp1).T)
        plt.colorbar()
        plt.show()
    # Control the values (with a precision of 3%)
    assert np.allclose(interp1_predicted, interp1, atol=3.e-2 * interp1.max())
Пример #6
0
Nz = 400  # Number of gridpoints along z
zmax = 0.e-6  # Length of the box along z (meters)
zmin = -20.e-6
Nr = 75  # Number of gridpoints along r
rmax = 150.e-6  # Length of the box along r (meters)
Nm = 2  # Number of modes used
n_guard = 40  # Number of guard cells
exchange_period = 10
# The simulation timestep
dt = (zmax - zmin) / Nz / c  # Timestep (seconds)
N_step = 101  # Number of iterations to perform
# (increase this number for a real simulation)

# Boosted frame
gamma_boost = 15.
boost = BoostConverter(gamma_boost)

# The laser (conversion to boosted frame is done inside 'add_laser')
a0 = 2.  # Laser amplitude
w0 = 50.e-6  # Laser waist
ctau = 5.e-6  # Laser duration
z0 = -10.e-6  # Laser centroid
zfoc = 0.e-6  # Focal position
lambda0 = 0.8e-6  # Laser wavelength

# The density profile
w_matched = 50.e-6
ramp_up = 5.e-3
plateau = 8.e-2
ramp_down = 5.e-3
Пример #7
0
def add_laser_pulse(sim,
                    laser_profile,
                    gamma_boost=None,
                    method='direct',
                    z0_antenna=None,
                    v_antenna=0.):
    """
    Introduce a laser pulse in the simulation.

    The laser is either added directly to the interpolation grid initially
    (method= ``direct``) or it is progressively emitted by an antenna
    (method= ``antenna``).

    Parameters
    ----------
    sim: a Simulation object
       The structure that contains the simulation.

    laser_profile: a valid laser profile object
        Laser profiles can be imported from ``fbpic.lpa_utils.laser``

    gamma_boost: float, optional
        When initializing the laser in a boosted frame, set the value of
        ``gamma_boost`` to the corresponding Lorentz factor.
        In this case, ``laser_profile`` should be initialized with all its
        physical quantities (wavelength, etc...) given in the lab frame,
        and the function ``add_laser_pulse`` will automatically convert
        these properties to their boosted-frame value.

    method: string, optional
        Whether to initialize the laser directly in the box
        (method= ``direct``) or through a laser antenna (method= ``antenna``).
        Default: ``direct``

    z0_antenna: float, optional (meters)
       Required for the ``antenna`` method: initial position
       (in the lab frame) of the antenna.

    v_antenna: float, optional (meters per second)
       Only used for the ``antenna`` method: velocity of the antenna
       (in the lab frame)

    Example
    -------
    In order to initialize a Laguerre-Gauss profile with a waist of
    5 microns and a duration of 30 femtoseconds, centered at :math:`z=3`
    microns initially:

    ::

        from fbpic.lpa_utils.laser import add_laser_pulse, LaguerreGaussLaser

        profile = LaguerreGaussLaser(a0=0.5, waist=5.e-6, tau=30.e-15, z0=3.e-6, p=1, m=0)

        add_laser_pulse( sim, profile )
    """
    # Prepare the boosted frame converter
    if (gamma_boost is not None) and (gamma_boost != 1.):
        if laser_profile.propag_direction == 1:
            boost = BoostConverter(gamma_boost)
        else:
            raise ValueError('For now, backward-propagating lasers '
                             'cannot be used in the boosted-frame.')
    else:
        boost = None

    # Handle the introduction method of the laser
    if method == 'direct':
        # Directly add the laser to the interpolation object
        add_laser_direct(sim, laser_profile, boost)

    elif method == 'antenna':
        # Add a laser antenna to the simulation object
        if z0_antenna is None:
            raise ValueError('You need to provide `z0_antenna`.')
        dr = sim.fld.interp[0].dr
        Nr = sim.fld.interp[0].Nr
        Nm = sim.fld.Nm
        sim.laser_antennas.append(
            LaserAntenna(laser_profile,
                         z0_antenna,
                         v_antenna,
                         dr,
                         Nr,
                         Nm,
                         boost,
                         use_cuda=sim.use_cuda))

    else:
        raise ValueError('Unknown laser method: %s' % method)
Пример #8
0
def add_laser(sim,
              a0,
              w0,
              ctau,
              z0,
              zf=None,
              lambda0=0.8e-6,
              cep_phase=0.,
              phi2_chirp=0.,
              theta_pol=0.,
              gamma_boost=None,
              method='direct',
              fw_propagating=True,
              update_spectral=True,
              z0_antenna=None):
    """
    Introduce a linearly-polarized, Gaussian laser in the simulation

    The laser is either added directly to the interpolation grid initially
    (method=`direct`) or it is progressively emitted by an antenna
    (method=`antenna`)

    Parameters
    ----------
    sim: a Simulation object
       The structure that contains the simulation.

    a0: float (unitless)
       The a0 of the pulse at focus (in the lab-frame).

    w0: float (in meters)
       The waist of the pulse at focus.

    ctau: float (in meters)
       The pulse length (in the lab-frame), defined as:
       E_laser ~ exp( - (z-ct)**2 / ctau**2 )

    z0: float (in meters)
       The position of the laser centroid relative to z=0 (in the lab-frame).

    zf: float (in meters), optional
       The position of the laser focus relative to z=0 (in the lab-frame).
       Default: the laser focus is at z0.

    lambda0: float (in meters), optional
       The central wavelength of the laser (in the lab-frame).
       Default: 0.8 microns (Ti:Sapph laser).

    cep_phase: float (rad)
        Carrier Enveloppe Phase (CEP), i.e. the phase of the laser
        oscillations, at the position where the laser enveloppe is maximum.

    phi2_chirp: float (in second^2)
        The amount of temporal chirp, at focus *in the lab frame*
        Namely, a wave packet centered on the frequency (w0 + dw) will
        reach its peak intensity at a time z(dw) = z0 - c*phi2*dw.
        Thus, a positive phi2 corresponds to positive chirp, i.e. red part
        of the spectrum in the front of the pulse and blue part of the
        spectrum in the back.

    theta_pol: float (in radians), optional
       The angle of polarization with respect to the x axis.
       Default: 0 rad.

    gamma_boost: float, optional
        When initializing the laser in a boosted frame, set the value of
        `gamma_boost` to the corresponding Lorentz factor. All the other
        quantities (ctau, zf, etc.) are to be given in the lab frame.

    method: string, optional
        Whether to initialize the laser directly in the box (method=`direct`)
        or through a laser antenna (method=`antenna`)

    fw_propagating: bool, optional
       Only for the `direct` method: Wether the laser is propagating in the
       forward or backward direction.

    update_spectral: bool, optional
       Only for the `direct` method: Wether to update the fields in spectral
       space after modifying the fields on the interpolation grid.

    z0_antenna: float, optional (meters)
       Only for the `antenna` method: initial position (in the lab frame)
       of the antenna. If not provided, then the z0_antenna is set to zf.
    """
    # Set a number of parameters for the laser
    k0 = 2 * np.pi / lambda0
    E0 = a0 * m_e * c**2 * k0 / e  # Amplitude at focus

    # Set default focusing position and laser antenna position
    if zf is None:
        zf = z0
    if z0_antenna is None:
        z0_antenna = z0

    # Prepare the boosted frame converter
    if (gamma_boost is not None) and (fw_propagating == True):
        boost = BoostConverter(gamma_boost)
    else:
        boost = None

    # Handle the introduction method of the laser
    if method == 'direct':
        # Directly add the laser to the interpolation object
        add_laser_direct(sim.fld, E0, w0, ctau, z0, zf, k0, cep_phase,
                         phi2_chirp, theta_pol, fw_propagating,
                         update_spectral, boost)
    elif method == 'antenna':
        dr = sim.fld.interp[0].dr
        Nr = sim.fld.interp[0].Nr
        Nm = sim.fld.Nm
        # Add a laser antenna to the simulation object
        sim.laser_antennas.append(
            LaserAntenna(E0,
                         w0,
                         ctau,
                         z0,
                         zf,
                         k0,
                         cep_phase,
                         phi2_chirp,
                         theta_pol,
                         z0_antenna,
                         dr,
                         Nr,
                         Nm,
                         boost=boost))
    else:
        raise ValueError('Unknown laser method: %s' % method)
Пример #9
0
def run_simulation(gamma_boost, show):
    """
    Run a simulation with a relativistic electron bunch crosses a laser

    Parameters
    ----------
    gamma_boost: float
        The Lorentz factor of the frame in which the simulation is carried out.
    show: bool
        Whether to show a plot of the angular distribution
    """
    # Boosted frame
    boost = BoostConverter(gamma_boost)

    # The simulation timestep
    diag_period = 100
    N_step = 101  # Number of iterations to perform
    # Calculate timestep to resolve the interaction with enough points
    laser_duration_boosted, = boost.copropag_length([laser_duration],
                                                    beta_object=-1)
    bunch_sigma_z_boosted, = boost.copropag_length([bunch_sigma_z],
                                                   beta_object=1)
    dt = (4 * laser_duration_boosted + bunch_sigma_z_boosted / c) / N_step

    # Initialize the simulation object
    zmax, zmin = boost.copropag_length([zmax_lab, zmin_lab], beta_object=1.)
    sim = Simulation(Nz,
                     zmax,
                     Nr,
                     rmax,
                     Nm,
                     dt,
                     p_zmin=0,
                     p_zmax=0,
                     p_rmin=0,
                     p_rmax=0,
                     p_nz=1,
                     p_nr=1,
                     p_nt=1,
                     n_e=1,
                     dens_func=None,
                     zmin=zmin,
                     boundaries='periodic',
                     use_cuda=use_cuda)
    # Remove particles that were previously created
    sim.ptcl = []
    print('Initialized simulation')

    # Add electron bunch (automatically converted to boosted-frame)
    add_elec_bunch_gaussian(sim,
                            sig_r=1.e-6,
                            sig_z=bunch_sigma_z,
                            n_emit=0.,
                            gamma0=gamma_bunch_mean,
                            sig_gamma=gamma_bunch_rms,
                            Q=Q_bunch,
                            N=N_bunch,
                            tf=0.0,
                            zf=0.5 * (zmax + zmin),
                            boost=boost)
    elec = sim.ptcl[0]
    print('Initialized electron bunch')
    # Add a photon species
    photons = Particles(q=0,
                        m=0,
                        n=0,
                        Npz=1,
                        zmin=0,
                        zmax=0,
                        Npr=1,
                        rmin=0,
                        rmax=0,
                        Nptheta=1,
                        dt=sim.dt,
                        ux_m=0.,
                        uy_m=0.,
                        uz_m=0.,
                        ux_th=0.,
                        uy_th=0.,
                        uz_th=0.,
                        dens_func=None,
                        continuous_injection=False,
                        grid_shape=sim.fld.interp[0].Ez.shape,
                        particle_shape='linear',
                        use_cuda=sim.use_cuda)
    sim.ptcl.append(photons)
    print('Initialized photons')

    # Activate Compton scattering for electrons of the bunch
    elec.activate_compton(target_species=photons,
                          laser_energy=laser_energy,
                          laser_wavelength=laser_wavelength,
                          laser_waist=laser_waist,
                          laser_ctau=laser_ctau,
                          laser_initial_z0=laser_initial_z0,
                          ratio_w_electron_photon=50,
                          boost=boost)
    print('Activated Compton')

    # Add diagnostics
    if write_hdf5:
        sim.diags = [
            ParticleDiagnostic(diag_period,
                               species={
                                   'electrons': elec,
                                   'photons': photons
                               },
                               comm=sim.comm)
        ]

    # Get initial total momentum
    initial_total_elec_px = (elec.w * elec.ux).sum() * m_e * c
    initial_total_elec_py = (elec.w * elec.uy).sum() * m_e * c
    initial_total_elec_pz = (elec.w * elec.uz).sum() * m_e * c

    ### Run the simulation
    for species in sim.ptcl:
        species.send_particles_to_gpu()

    for i_step in range(N_step):
        for species in sim.ptcl:
            species.halfpush_x()
        elec.handle_elementary_processes(sim.time + 0.5 * sim.dt)
        for species in sim.ptcl:
            species.halfpush_x()
        # Increment time and run diagnostics
        sim.time += sim.dt
        sim.iteration += 1
        for diag in sim.diags:
            diag.write(sim.iteration)
        # Print fraction of photons produced
        if i_step % 10 == 0:
            for species in sim.ptcl:
                species.receive_particles_from_gpu()
            simulated_frac = photons.w.sum() / elec.w.sum()
            for species in sim.ptcl:
                species.send_particles_to_gpu()
            print( 'Iteration %d: Photon fraction per electron = %f' \
                       %(i_step, simulated_frac) )

    for species in sim.ptcl:
        species.receive_particles_from_gpu()

    # Check estimation of photon fraction
    check_photon_fraction(simulated_frac)
    # Check conservation of momentum (is only conserved )
    if elec.compton_scatterer.ratio_w_electron_photon == 1:
        check_momentum_conservation(gamma_boost, photons, elec,
                                    initial_total_elec_px,
                                    initial_total_elec_py,
                                    initial_total_elec_pz)

    # Transform the photon momenta back into the lab frame
    photon_u = 1. / photons.inv_gamma
    photon_lab_pz = boost.gamma0 * (photons.uz + boost.beta0 * photon_u)
    photon_lab_p = boost.gamma0 * (photon_u + boost.beta0 * photons.uz)

    # Plot the scaled angle and frequency
    if show:
        import matplotlib.pyplot as plt
        # Bin the photons on a grid in frequency and angle
        freq_min = 0.5
        freq_max = 1.2
        N_freq = 500
        gammatheta_min = 0.
        gammatheta_max = 1.
        N_gammatheta = 100
        hist_range = [[freq_min, freq_max], [gammatheta_min, gammatheta_max]]
        extent = [freq_min, freq_max, gammatheta_min, gammatheta_max]
        fundamental_frequency = 4 * gamma_bunch_mean**2 * c / laser_wavelength
        photon_scaled_freq = photon_lab_p * c / (h * fundamental_frequency)
        gamma_theta = gamma_bunch_mean * np.arccos(
            photon_lab_pz / photon_lab_p)
        grid, freq_bins, gammatheta_bins = np.histogram2d(
            photon_scaled_freq,
            gamma_theta,
            weights=photons.w,
            range=hist_range,
            bins=[N_freq, N_gammatheta])
        # Normalize by solid angle, frequency and number of photons
        dw = (freq_bins[1] - freq_bins[0]) * 2 * np.pi * fundamental_frequency
        dtheta = (gammatheta_bins[1] - gammatheta_bins[0]) / gamma_bunch_mean
        domega = 2. * np.pi * np.sin(
            gammatheta_bins / gamma_bunch_mean) * dtheta
        grid /= dw * domega[np.newaxis, 1:] * elec.w.sum()
        grid = np.where(grid == 0, np.nan, grid)
        plt.imshow(grid.T,
                   origin='lower',
                   extent=extent,
                   cmap='gist_earth',
                   aspect='auto',
                   vmax=1.8e-16)
        plt.title('Particles, $d^2N/d\omega \,d\Omega$')
        plt.xlabel('Scaled energy ($\omega/4\gamma^2\omega_\ell$)')
        plt.ylabel(r'$\gamma \theta$')
        plt.colorbar()
        # Plot theory
        plt.plot(1. / (1 + gammatheta_bins**2), gammatheta_bins, color='r')
        plt.show()
        plt.clf()
Пример #10
0
def test_boosted_output(gamma_boost=10.):
    """
    # TODO

    Parameters
    ----------
    gamma_boost: float
        The Lorentz factor of the frame in which the simulation is carried out.
    """
    # The simulation box
    Nz = 500  # Number of gridpoints along z
    zmax_lab = 0.e-6  # Length of the box along z (meters)
    zmin_lab = -20.e-6
    Nr = 10  # Number of gridpoints along r
    rmax = 10.e-6  # Length of the box along r (meters)
    Nm = 2  # Number of modes used

    # Number of timesteps
    N_steps = 500
    diag_period = 20  # Period of the diagnostics in number of timesteps
    dt_lab = (zmax_lab - zmin_lab) / Nz * 1. / c
    T_sim_lab = N_steps * dt_lab

    # Move into directory `tests`
    os.chdir('./tests')

    # Initialize the simulation object
    sim = Simulation(
        Nz,
        zmax_lab,
        Nr,
        rmax,
        Nm,
        dt_lab,
        0,
        0,  # No electrons get created because we pass p_zmin=p_zmax=0
        0,
        rmax,
        1,
        1,
        4,
        n_e=0,
        zmin=zmin_lab,
        initialize_ions=False,
        gamma_boost=gamma_boost,
        v_comoving=-0.9999 * c,
        boundaries='open',
        use_cuda=use_cuda)
    sim.set_moving_window(v=c)
    # Remove the electron species
    sim.ptcl = []

    # Add a Gaussian electron bunch
    # Note: the total charge is 0 so all fields should remain 0
    # throughout the simulation. As a consequence, the motion of the beam
    # is a mere translation.
    N_particles = 3000
    add_elec_bunch_gaussian(sim,
                            sig_r=1.e-6,
                            sig_z=1.e-6,
                            n_emit=0.,
                            gamma0=100,
                            sig_gamma=0.,
                            Q=0.,
                            N=N_particles,
                            zf=0.5 * (zmax_lab + zmin_lab),
                            boost=BoostConverter(gamma_boost))
    sim.ptcl[0].track(sim.comm)

    # openPMD diagnostics
    sim.diags = [
        BackTransformedParticleDiagnostic(zmin_lab,
                                          zmax_lab,
                                          v_lab=c,
                                          dt_snapshots_lab=T_sim_lab / 3.,
                                          Ntot_snapshots_lab=3,
                                          gamma_boost=gamma_boost,
                                          period=diag_period,
                                          fldobject=sim.fld,
                                          species={"bunch": sim.ptcl[0]},
                                          comm=sim.comm)
    ]

    # Run the simulation
    sim.step(N_steps)

    # Check consistency of the back-transformed openPMD diagnostics:
    # Make sure that all the particles were retrived by checking particle IDs
    ts = OpenPMDTimeSeries('./lab_diags/hdf5/')
    ref_pid = np.sort(sim.ptcl[0].tracker.id)
    for iteration in ts.iterations:
        pid, = ts.get_particle(['id'], iteration=iteration)
        pid = np.sort(pid)
        assert len(pid) == N_particles
        assert np.all(ref_pid == pid)

    # Remove openPMD files
    shutil.rmtree('./lab_diags/')
    os.chdir('../')
Nr = 100  # Number of gridpoints along r
rmax = 100.e-6  # Length of the box along r (meters)
Nm = 2  # Number of modes used
n_order = -1  # The order of the stencil in z

# The simulation timestep
dt = (zmax - zmin) / Nz / c  # Timestep (seconds)
N_step = 800  # Number of iterations to perform
N_show = 40  # Number of timestep between every plot
show_fields = False
l_boost = False

if l_boost:
    # Boosted frame
    gamma_boost = 15.
    boost = BoostConverter(gamma_boost)
else:
    gamma_boost = 1.
    boost = None

# Initialize the simulation object
sim = Simulation(Nz,
                 zmax,
                 Nr,
                 rmax,
                 Nm,
                 dt,
                 -1.e-6,
                 0.,
                 -1.e-6,
                 0.,
Пример #12
0
def run_external_laser_field_simulation(show, gamma_boost=None):
    """
    Runs a simulation with a set of particles whose motion corresponds
    to that of a particle that is initially at rest (in the lab frame)
    before being reached by a plane wave (propagating to the right)

    In the lab frame, the motion is given by
    ux = a0 sin ( k0(z-ct) )
    uz = ux^2 / 2    (from the conservation of gamma - uz)

    In the boosted frame, the motion is given by
    ux = a0 sin ( k0 gamma0 (1-beta0) (z-ct) )
    uz = - gamma0 beta0 + gamma0 (1-beta0) ux^2 / 2
    """
    # Time parameters
    dt = lambda0 / c / 200  # 200 points per laser period
    N_step = 400  # Two laser periods

    # Initialize BoostConverter object
    if gamma_boost is None:
        boost = BoostConverter(gamma0=1.)
    else:
        boost = BoostConverter(gamma_boost)
    # Reduce time resolution, for the case of a boosted simulation
    if gamma_boost is not None:
        dt = dt * (1. + boost.beta0) / boost.gamma0

    # Initialize the simulation
    sim = Simulation(Nz,
                     zmax,
                     Nr,
                     rmax,
                     Nm,
                     dt,
                     initialize_ions=False,
                     zmin=zmin,
                     use_cuda=use_cuda,
                     boundaries='periodic',
                     gamma_boost=gamma_boost)
    # Add electrons
    sim.ptcl = []
    sim.add_new_species(-e,
                        m_e,
                        n=n,
                        p_rmax=p_rmax,
                        p_nz=p_nz,
                        p_nr=p_nr,
                        p_nt=p_nt)

    # Add the external fields
    sim.external_fields = [
        ExternalField(laser_func,
                      'Ex',
                      a0 * m_e * c**2 * k0 / e,
                      lambda0,
                      gamma_boost=gamma_boost),
        ExternalField(laser_func,
                      'By',
                      a0 * m_e * c * k0 / e,
                      lambda0,
                      gamma_boost=gamma_boost)
    ]

    # Prepare the arrays for the time history of the pusher
    Nptcl = sim.ptcl[0].Ntot
    x = np.zeros((N_step, Nptcl))
    y = np.zeros((N_step, Nptcl))
    z = np.zeros((N_step, Nptcl))
    ux = np.zeros((N_step, Nptcl))
    uy = np.zeros((N_step, Nptcl))
    uz = np.zeros((N_step, Nptcl))

    # Prepare the particles with proper transverse and longitudinal momentum,
    # at t=0 in the simulation frame
    k0p = k0 * boost.gamma0 * (1. - boost.beta0)
    sim.ptcl[0].ux = a0 * np.sin(k0p * sim.ptcl[0].z)
    sim.ptcl[0].uz[:] = -boost.gamma0*boost.beta0 \
                    + boost.gamma0*(1-boost.beta0)*0.5*sim.ptcl[0].ux**2

    # Push the particles over N_step and record the corresponding history
    for i in range(N_step):
        # Record the history
        x[i, :] = sim.ptcl[0].x[:]
        y[i, :] = sim.ptcl[0].y[:]
        z[i, :] = sim.ptcl[0].z[:]
        ux[i, :] = sim.ptcl[0].ux[:]
        uy[i, :] = sim.ptcl[0].uy[:]
        uz[i, :] = sim.ptcl[0].uz[:]
        # Take a simulation step
        sim.step(1)

    # Compute the analytical solution
    t = sim.dt * np.arange(N_step)
    # Conservation of ux
    ux_analytical = np.zeros((N_step, Nptcl))
    uz_analytical = np.zeros((N_step, Nptcl))
    for i in range(N_step):
        ux_analytical[i, :] = a0 * np.sin(k0p * (z[i, :] - c * t[i]))
        uz_analytical[i,:] = -boost.gamma0*boost.beta0 \
                    + boost.gamma0*(1-boost.beta0)*0.5*ux_analytical[i,:]**2

    # Show the results
    if show:
        import matplotlib.pyplot as plt
        plt.figure(figsize=(10, 5))

        plt.subplot(211)
        plt.plot(t, ux_analytical, '--')
        plt.plot(t, ux, 'o')
        plt.xlabel('t')
        plt.ylabel('ux')

        plt.subplot(212)
        plt.plot(t, uz_analytical, '--')
        plt.plot(t, uz, 'o')
        plt.xlabel('t')
        plt.ylabel('uz')

        plt.show()
    else:
        assert np.allclose(ux, ux_analytical, atol=5.e-2)
        assert np.allclose(uz, uz_analytical, atol=5.e-2)
Пример #13
0
use_cuda = True

# The simulation box
Nz = 100
zmax = 0.e-6
zmin = -20.e-6
Nr = 200
rmax = 20.e-6
Nm = 1
# The simulation timestep
dt = (zmax - zmin) / Nz / c
N_step = 101

# Boosted frame
gamma_boost = 15.
boost = BoostConverter(gamma_boost)

# The bunch
sigma_r = 1.e-6
sigma_z = 3.e-6
Q = 200.e-12
gamma0 = 100.
sigma_gamma = 0.
n_emit = 0.1e-6
z_focus = 2000.e-6
z0 = -10.e-6
N = 40000

# The diagnostics
diag_period = 5
Ntot_snapshot_lab = 21
Пример #14
0
    def __init__(self,
                 interp,
                 comm,
                 ptcl,
                 v,
                 p_nz,
                 time,
                 ux_m=0.,
                 uy_m=0.,
                 uz_m=0.,
                 ux_th=0.,
                 uy_th=0.,
                 uz_th=0.,
                 gamma_boost=None):
        """
        Initializes a moving window object.

        Parameters
        ----------
        interp: a list of Interpolation objects
            Contains the positions of the boundaries

        comm: a BoundaryCommunicator object
            Contains information about the MPI decomposition
            and about the longitudinal boundaries

        ptcl: a list of Particle objects
            Needed in order to infer the position of injection
            of the particles by the moving window.

        v: float (meters per seconds), optional
            The speed of the moving window

        p_nz: int
            Number of macroparticles per cell along the z direction

        time: float (seconds)
            The time (in the simulation) at which the moving
            window was initialized

        ux_m, uy_m, uz_m: floats (dimensionless)
           Normalized mean momenta of the injected particles in each direction

        ux_th, uy_th, uz_th: floats (dimensionless)
           Normalized thermal momenta in each direction

        gamma_boost : float, optional
            When initializing the laser in a boosted frame, set the
            value of `gamma_boost` to the corresponding Lorentz factor.
            (uz_m is to be given in the lab frame ; for the moment, this
            will not work if any of ux_th, uy_th, uz_th, ux_m, uy_m is nonzero)
        """
        # Check that the boundaries are open
        if ((comm.rank == comm.size-1) and (comm.right_proc is not None)) \
          or ((comm.rank == 0) and (comm.left_proc is not None)):
            raise ValueError(
                'The simulation is using a moving window, but '
                'the boundaries are periodic.\n Please select open '
                'boundaries when initializing the Simulation object.')

        # Momenta parameters
        self.ux_m = ux_m
        self.uy_m = uy_m
        self.uz_m = uz_m
        self.ux_th = ux_th
        self.uy_th = uy_th
        self.uz_th = uz_th

        # When running the simulation in boosted frame, convert the arguments
        if gamma_boost is not None:
            boost = BoostConverter(gamma_boost)
            self.uz_m, = boost.longitudinal_momentum([self.uz_m])

        # Attach moving window speed and period
        self.v = v
        self.exchange_period = comm.exchange_period

        # Attach reference position of moving window (only for the first proc)
        # (Determines by how many cells the window should be moved)
        if comm.rank == 0:
            self.zmin = interp[0].zmin

        # Attach injection position and speed (only for the last proc)
        if comm.rank == comm.size - 1:
            ng = comm.n_guard
            self.z_inject = interp[0].zmax - ng / 2 * interp[0].dz
            # Try to detect the position of the end of the plasma:
            # Find the maximal position of the particles which are
            # continously injected.
            self.z_end_plasma = None
            for species in ptcl:
                if species.continuous_injection and species.Ntot != 0:
                    # Add half of the spacing between particles (the injection
                    # function itself will add a half-spacing again)
                    self.z_end_plasma = species.z.max(
                    ) + 0.5 * interp[0].dz / p_nz
                    break
            # Default value in the absence of continuously-injected particles
            if self.z_end_plasma is None:
                self.z_end_plasma = self.z_inject
            self.v_end_plasma = \
              c * self.uz_m / np.sqrt(1 + ux_m**2 + uy_m**2 + self.uz_m**2)
            self.nz_inject = 0
            self.p_nz = p_nz

        # Attach time of last move
        self.t_last_move = time
Пример #15
0
    def __init__(self,
                 interp,
                 comm,
                 dt,
                 ptcl,
                 v,
                 p_nz,
                 time,
                 ux_m=0.,
                 uy_m=0.,
                 uz_m=0.,
                 ux_th=0.,
                 uy_th=0.,
                 uz_th=0.,
                 gamma_boost=None):
        """
        Initializes a moving window object.

        Parameters
        ----------
        interp: a list of Interpolation objects
            Contains the positions of the boundaries

        comm: a BoundaryCommunicator object
            Contains information about the MPI decomposition
            and about the longitudinal boundaries

        dt: float
            The timestep of the simulation.

        ptcl: a list of Particle objects
            Needed in order to infer the position of injection
            of the particles by the moving window.

        v: float (meters per seconds), optional
            The speed of the moving window

        p_nz: int
            Number of macroparticles per cell along the z direction

        time: float (seconds)
            The time (in the simulation) at which the moving
            window was initialized

        ux_m, uy_m, uz_m: floats (dimensionless)
           Normalized mean momenta of the injected particles in each direction

        ux_th, uy_th, uz_th: floats (dimensionless)
           Normalized thermal momenta in each direction

        gamma_boost : float, optional
            When initializing the laser in a boosted frame, set the
            value of `gamma_boost` to the corresponding Lorentz factor.
            (uz_m is to be given in the lab frame ; for the moment, this
            will not work if any of ux_th, uy_th, uz_th, ux_m, uy_m is nonzero)
        """
        # Check that the boundaries are open
        if ((comm.rank == comm.size-1) and (comm.right_proc is not None)) \
          or ((comm.rank == 0) and (comm.left_proc is not None)):
            raise ValueError(
                'The simulation is using a moving window, but '
                'the boundaries are periodic.\n Please select open '
                'boundaries when initializing the Simulation object.')

        # Momenta parameters
        self.ux_m = ux_m
        self.uy_m = uy_m
        self.uz_m = uz_m
        self.ux_th = ux_th
        self.uy_th = uy_th
        self.uz_th = uz_th

        # When running the simulation in boosted frame, convert the arguments
        if gamma_boost is not None:
            boost = BoostConverter(gamma_boost)
            self.uz_m, = boost.longitudinal_momentum([self.uz_m])

        # Attach moving window speed and period
        self.v = v

        # Get the positions of the global physical domain
        zmin_global_domain, zmax_global_domain = comm.get_zmin_zmax(
            local=False, with_damp=False, with_guard=False)

        # Attach reference position of moving window (only for the first proc)
        # (Determines by how many cells the window should be moved)
        if comm.rank == 0:
            self.zmin = zmin_global_domain

        # Attach injection position and speed (only for the last proc)
        if comm.rank == comm.size - 1:
            self.v_end_plasma = \
                c * self.uz_m / np.sqrt(1 + ux_m**2 + uy_m**2 + self.uz_m**2)
            # Initialize plasma *ahead* of the right *physical* boundary of
            # the box so, after `exchange_period` iterations
            # (without adding new plasma), there will still be plasma
            # inside the physical domain. ( +3 takes into account that 3 more
            # cells need to be filled w.r.t the left edge of the physical box
            # such that the last cell inside the box is always correct for
            # 1st and 3rd order shape factor particles after the moving window
            # shifted by exchange_period cells. )
            self.z_inject = zmax_global_domain + 3*comm.dz + \
                comm.exchange_period * (v-self.v_end_plasma) * dt
            # Try to detect the position of the end of the plasma:
            # Find the maximal position of the particles which are
            # continously injected.
            self.z_end_plasma = None
            for species in ptcl:
                if species.continuous_injection and species.Ntot != 0:
                    # Add half of the spacing between particles (the injection
                    # function itself will add a half-spacing again)
                    self.z_end_plasma = species.z.max() + 0.5 * comm.dz / p_nz
                    break
            # Default value in the absence of continuously-injected particles
            if self.z_end_plasma is None:
                self.z_end_plasma = zmax_global_domain
            self.nz_inject = 0
            self.p_nz = p_nz

        # Attach time of last move
        self.t_last_move = time - dt
Пример #16
0
def run_simulation(gamma_boost):
    """
    Run a simulation with a laser pulse going through a gas jet of ionizable
    N5+ atoms, and check the fraction of atoms that are in the N5+ state.

    Parameters
    ----------
    gamma_boost: float
        The Lorentz factor of the frame in which the simulation is carried out.
    """
    # The simulation box
    zmax_lab = 20.e-6  # Length of the box along z (meters)
    zmin_lab = 0.e-6
    Nr = 3  # Number of gridpoints along r
    rmax = 10.e-6  # Length of the box along r (meters)
    Nm = 2  # Number of modes used

    # The particles of the plasma
    p_zmin = 5.e-6  # Position of the beginning of the plasma (meters)
    p_zmax = 15.e-6
    p_rmin = 0.  # Minimal radial position of the plasma (meters)
    p_rmax = 100.e-6  # Maximal radial position of the plasma (meters)
    n_e = 1.  # The plasma density is chosen very low,
    # to avoid collective effects
    p_nz = 2  # Number of particles per cell along z
    p_nr = 1  # Number of particles per cell along r
    p_nt = 4  # Number of particles per cell along theta

    # Boosted frame
    boost = BoostConverter(gamma_boost)
    # Boost the different quantities
    beta_boost = np.sqrt(1. - 1. / gamma_boost**2)
    zmin, zmax = boost.static_length([zmin_lab, zmax_lab])
    p_zmin, p_zmax = boost.static_length([p_zmin, p_zmax])
    n_e, = boost.static_density([n_e])
    # Increase the number of particles per cell in order to keep sufficient
    # statistics for the evaluation of the ionization fraction
    if gamma_boost > 1:
        p_nz = int(2 * gamma_boost * (1 + beta_boost) * p_nz)

    # The laser
    a0 = 1.8  # Laser amplitude
    lambda0_lab = 0.8e-6  # Laser wavelength
    # Boost the laser wavelength before calculating the laser amplitude
    lambda0, = boost.copropag_length([lambda0_lab], beta_object=1.)
    # Duration and initial position of the laser
    ctau = 10. * lambda0
    z0 = -2 * ctau
    # Calculate laser amplitude
    omega = 2 * np.pi * c / lambda0
    E0 = a0 * m_e * c * omega / e
    B0 = E0 / c

    def laser_func(F, x, y, z, t, amplitude, length_scale):
        """
        Function that describes a Gaussian laser with infinite waist
        """
        return( F + amplitude * math.cos( 2*np.pi*(z-c*t)/lambda0 ) * \
                math.exp( - (z - c*t - z0)**2/ctau**2 ) )

    # Resolution and number of timesteps
    dz = lambda0 / 16.
    dt = dz / c
    Nz = int((zmax - zmin) / dz) + 1
    N_step = int(
        (2. * 40. * lambda0 + zmax - zmin) / (dz * (1 + beta_boost))) + 1

    # Get the speed of the plasma
    uz_m, = boost.longitudinal_momentum([0.])
    v_plasma, = boost.velocity([0.])

    # The diagnostics
    diag_period = N_step - 1  # Period of the diagnostics in number of timesteps

    # Initialize the simulation object
    sim = Simulation(
        Nz,
        zmax,
        Nr,
        rmax,
        Nm,
        dt,
        p_zmax,
        p_zmax,  # No electrons get created because we pass p_zmin=p_zmax
        p_rmin,
        p_rmax,
        p_nz,
        p_nr,
        p_nt,
        n_e,
        zmin=zmin,
        initialize_ions=False,
        v_comoving=v_plasma,
        use_galilean=False,
        boundaries='open',
        use_cuda=use_cuda)
    sim.set_moving_window(v=v_plasma)

    # Add the N atoms
    p_zmin, p_zmax, Npz = adapt_to_grid(sim.fld.interp[0].z, p_zmin, p_zmax,
                                        p_nz)
    p_rmin, p_rmax, Npr = adapt_to_grid(sim.fld.interp[0].r, p_rmin, p_rmax,
                                        p_nr)
    sim.ptcl.append(
        Particles(q=e,
                  m=14. * m_p,
                  n=0.2 * n_e,
                  Npz=Npz,
                  zmin=p_zmin,
                  zmax=p_zmax,
                  Npr=Npr,
                  rmin=p_rmin,
                  rmax=p_rmax,
                  Nptheta=p_nt,
                  dt=dt,
                  use_cuda=use_cuda,
                  uz_m=uz_m,
                  grid_shape=sim.fld.interp[0].Ez.shape,
                  continuous_injection=False))
    sim.ptcl[1].make_ionizable(element='N',
                               level_start=0,
                               target_species=sim.ptcl[0])

    # Add a laser to the fields of the simulation (external fields)
    sim.external_fields = [
        ExternalField(laser_func, 'Ex', E0, 0.),
        ExternalField(laser_func, 'By', B0, 0.)
    ]

    # Add a field diagnostic
    sim.diags = [
        ParticleDiagnostic(diag_period, {"ions": sim.ptcl[1]},
                           write_dir='tests/diags',
                           comm=sim.comm)
    ]
    if gamma_boost > 1:
        T_sim_lab = (2. * 40. * lambda0_lab + zmax_lab - zmin_lab) / c
        sim.diags.append(
            BoostedParticleDiagnostic(zmin_lab,
                                      zmax_lab,
                                      v_lab=0.,
                                      dt_snapshots_lab=T_sim_lab / 2.,
                                      Ntot_snapshots_lab=3,
                                      gamma_boost=gamma_boost,
                                      period=diag_period,
                                      fldobject=sim.fld,
                                      species={"ions": sim.ptcl[1]},
                                      comm=sim.comm,
                                      write_dir='tests/lab_diags'))

    # Run the simulation
    sim.step(N_step, use_true_rho=True)

    # Check the fraction of N5+ ions at the end of the simulation
    w = sim.ptcl[1].w
    ioniz_level = sim.ptcl[1].ionizer.ionization_level
    # Get the total number of N atoms/ions (all ionization levels together)
    ntot = w.sum()
    # Get the total number of N5+ ions
    n_N5 = w[ioniz_level == 5].sum()
    # Get the fraction of N5+ ions, and check that it is close to 0.32
    N5_fraction = n_N5 / ntot
    print('N5+ fraction: %.4f' % N5_fraction)
    assert ((N5_fraction > 0.30) and (N5_fraction < 0.34))

    # Check consistency in the regular openPMD diagnostics
    ts = OpenPMDTimeSeries('./tests/diags/hdf5/')
    last_iteration = ts.iterations[-1]
    w, q = ts.get_particle(['w', 'charge'],
                           species="ions",
                           iteration=last_iteration)
    # Check that the openPMD file contains the same number of N5+ ions
    n_N5_openpmd = np.sum(w[(4.5 * e < q) & (q < 5.5 * e)])
    assert np.isclose(n_N5_openpmd, n_N5)
    # Remove openPMD files
    shutil.rmtree('./tests/diags/')

    # Check consistency of the back-transformed openPMD diagnostics
    if gamma_boost > 1.:
        ts = OpenPMDTimeSeries('./tests/lab_diags/hdf5/')
        last_iteration = ts.iterations[-1]
        w, q = ts.get_particle(['w', 'charge'],
                               species="ions",
                               iteration=last_iteration)
        # Check that the openPMD file contains the same number of N5+ ions
        n_N5_openpmd = np.sum(w[(4.5 * e < q) & (q < 5.5 * e)])
        assert np.isclose(n_N5_openpmd, n_N5)
        # Remove openPMD files
        shutil.rmtree('./tests/lab_diags/')