Esempio n. 1
0
    def add_applied_field(self, applied_field):
        # Call method of parent class
        PICMI_Simulation.add_applied_field(self, applied_field)

        if type(applied_field) == PICMI_Mirror:
            assert applied_field.z_front_location is not None
            mirror = Mirror(z_lab=applied_field.z_front_location,
                            n_cells=applied_field.number_of_cells,
                            gamma_boost=self.fbpic_sim.boost.gamma0)
            self.fbpic_sim.mirrors.append(mirror)

        elif type(applied_field) == PICMI_ConstantAppliedField:
            # TODO: Handle bounds
            for field_name in ['Ex', 'Ey', 'Ez', 'Bx', 'By', 'Bz']:
                field_value = getattr(applied_field, field_name)
                if field_value is None:
                    continue

                def field_func(F, x, y, z, t, amplitude, length_scale):
                    return (F + amplitude * field_value)

                # Pass it to FBPIC
                self.fbpic_sim.external_fields.append(
                    ExternalField(field_func, field_name, 1., 0.))

        elif type(applied_field) == PICMI_AnalyticAppliedField:
            # TODO: Handle bounds
            for field_name in ['Ex', 'Ey', 'Ez', 'Bx', 'By', 'Bz']:
                # Extract expression and execute it inside a function definition
                expression = getattr(applied_field, field_name + '_expression')
                if expression is None:
                    continue
                fieldfunc = None
                define_function_code = \
                """def fieldfunc( F, x, y, z, t, amplitude, length_scale ):\n    return( F + amplitude * %s )""" %expression
                # Take into account user-defined variables
                for k in applied_field.user_defined_kw:
                    define_function_code = \
                        "%s = %s\n" %(k,applied_field.user_defined_kw[k]) \
                        + define_function_code
                exec(define_function_code, globals())
                # Pass it to FBPIC
                self.fbpic_sim.external_fields.append(
                    ExternalField(fieldfunc, field_name, 1., 0.))

        else:
            raise ValueError("Unrecognized `applied_field` type.")
Esempio n. 2
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/')
Esempio n. 3
0
def test_external_laser_field(show=False):
    "Function that is run by py.test, when doing `python setup.py test`"

    # Initialize the simulation
    sim = Simulation(Nz,
                     zmax,
                     Nr,
                     rmax,
                     Nm,
                     dt,
                     p_zmin,
                     p_zmax,
                     0,
                     p_rmax,
                     p_nz,
                     p_nr,
                     p_nt,
                     n,
                     initialize_ions=False,
                     zmin=zmin,
                     use_cuda=use_cuda,
                     boundaries='periodic')

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

    # 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))
    uz = np.zeros((N_step, Nptcl))
    uy = np.zeros((N_step, Nptcl))

    # Prepare the particles with proper transverse and longitudinal momentum
    sim.ptcl[0].ux = a0 * np.sin(k0 * sim.ptcl[0].z)
    sim.ptcl[0].uz[:] = 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 = 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(k0 * (z[i, :] - c * t[i]))
        uz_analytical[i, :] = 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)
Esempio n. 4
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)
Esempio n. 5
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/')