Exemplo n.º 1
0
def test_particle_list_sort_with_key_and_reverse():
    """
    Test that a `ParticleList` instance can be sorted if a key is
    provided, and that the ``reverse`` keyword argument works too.
    """
    elements = ["He", "H", "Fe", "U"]
    particle_list = ParticleList(elements)
    particle_list.sort(key=atomic_number, reverse=True)
    assert particle_list.symbols == ["U", "Fe", "He", "H"]
Exemplo n.º 2
0
def test_particle_list_gt_as_nuclear_reaction_energy():
    """
    Test that `ParticleList.__gt__` can be used to get the same result
    as `nuclear_reaction_energy`.
    """
    reactants = ParticleList(["D+", "T+"])
    products = ParticleList(["alpha", "n"])
    expected_energy = nuclear_reaction_energy("D + T --> alpha + n")
    actual_energy = reactants > products
    assert u.allclose(expected_energy, actual_energy)
Exemplo n.º 3
0
def test_mean_particle():
    """
    Test that ``ParticleList.average_particle()`` returns a particle with
    the mean mass and mean charge of a |ParticleList|.
    """
    massless_uncharged_particle = CustomParticle(mass=0 * u.kg, charge=0 * u.C)
    particle_list = ParticleList([proton, electron, alpha, massless_uncharged_particle])
    expected_mass = (proton.mass + electron.mass + alpha.mass) / 4
    expected_charge = (proton.charge + electron.charge + alpha.charge) / 4
    average_particle = particle_list.average_particle()
    assert u.isclose(average_particle.mass, expected_mass, rtol=1e-14)
    assert u.isclose(average_particle.charge, expected_charge, rtol=1e-14)
Exemplo n.º 4
0
def test_weighted_mean_particle():
    """
    Test that ``ParticleList.average_particle()`` returns a particle with
    the weighted mean.
    """
    custom_proton = CustomParticle(mass=proton.mass, charge=proton.charge)
    particle_list = ParticleList([proton, electron, alpha, custom_proton])
    abundances = [1, 2, 0, 1]
    expected_mass = (proton.mass + electron.mass) / 2
    expected_charge = 0 * u.C
    average_particle = particle_list.average_particle(abundances=abundances)
    assert u.isclose(average_particle.mass, expected_mass, rtol=1e-14)
    assert u.isclose(average_particle.charge, expected_charge, rtol=1e-14)
Exemplo n.º 5
0
def test_weighted_averages_of_particles(
    particle_multiplicities: Dict[ParticleLike, int],
    use_rms_charge,
    use_rms_mass,
):
    """
    Compare the mass and charge of the average particle for two |ParticleList|
    instances.

    The first |ParticleList| contains repeated particles.

    The second |ParticleList| contains only one of each kind of particle
    present in the first list, with the number of each particle recorded
    in a separate array.

    The unweighted averages of the first |ParticleList| should equal the
    weighted averages of the second |ParticleList|, with the number of
    each particle provided as the abundances.
    """
    all_particles = ParticleList([])
    for particle, multiplicity in particle_multiplicities.items():
        all_particles.extend(ParticleList(multiplicity * [particle]))

    unique_particles = ParticleList(particle_multiplicities.keys())
    number_of_each_particle = list(particle_multiplicities.values())

    unweighted_mean_of_all_particles = all_particles.average_particle(
        use_rms_charge=use_rms_charge,
        use_rms_mass=use_rms_mass,
    )

    weighted_mean_of_unique_particles = unique_particles.average_particle(
        use_rms_charge=use_rms_charge,
        use_rms_mass=use_rms_mass,
        abundances=number_of_each_particle,
    )

    assert u.isclose(
        unweighted_mean_of_all_particles.mass,
        weighted_mean_of_unique_particles.mass,
        rtol=1e-14,
        equal_nan=True,
    )

    assert u.isclose(
        unweighted_mean_of_all_particles.charge,
        weighted_mean_of_unique_particles.charge,
        rtol=1e-14,
        equal_nan=True,
    )

    if len(unique_particles) == 1 and isinstance(unique_particles[0],
                                                 Particle):
        assert isinstance(unweighted_mean_of_all_particles, Particle)
        assert isinstance(weighted_mean_of_unique_particles, Particle)
