Пример #1
0
from plasmapy.particles.particle_collections import ParticleList
from plasmapy.particles.serialization import (
    json_load_particle,
    json_loads_particle,
    ParticleJSONDecoder,
)
from plasmapy.particles.special_particles import ParticleZoo
from plasmapy.particles.symbols import (
    atomic_symbol,
    element_name,
    ionic_symbol,
    isotope_symbol,
    particle_symbol,
)

proton = Particle("p+")
"""A `Particle` instance representing a proton."""

electron = Particle("e-")
"""A `Particle` instance representing an electron."""

neutron = Particle("n")
"""A `Particle` instance representing a neutron."""

positron = Particle("e+")
"""A `Particle` instance representing a positron."""

deuteron = Particle("D 1+")
"""A `Particle` instance representing a positively charged deuterium ion."""

triton = Particle("T 1+")
Пример #2
0
    def ionic_fractions(self, inputs: Union[Dict, List, Tuple]):
        """
        Set the ionic fractions.

        Notes
        -----
        The ionic fractions are initialized during instantiation of
        `~plasmapy.particles.IonizationStates`.  After this, the only way
        to reset the ionic fractions via the ``ionic_fractions``
        attribute is via a `dict` with elements or isotopes that are a
        superset of the previous elements or isotopes.  However, you may
        use item assignment of the `~plasmapy.particles.IonizationState`
        instance to assign new ionic fractions one element or isotope
        at a time.

        Raises
        ------
        AtomicError
            If the ionic fractions cannot be set.

        TypeError
            If ``inputs`` is not a `list`, `tuple`, or `dict` during
            instantiation, or if ``inputs`` is not a `dict` when it is
            being set.

        """

        # A potential problem is that using item assignment on the
        # ionic_fractions attribute could cause the original attributes
        # to be overwritten without checks being performed.  We might
        # eventually want to create a new class or subclass of UserDict
        # that goes through these checks.  In the meantime, we should
        # make it clear to users to set ionic_fractions by using item
        # assignment on the IonizationStates instance as a whole.  An
        # example of the problem is `s = IonizationStates(["He"])` being
        # followed by `s.ionic_fractions["He"] = 0.3`.

        if hasattr(self, "_ionic_fractions"):
            if not isinstance(inputs, dict):
                raise TypeError(
                    "Can only reset ionic_fractions with a dict if "
                    "ionic_fractions has been set already.")
            old_particles = set(self.base_particles)
            new_particles = {particle_symbol(key) for key in inputs.keys()}
            missing_particles = old_particles - new_particles
            if missing_particles:
                raise AtomicError(
                    "Can only reset ionic fractions with a dict if "
                    "the new base particles are a superset of the "
                    "prior base particles.  To change ionic fractions "
                    "for one base particle, use item assignment on the "
                    "IonizationStates instance instead.")

        if isinstance(inputs, dict):
            original_keys = inputs.keys()
            ionfrac_types = {type(inputs[key]) for key in original_keys}
            inputs_have_quantities = u.Quantity in ionfrac_types

            if inputs_have_quantities and len(ionfrac_types) != 1:
                raise TypeError(
                    "Ionic fraction information may only be inputted "
                    "as a Quantity object if all ionic fractions are "
                    "Quantity arrays with units of inverse volume.")

            try:
                particles = {key: Particle(key) for key in original_keys}
            except (InvalidParticleError, TypeError) as exc:
                raise AtomicError(
                    "Unable to create IonizationStates instance "
                    "because not all particles are valid.") from exc

            # The particles whose ionization states are to be recorded
            # should be elements or isotopes but not ions or neutrals.

            for key in particles.keys():
                is_element = particles[key].is_category("element")
                has_charge_info = particles[key].is_category(
                    any_of=["charged", "uncharged"])

                if not is_element or has_charge_info:
                    raise AtomicError(
                        f"{key} is not an element or isotope without "
                        f"charge information.")

            # We are sorting the elements/isotopes by atomic number and
            # mass number since we will often want to plot and analyze
            # things and this is the most sensible order.

            sorted_keys = sorted(
                original_keys,
                key=lambda k: (
                    particles[k].atomic_number,
                    particles[k].mass_number if particles[k].isotope else 0,
                ),
            )

            _elements_and_isotopes = []
            _particle_instances = []
            new_ionic_fractions = {}

            if inputs_have_quantities:
                n_elems = {}

            for key in sorted_keys:
                new_key = particles[key].particle
                _particle_instances.append(particles[key])
                if new_key in _elements_and_isotopes:
                    raise AtomicError(
                        "Repeated particles in IonizationStates.")

                nstates_input = len(inputs[key])
                nstates = particles[key].atomic_number + 1
                if nstates != nstates_input:
                    raise AtomicError(
                        f"The ionic fractions array for {key} must "
                        f"have a length of {nstates}.")

                _elements_and_isotopes.append(new_key)
                if inputs_have_quantities:
                    try:
                        number_densities = inputs[key].to(u.m**-3)
                        n_elem = np.sum(number_densities)
                        new_ionic_fractions[new_key] = np.array(
                            number_densities / n_elem)
                        n_elems[key] = n_elem
                    except u.UnitConversionError as exc:
                        raise AtomicError(
                            "Units are not inverse volume.") from exc
                elif (isinstance(inputs[key], np.ndarray)
                      and inputs[key].dtype.kind == "f"):
                    new_ionic_fractions[particles[key].particle] = inputs[key]
                else:
                    try:
                        new_ionic_fractions[
                            particles[key].particle] = np.array(inputs[key],
                                                                dtype=np.float)
                    except ValueError as exc:
                        raise AtomicError(
                            f"Inappropriate ionic fractions for {key}."
                        ) from exc

            for key in _elements_and_isotopes:
                fractions = new_ionic_fractions[key]
                if not np.all(np.isnan(fractions)):
                    if np.min(fractions) < 0 or np.max(fractions) > 1:
                        raise AtomicError(
                            f"Ionic fractions for {key} are not between 0 and 1."
                        )
                    if not np.isclose(
                            np.sum(fractions), 1, atol=self.tol, rtol=0):
                        raise AtomicError(
                            f"Ionic fractions for {key} are not normalized to 1."
                        )

            # When the inputs provide the densities, the abundances must
            # not have been provided because that would be redundant
            # or contradictory information.  The number density scaling
            # factor might or might not have been provided.  Have the
            # number density scaling factor default to the total number
            # of neutrals and ions across all elements and isotopes, if
            # it was not provided.  Then go ahead and calculate the
            # abundances based on that.  However, we need to be careful
            # that the abundances are not overwritten during the
            # instantiation of the class.

            if inputs_have_quantities:
                if np.isnan(self.n):
                    new_n = 0 * u.m**-3
                    for key in _elements_and_isotopes:
                        new_n += n_elems[key]
                    self.n = new_n

                new_abundances = {}
                for key in _elements_and_isotopes:
                    new_abundances[key] = np.float(n_elems[key] / self.n)

                self._pars["abundances"] = new_abundances

        elif isinstance(inputs, (list, tuple)):

            try:
                _particle_instances = [
                    Particle(particle) for particle in inputs
                ]
            except (InvalidParticleError, TypeError) as exc:
                raise AtomicError(
                    "Invalid inputs to IonizationStates.") from exc

            _particle_instances.sort(key=lambda p: (
                p.atomic_number, p.mass_number if p.isotope else 0))
            _elements_and_isotopes = [
                particle.particle for particle in _particle_instances
            ]
            new_ionic_fractions = {
                particle.particle: np.full(particle.atomic_number + 1,
                                           fill_value=np.nan,
                                           dtype=np.float64)
                for particle in _particle_instances
            }
        else:
            raise TypeError("Incorrect inputs to set ionic_fractions.")

        for i in range(1, len(_particle_instances)):
            if _particle_instances[
                    i - 1].element == _particle_instances[i].element:
                if (not _particle_instances[i - 1].isotope
                        and _particle_instances[i].isotope):
                    raise AtomicError(
                        "Cannot have an element and isotopes of that element.")

        self._particle_instances = _particle_instances
        self._base_particles = _elements_and_isotopes
        self._ionic_fractions = new_ionic_fractions