Exemplo n.º 6
0
def test_comparison_to_equivalent_particle_list(physical_property, use_rms):
    """
    Test that `IonizationState.average_ion` gives consistent results with
    `ParticleList.average_particle` when the ratios of different particles
    is the same between the `IonizationState` and the `ParticleList`.
    """
    particles = ParticleList(2 * ["He-4 0+"] + 3 * ["He-4 1+"] + 5 * ["He-4 2+"])
    ionization_state = IonizationState("He-4", [0.2, 0.3, 0.5])
    kwargs = {f"use_rms_{physical_property}": True}
    expected_average_particle = particles.average_particle(**kwargs)
    expected_average_quantity = getattr(expected_average_particle, physical_property)
    actual_average_particle = ionization_state.average_ion(**kwargs)
    actual_average_quantity = getattr(actual_average_particle, physical_property)
    assert_quantity_allclose(actual_average_quantity, expected_average_quantity)
Exemplo n.º 7
0
def test_particle_list_instantiate_with_invalid_particles():
    """
    Test that a `ParticleList` instance cannot be created when it is
    provided with invalid particles.
    """
    with pytest.raises(InvalidParticleError):
        ParticleList(invalid_particles)
Exemplo n.º 8
0
def test_particle_list_dimensionless_particles():
    """
    Test that a `ParticleList` cannot be instantiated with a
    `DimensionlessParticle`.
    """
    with pytest.raises(TypeError):
        ParticleList([DimensionlessParticle()])
Exemplo n.º 9
0
def test_particle_list_membership(args):
    """
    Test that the particles in the `ParticleList` match the particles
    (or particle-like objects) that are passed to it.
    """
    particle_list = ParticleList(args)
    for arg, particle in zip(args, particle_list):
        assert particle == arg
    assert _everything_is_particle_or_custom_particle(particle_list)
    assert _everything_is_particle_or_custom_particle(particle_list.data)
Exemplo n.º 10
0
def test_root_mean_square_particle(use_rms_charge, use_rms_mass):
    """
    Test that ``ParticleList.average_particle`` returns the mean or root
    mean square of the charge and mass, as appropriate.
    """

    particle_list = ParticleList(["p+", "e-"])
    average_particle = particle_list.average_particle(
        use_rms_charge=use_rms_charge, use_rms_mass=use_rms_mass
    )

    expected_average_charge = (1 if use_rms_charge else 0) * proton.charge
    assert u.isclose(average_particle.charge, expected_average_charge, rtol=1e-14)

    if use_rms_mass:
        expected_average_mass = np.sqrt((proton.mass**2 + electron.mass**2) / 2)
    else:
        expected_average_mass = (proton.mass + electron.mass) / 2

    assert u.isclose(average_particle.mass, expected_average_mass, atol=1e-35 * u.kg)
Exemplo n.º 11
0
def various_particles():
    """A sample `ParticleList` with several different valid particles."""
    return ParticleList([
        "H",
        "He",
        "e-",
        "alpha",
        "tau neutrino",
        CustomParticle(mass=3 * u.kg, charge=5 * u.C),
        CustomParticle(),
        CustomParticle(mass=7 * u.kg),
        CustomParticle(charge=11 * u.C),
    ])
Exemplo n.º 12
0
def test_particle_list_attributes(attribute, various_particles):
    """
    Test that the attributes of ParticleList correspond to the
    attributes of the listed particles.

    This class does not test ParticleList instances that include
    CustomParticle instances inside of them.
    """
    particle_list_arguments = (electron, "e+", proton, neutron, alpha)
    particle_list = ParticleList(particle_list_arguments)
    expected_particles = [Particle(arg) for arg in particle_list_arguments]
    actual = getattr(particle_list, attribute)
    expected = [getattr(particle, attribute) for particle in expected_particles]
    assert u.allclose(actual, expected, equal_nan=True)
Exemplo n.º 13
0
def test_particle_list_with_no_arguments():
    """Test that `ParticleList()` returns an empty `ParticleList`."""
    empty_particle_list = ParticleList()
    assert isinstance(empty_particle_list, ParticleList)
    assert len(empty_particle_list) == 0