Пример #3
0
def particle(request):
    return Particle(request.param)
Пример #4
0
    def get_particle(argname, params, already_particle, funcname):
        argval, Z, mass_numb = params

        # Convert the argument to a Particle object if it is not
        # already one.

        if not already_particle:

            if not isinstance(argval, (numbers.Integral, str, tuple, list)):
                raise TypeError(
                    f"The argument {argname} to {funcname} must be "
                    f"a string, an integer or a tuple or list of them "
                    f"corresponding to an atomic number, or a "
                    f"Particle object.")

            try:
                particle = Particle(argval, Z=Z, mass_numb=mass_numb)
            except InvalidParticleError as e:
                raise InvalidParticleError(
                    _particle_errmsg(argname, argval, Z, mass_numb,
                                     funcname)) from e

        # We will need to do the same error checks whether or not the
        # argument is already an instance of the Particle class.

        if already_particle:
            particle = argval

        # If the name of the argument annotated with Particle in the
        # decorated function is element, isotope, or ion; then this
        # decorator should raise the appropriate exception when the
        # particle ends up not being an element, isotope, or ion.

        cat_table = [
            ("element", particle.element, InvalidElementError),
            ("isotope", particle.isotope, InvalidIsotopeError),
            ("ion", particle.ionic_symbol, InvalidIonError),
        ]

        for category_name, category_symbol, CategoryError in cat_table:
            if argname == category_name and not category_symbol:
                raise CategoryError(
                    f"The argument {argname} = {repr(argval)} to "
                    f"{funcname} does not correspond to a valid "
                    f"{argname}.")

        # Some functions require that particles be charged, or
        # at least that particles have charge information.

        _integer_charge = particle._attributes["integer charge"]

        must_be_charged = "charged" in require
        must_have_charge_info = set(any_of) == {"charged", "uncharged"}

        uncharged = _integer_charge == 0
        lacks_charge_info = _integer_charge is None

        if must_be_charged and (uncharged or must_have_charge_info):
            raise ChargeError(
                f"A charged particle is required for {funcname}.")

        if must_have_charge_info and lacks_charge_info:
            raise ChargeError(
                f"Charge information is required for {funcname}.")

        # Some functions require particles that belong to more complex
        # classification schemes.  Again, be sure to provide a
        # maximally useful error message.

        if not particle.is_category(
                require=require, exclude=exclude, any_of=any_of):
            raise AtomicError(
                _category_errmsg(particle, require, exclude, any_of, funcname))

        return particle