Exemplo n.º 14
0
            if key not in params:
                raise ValueError(
                    f"{p} was not provided in kwarg 'parameters', but is required."
                )

    # **************
    # ions
    # **************

    ions = settings["ions"]
    # Condition ions
    # If a single value is provided, turn into a particle list
    if isinstance(ions, ParticleList):
        pass
    elif isinstance(ions, str):
        ions = ParticleList([Particle(ions)])
    # If a list is provided, ensure all values are Particles, then convert
    # to a ParticleList
    elif isinstance(ions, list):
        for ii, ion in enumerate(ions):
            if isinstance(ion, Particle):
                continue
            ions[ii] = Particle(ion)
        ions = ParticleList(ions)
    else:
        raise ValueError("The type of object provided to the ``ions`` keyword "
                         f"is not supported: {type(ions)}")

    # Validate ions
    if len(ions) == 0:
        raise ValueError("At least one ion species needs to be defined.")
Exemplo n.º 15
0
def test_particle_list_is_category(particles, args, kwargs, expected):
    """
    Test that ``ParticleList.is_category()`` behaves as expected.
    """
    sample_list = ParticleList(particles)
    assert sample_list.is_category(*args, **kwargs) == expected
Exemplo n.º 16
0
def spectral_density(
    wavelengths: u.nm,
    probe_wavelength: u.nm,
    n: u.m**-3,
    *,
    T_e: u.K,
    T_i: u.K,
    efract: np.ndarray = None,
    ifract: np.ndarray = None,
    ions: Union[str, List[str], Particle, List[Particle]] = "p",
    electron_vel: u.m / u.s = None,
    ion_vel: u.m / u.s = None,
    probe_vec=None,
    scatter_vec=None,
    instr_func=None,
) -> Tuple[Union[np.floating, np.ndarray], np.ndarray]:
    r"""
    Calculate the spectral density function for Thomson scattering of a
    probe laser beam by a multi-species Maxwellian plasma. See **Notes**
    section below for additional details.

    Parameters
    ----------

    wavelengths : `~astropy.units.Quantity`
        Array of wavelengths over which the spectral density function
        will be calculated. (convertible to nm)

    probe_wavelength : `~astropy.units.Quantity`
        Wavelength of the probe laser. (convertible to nm)

    n : `~astropy.units.Quantity`
        Total combined number density of all electron populations.
        (convertible to cm\ :sup:`-3`)

    T_e : `~astropy.units.Quantity`, keyword-only, shape (Ne, )
        Temperature of each electron component. Shape (Ne, ) must be equal to the
        number of electron populations Ne. (in K or convertible to eV)

    T_i : `~astropy.units.Quantity`, keyword-only, shape (Ni, )
        Temperature of each ion component. Shape (Ni, ) must be equal to the
        number of ion populations Ni. (in K or convertible to eV)

    efract : array_like, shape (Ne, ), optional
        An array-like object representing :math:`F_e` (defined above).
        Must sum to 1.0. Default is [1.0], representing a single
        electron component.

    ifract : array_like, shape (Ni, ), optional
        An array-like object representing :math:`F_i` (defined above).
        Must sum to 1.0. Default is [1.0], representing a single
        ion component.

    ions : `str` or `~plasmapy.particles.particle_class.Particle` or
           `~plasmapy.particles.particle_collections.ParticleList`,
           shape (Ni, ), optional

        A list or single instance of `~plasmapy.particles.particle_class.Particle`, or
        strings convertible to `~plasmapy.particles.particle_class.Particle`,
        or a `~plasmapy.particles.particle_collections.ParticleList`. All ions
        must be positively charged. Default is ``'H+'`` corresponding to a
        single species of hydrogen ions.

    electron_vel : `~astropy.units.Quantity`, shape (Ne, 3), optional
        Velocity of each electron population in the rest frame. (convertible to m/s)
        If set, overrides ``electron_vdir`` and ``electron_speed``.
        Defaults to a stationary plasma [0, 0, 0] m/s.

    ion_vel : `~astropy.units.Quantity`, shape (Ni, 3), optional
        Velocity vectors for each electron population in the rest frame
        (convertible to m/s). If set, overrides ``ion_vdir`` and ``ion_speed``.
        Defaults to zero drift for all specified ion species.

    probe_vec : float `~numpy.ndarray`, shape (3, )
        Unit vector in the direction of the probe laser. Defaults to
        [1, 0, 0].

    scatter_vec : float `~numpy.ndarray`, shape (3, )
        Unit vector pointing from the scattering volume to the detector.
        Defaults to [0, 1, 0] which, along with the default ``probe_vec``,
        corresponds to a 90° scattering angle geometry.

    instr_func : function
        A function representing the instrument function that takes a `~astropy.units.Quantity`
        of wavelengths (centered on zero) and returns the instrument point
        spread function. The resulting array will be convolved with the
        spectral density function before it is returned.

    Returns
    -------
    alpha : `float`
        Mean scattering parameter, where ``alpha`` > 1 corresponds to collective
        scattering and ``alpha`` < 1 indicates non-collective scattering. The
        scattering parameter is calculated based on the total plasma density ``n``.

    Skw : `~astropy.units.Quantity`
        Computed spectral density function over the input ``wavelengths`` array
        with units of s/rad.

    Notes
    -----

    This function calculates the spectral density function for Thomson
    scattering of a probe laser beam by a plasma consisting of one or more ion
    species and one or more thermal electron populations (the entire plasma
    is assumed to be quasi-neutral)

    .. math::
        S(k,ω) = \sum_e \frac{2π}{k}
        \bigg |1 - \frac{χ_e}{ε} \bigg |^2
        f_{e0,e} \bigg (\frac{ω}{k} \bigg ) +
        \sum_i \frac{2π Z_i}{k}
        \bigg |\frac{χ_e}{ε} \bigg |^2 f_{i0,i}
        \bigg ( \frac{ω}{k} \bigg )

    where :math:`χ_e` is the electron component susceptibility of the
    plasma and :math:`ε = 1 + \sum_e χ_e + \sum_i χ_i` is the total
    plasma dielectric function (with :math:`χ_i` being the ion component
    of the susceptibility), :math:`Z_i` is the charge of each ion, :math:`k`
    is the scattering wavenumber, :math:`ω` is the scattering frequency,
    and :math:`f_{e0,e}` and :math:`f_{i0,i}` are the electron and ion velocity
    distribution functions respectively. In this function the electron and ion
    velocity distribution functions are assumed to be Maxwellian, making this
    function equivalent to Eq. 3.4.6 in :cite:t:`sheffield:2011`\ .

    The number density of the e\ :sup:`th` electron populations is defined as

    .. math::
        n_e = F_e n

    where :math:`n` is total density of all electron population combined and
    :math:`F_e` is the fractional density of each electron population such
    that

    .. math::
        \sum_e n_e = n

    .. math::
        \sum_e F_e = 1

    The plasma is assumed to be charge neutral, and therefore the number
    density of the i\ :sup:`th` ion population is

    .. math::
        n_i = \frac{F_i n}{\sum_i F_i Z_i}

    with :math:`F_i` defined in the same way as :math:`F_e`.

    For details, see "Plasma Scattering of Electromagnetic Radiation" by
    :cite:t:`sheffield:2011`. This code is a modified version of the
    program described therein.

    For a concise summary of the relevant physics, see Chapter 5 of
    the :cite:t:`schaeffer:2014` thesis.
    """

    # Validate efract
    if efract is None:
        efract = np.ones(1)
    else:
        efract = np.asarray(efract, dtype=np.float64)
        if np.sum(efract) != 1:
            raise ValueError(
                f"The provided efract does not sum to 1: {efract}")

    # Validate ifract
    if ifract is None:
        ifract = np.ones(1)
    else:
        ifract = np.asarray(ifract, dtype=np.float64)
        if np.sum(ifract) != 1:
            raise ValueError(
                f"The provided ifract does not sum to 1: {ifract}")

    if probe_vec is None:
        probe_vec = np.array([1, 0, 0])

    if scatter_vec is None:
        scatter_vec = np.array([0, 1, 0])

    # If electron velocity is not specified, create an array corresponding
    # to zero drift
    if electron_vel is None:
        electron_vel = np.zeros([efract.size, 3]) * u.m / u.s

    # Condition the electron velocity keywords
    if ion_vel is None:
        ion_vel = np.zeros([ifract.size, 3]) * u.m / u.s

    # Condition ions
    # If a single value is provided, turn into a particle list
    if isinstance(ions, ParticleList):
        pass
    elif isinstance(ions, str):
        ions = ParticleList([Particle(ions)])
    # If a list is provided, ensure all values are Particles, then convert
    # to a ParticleList
    elif isinstance(ions, list):
        for ii, ion in enumerate(ions):
            if isinstance(ion, Particle):
                continue
            ions[ii] = Particle(ion)
        ions = ParticleList(ions)
    else:
        raise ValueError("The type of object provided to the ``ions`` keyword "
                         f"is not supported: {type(ions)}")

    # Validate ions
    if len(ions) == 0:
        raise ValueError("At least one ion species needs to be defined.")

    try:
        if sum(ion.charge_number <= 0 for ion in ions):
            raise ValueError("All ions must be positively charged.")
    # Catch error if charge information is missing
    except ChargeError:
        raise ValueError("All ions must be positively charged.")

    # Condition T_i
    if T_i.size == 1:
        # If a single quantity is given, put it in an array so it's iterable
        # If T_i.size != len(ions), assume same temp. for all species
        T_i = np.array([T_i.value]) * T_i.unit

    # Make sure the sizes of ions, ifract, ion_vel, and T_i all match
    if ((len(ions) != ifract.size) or (ion_vel.shape[0] != ifract.size)
            or (T_i.size != ifract.size)):
        raise ValueError(
            f"Inconsistent number of ion species in ifract ({ifract}), "
            f"ions ({len(ions)}), T_i ({T_i.size}), "
            f"and/or ion_vel ({ion_vel.shape[0]}).")

    # Condition T_e
    if T_e.size == 1:
        # If a single quantity is given, put it in an array so it's iterable
        # If T_e.size != len(efract), assume same temp. for all species
        T_e = np.array([T_e.value]) * T_e.unit

    # Make sure the sizes of efract, electron_vel, and T_e all match
    if (electron_vel.shape[0] != efract.size) or (T_e.size != efract.size):
        raise ValueError(
            f"Inconsistent number of electron populations in efract ({efract.size}), "
            f"T_e ({T_e.size}), or electron velocity ({electron_vel.shape[0]})."
        )

    # Create arrays of ion Z and mass from particles given
    ion_z = ions.charge_number
    ion_mass = ions.mass

    probe_vec = probe_vec / np.linalg.norm(probe_vec)
    scatter_vec = scatter_vec / np.linalg.norm(scatter_vec)

    # Apply the instrument function
    if instr_func is not None and callable(instr_func):

        # Create an array of wavelengths of the same size as wavelengths
        # but centered on zero
        wspan = (np.max(wavelengths) - np.min(wavelengths)) / 2
        eval_w = np.linspace(-wspan, wspan, num=wavelengths.size)
        instr_func_arr = instr_func(eval_w)

        if type(instr_func_arr) != np.ndarray:
            raise ValueError("instr_func must be a function that returns a "
                             "np.ndarray, but the provided function returns "
                             f" a {type(instr_func_arr)}")

        if wavelengths.shape != instr_func_arr.shape:
            raise ValueError("The shape of the array returned from the "
                             f"instr_func ({instr_func_arr.shape}) "
                             "does not match the shape of the wavelengths "
                             f"array ({wavelengths.shape}).")

        instr_func_arr /= np.sum(instr_func_arr)
    else:
        instr_func_arr = None

    alpha, Skw = spectral_density_lite(
        wavelengths.to(u.m).value,
        probe_wavelength.to(u.m).value,
        n.to(u.m**-3).value,
        T_e.to(u.K).value,
        T_i.to(u.K).value,
        efract=efract,
        ifract=ifract,
        ion_z=ion_z,
        ion_mass=ion_mass.to(u.kg).value,
        ion_vel=ion_vel.to(u.m / u.s).value,
        electron_vel=electron_vel.to(u.m / u.s).value,
        probe_vec=probe_vec,
        scatter_vec=scatter_vec,
        instr_func_arr=instr_func_arr,
    )

    return alpha, Skw * u.s / u.rad