Пример #5
0
def test_antiparticle_inversion(particle, antiparticle):
    """Test that antiparticles have the correct antiparticles."""
    assert Particle(antiparticle).antiparticle == Particle(particle), (
        f"The antiparticle of {antiparticle} is found to be "
        f"{~Particle(antiparticle)} instead of {particle}.")
Пример #6
0
def test_unary_operator_for_elements():
    with pytest.raises(ParticleError):
        Particle("C").antiparticle
Пример #7
0
         "symbol": "He-4 2+",
         "element": "He",
         "element_name": "helium",
         "isotope": "He-4",
         "isotope_name": "helium-4",
         "ionic_symbol": "He-4 2+",
         "roman_symbol": "He-4 III",
         "mass_energy": 5.971919969131517e-10 * u.J,
         "is_ion": True,
         "integer_charge": 2,
         "atomic_number": 2,
         "mass_number": 4,
         "baryon_number": 4,
         "lepton_number": 0,
         "half_life": np.inf * u.s,
         "recombine()": Particle("He-4 1+"),
     },
 ),
 (
     "He-4 0+",
     {},
     {
         "symbol": "He-4 0+",
         "element": "He",
         "isotope": "He-4",
         "mass_energy": 5.971919969131517e-10 * u.J,
     },
 ),
 (
     "Li",
     {
Пример #8
0
def test_particle_bool_error():
    with pytest.raises(ParticleError):
        bool(Particle("e-"))
Пример #9
0
 def append(self, particle: ParticleLike):
     """Append a particle to the end of the `ParticleList`."""
     # TODO: use particle_input when it works with CustomParticle and ParticleLike
     if not isinstance(particle, (Particle, CustomParticle)):
         particle = Particle(particle)
     self.data.append(particle)
Пример #10
0
 def insert(self, index, particle: ParticleLike):
     """Insert a particle before an index."""
     # TODO: use particle_input when it works with CustomParticle and ParticleLike
     if not isinstance(particle, (Particle, CustomParticle)):
         particle = Particle(particle)
     self.data.insert(index, particle)
Пример #11
0
         "particle": "He-4 2+",
         "element": "He",
         "element_name": "helium",
         "isotope": "He-4",
         "isotope_name": "helium-4",
         "ionic_symbol": "He-4 2+",
         "roman_symbol": "He-4 III",
         "mass_energy": 5.971919969131517e-10 * u.J,
         "is_ion": True,
         "integer_charge": 2,
         "atomic_number": 2,
         "mass_number": 4,
         "baryon_number": 4,
         "lepton_number": 0,
         "half_life": np.inf * u.s,
         "recombine()": Particle("He-4 1+"),
     },
 ),
 (
     "He-4 0+",
     {},
     {
         "particle": "He-4 0+",
         "element": "He",
         "isotope": "He-4",
         "mass_energy": 5.971919969131517e-10 * u.J,
     },
 ),
 (
     "Li",
     {"mass_numb": 7},
Пример #12
0
    assert isinstance(empty_particle_list, ParticleList)
    assert len(empty_particle_list) == 0


def test_ion_list_example():
    ions = ionic_levels("He-4")
    np.testing.assert_equal(ions.charge_number, [0, 1, 2])
    assert ions.symbols == ["He-4 0+", "He-4 1+", "He-4 2+"]


@pytest.mark.parametrize(
    "particle, min_charge, max_charge, expected_charge_numbers",
    [
        ("H-1", 0, 1, [0, 1]),
        ("p+", 1, 1, [1]),
        (Particle("p+"), 0, 0, [0]),
        ("C", 3, 5, [3, 4, 5]),
    ],
)
def test_ion_list(particle, min_charge, max_charge, expected_charge_numbers):
    """Test that inputs to ionic_levels are interpreted correctly."""
    particle = Particle(particle)
    ions = ionic_levels(particle, min_charge, max_charge)
    np.testing.assert_equal(ions.charge_number, expected_charge_numbers)
    assert ions[0].element == particle.element
    if particle.is_category("isotope"):
        assert ions[0].isotope == particle.isotope


@pytest.mark.parametrize(
    "element, min_charge, max_charge", [("Li", 0, 4), ("Li", 3, 2)]
Пример #13
0
def test_unary_operator_for_elements():
    with pytest.raises(AtomicError):
        Particle('C').antiparticle
Пример #14
0
def test_particle_bool_error():
    with pytest.raises(AtomicError):
        bool(Particle('e-'))
Пример #15
0
     'particle': 'He-4 2+',
     'element': 'He',
     'element_name': 'helium',
     'isotope': 'He-4',
     'isotope_name': 'helium-4',
     'ionic_symbol': 'He-4 2+',
     'roman_symbol': 'He-4 III',
     'mass_energy': 5.971919969131517e-10 * u.J,
     'is_ion': True,
     'integer_charge': 2,
     'atomic_number': 2,
     'mass_number': 4,
     'baryon_number': 4,
     'lepton_number': 0,
     'half_life': np.inf * u.s,
     'recombine()': Particle('He-4 1+')
 }),
 ('He-4 0+', {}, {
     'particle': 'He-4 0+',
     'element': 'He',
     'isotope': 'He-4',
     'mass_energy': 5.971919969131517e-10 * u.J,
 }),
 ('Li', {
     'mass_numb': 7
 }, {
     'particle': 'Li-7',
     'element': 'Li',
     'element_name': 'lithium',
     'isotope': 'Li-7',
     'isotope_name': 'lithium-7',