Exemplo n.º 17
0
def test_particle_list_adding_particle_list(various_particles):
    """Test that a `ParticleList` can be added to another `ParticleList`."""
    extra_particles = ParticleList(["H", "D", "T"])
    new_particles_list = various_particles + extra_particles
    assert new_particles_list[-3:] == extra_particles
    assert isinstance(new_particles_list, ParticleList)
Exemplo n.º 18
0
     None,
     None,
 ),
 # List of Particles
 (
     {
         "ions": [
             Particle("p+"),
         ]
     },
     None,
     None,
 ),
 # Particle list
 ({
     "ions": ParticleList(["p+"])
 }, None, None),
 # ValueError when an ion is negative
 (
     {
         "ions": ParticleList(["p-"])
     },
     ValueError,
     "All ions must be positively charged.",
 ),
 # ValueError when an ion charge information is not provided
 (
     {
         "ions": ParticleList(["He"])
     },
     ValueError,
Exemplo n.º 19
0
def test_particle_list_extended_with_particle_list(various_particles):
    """Test that a `ParticleList` can be extended with another `ParticleList`."""
    particle_list = ParticleList(["D", "T", CustomParticle()])
    various_particles.extend(particle_list)
    assert various_particles[-3:] == particle_list
Exemplo n.º 20
0
def test_particle_list_len():
    """Test that using `len` on a `ParticleList` returns the expected number."""
    original_list = ["n", "p", "e-"]
    particle_list = ParticleList(original_list)
    assert len(particle_list) == len(original_list)
Exemplo n.º 21
0
import astropy.units as u
import pytest

from plasmapy.particles import deuteron, electron, proton
from plasmapy.particles._factory import _physical_particle_factory
from plasmapy.particles.exceptions import InvalidParticleError
from plasmapy.particles.particle_class import CustomParticle, Particle
from plasmapy.particles.particle_collections import ParticleList

mass = 1e-26 * u.kg
charge = 1e-29 * u.C
custom_particle = CustomParticle(mass, charge)

test_cases = [
    ([[]], {}, ParticleList()),
    ([proton], {}, proton),
    (["p+"], {}, proton),
    (["H"], {
        "Z": 1,
        "mass_numb": 2
    }, deuteron),
    (["muon"], {}, Particle("muon")),
    pytest.param([charge, mass], {},
                 custom_particle,
                 marks=[pytest.mark.xfail]),
    ([mass, charge], {}, custom_particle),
    ([], {
        "symbol": "ξ"
    }, CustomParticle(symbol="ξ")),
    ([[proton, electron]], {}, ParticleList([proton, electron])),
    ([], {
Exemplo n.º 22
0
    def average_ion(
        self,
        *,
        include_neutrals: bool = True,
        use_rms_charge: bool = False,
        use_rms_mass: bool = False,
    ) -> CustomParticle:
        """
        Return a |CustomParticle| representing the mean particle
        included across all ionization states.

        By default, this method will use the weighted mean to calculate
        the properties of the |CustomParticle|, where the weights for
        each ionic level is given by its ionic fraction multiplied by
        the abundance of the base element or isotope. If
        ``use_rms_charge`` or ``use_rms_mass`` is `True`, then this
        method will return the root mean square of the charge or mass,
        respectively.

        Parameters
        ----------
        include_neutrals : `bool`, optional, keyword-only
            If `True`, include neutrals when calculating the mean values
            of the different particles.  If `False`, exclude neutrals.
            Defaults to `True`.

        use_rms_charge : `bool`, optional, keyword-only
            If `True`, use the root mean square charge instead of the
            mean charge. Defaults to `False`.

        use_rms_mass : `bool`, optional, keyword-only
            If `True`, use the root mean square mass instead of the mean
            mass. Defaults to `False`.

        Raises
        ------
        `~plasmapy.particles.exceptions.ParticleError`
            If the abundance of any of the elements or isotopes is not
            defined and the |IonizationStateCollection| instance includes
            more than one element or isotope.

        Returns
        -------
        ~plasmapy.particles.particle_class.CustomParticle

        Examples
        --------
        >>> states = IonizationStateCollection(
        ...     {"H": [0.1, 0.9], "He": [0, 0.1, 0.9]},
        ...     abundances={"H": 1, "He": 0.1}
        ... )
        >>> states.average_ion()
        CustomParticle(mass=2.12498...e-27 kg, charge=1.5876...e-19 C)
        >>> states.average_ion(include_neutrals=False, use_rms_charge=True, use_rms_mass=True)
        CustomParticle(mass=2.633...e-27 kg, charge=1.805...e-19 C)
        """
        min_charge = 0 if include_neutrals else 1

        all_particles = ParticleList()
        all_abundances = []

        for base_particle in self.base_particles:

            ionization_state = self[base_particle]
            ionic_levels = ionization_state.to_list()[min_charge:]
            all_particles.extend(ionic_levels)

            base_particle_abundance = self.abundances[base_particle]

            if np.isnan(base_particle_abundance):
                if len(self) == 1:
                    base_particle_abundance = 1
                else:
                    raise ParticleError(
                        "Unable to provide an average particle without abundances."
                    )

            ionic_fractions = ionization_state.ionic_fractions[min_charge:]
            ionic_abundances = base_particle_abundance * ionic_fractions
            all_abundances.extend(ionic_abundances)

        return all_particles.average_particle(
            use_rms_charge=use_rms_charge,
            use_rms_mass=use_rms_mass,
            abundances=all_abundances,
        )