Esempio n. 1
0
    def _extract_charge(arg: str):
        """
        Receive a `str` representing an element, isotope, or ion.
        Return a `tuple` containing a `str` that should represent an
        element or isotope, and either an `int` representing the
        charge or `None` if no charge information is provided.  Raise
        an `~plasmapy.utils.InvalidParticleError` if charge information
        is inputted incorrectly.
        """

        invalid_charge_errmsg = (
            f"Invalid charge information in the particle string '{arg}'.")

        if arg.count(' ') == 1:  # Cases like 'H 1-' and 'Fe-56 1+'
            isotope_info, charge_info = arg.split(' ')

            sign_indicator_only_on_one_end = (charge_info.endswith(
                ('-', '+')) ^ charge_info.startswith(('-', '+')))

            just_one_sign_indicator = (
                (charge_info.count('-') == 1 and charge_info.count('+') == 0)
                or
                (charge_info.count('-') == 0 and charge_info.count('+') == 1))

            if not sign_indicator_only_on_one_end and just_one_sign_indicator:
                raise InvalidParticleError(invalid_charge_errmsg) from None

            charge_str = charge_info.strip('+-')

            try:
                if roman.is_roman_numeral(charge_info):
                    Z_from_arg = roman.from_roman(charge_info) - 1
                elif '-' in charge_info:
                    Z_from_arg = -int(charge_str)
                elif '+' in charge_info:
                    Z_from_arg = int(charge_str)
                else:
                    raise InvalidParticleError(invalid_charge_errmsg) from None
            except ValueError:
                raise InvalidParticleError(invalid_charge_errmsg) from None

        elif arg.endswith(('-', '+')):  # Cases like 'H-' and 'Pb-209+++'
            char = arg[-1]
            match = re.match(f"[{char}]*", arg[::-1])
            Z_from_arg = match.span()[1]
            isotope_info = arg[0:len(arg) - match.span()[1]]

            if char == '-':
                Z_from_arg = -Z_from_arg
            if isotope_info.endswith(('-', '+')):
                raise InvalidParticleError(invalid_charge_errmsg) from None
        else:
            isotope_info = arg
            Z_from_arg = None

        return isotope_info, Z_from_arg
Esempio n. 2
0
    def _reconstruct_isotope_symbol(element: str,
                                    mass_numb: numbers.Integral) -> str:
        """
        Receive a `str` representing an atomic symbol and an
        `int` representing a mass number.  Return the isotope symbol
        or `None` if no mass number information is available.  Raises an
        `~plasmapy.utils.InvalidParticleError` for isotopes that have
        not yet been discovered.
        """

        if mass_numb is not None:
            isotope = f"{element}-{mass_numb}"

            if isotope == 'H-2':
                isotope = 'D'
            elif isotope == 'H-3':
                isotope = 'T'

            if isotope not in _Isotopes.keys():
                raise InvalidParticleError(
                    f"The string '{isotope}' does not correspond to "
                    f"a valid isotope.")

        else:
            isotope = None

        return isotope
Esempio n. 3
0
    def _extract_mass_number(isotope_info: str):
        """
        Receives a string representing an element or isotope.
        Return a tuple containing a string that should represent
        an element, and either an integer representing the mass
        number or None if no mass number is available.  Raises an
        `~plasmapy.utils.InvalidParticleError` if the mass number
        information is inputted incorrectly.
        """

        if isotope_info == 'D':
            element_info, mass_numb = 'H', 2
        elif isotope_info == 'T':
            element_info = 'H'
            mass_numb = 3
        elif isotope_info == 'p':
            element_info = 'H'
            mass_numb = 1
        elif '-' not in isotope_info:
            element_info = isotope_info
            mass_numb = None
        elif isotope_info.count('-') == 1:
            element_info, mass_numb_str = isotope_info.split('-')
            try:
                mass_numb = int(mass_numb_str)
            except ValueError:
                raise InvalidParticleError(
                    f"Invalid mass number in isotope string "
                    f"'{isotope_info}'.") from None

        return element_info, mass_numb
Esempio n. 4
0
    def __getitem__(self, value) -> State:
        """Return the ionic fraction(s)."""
        if isinstance(value, slice):
            raise TypeError("IonizationState instances cannot be sliced.")

        if isinstance(value,
                      (int, np.integer)) and 0 <= value <= self.atomic_number:
            result = State(value, self.ionic_fractions[value],
                           self.ionic_symbols[value])
        else:
            if not isinstance(value, Particle):
                try:
                    value = Particle(value)
                except InvalidParticleError as exc:
                    raise InvalidParticleError(
                        f"{value} is not a valid integer charge or "
                        f"particle.") from exc

            same_element = value.element == self.element
            same_isotope = value.isotope == self.isotope
            has_charge_info = value.is_category(
                any_of=["charged", "uncharged"])

            if same_element and same_isotope and has_charge_info:
                Z = value.integer_charge
                result = State(Z, self.ionic_fractions[Z],
                               self.ionic_symbols[Z])
            else:
                if not same_element or not same_isotope:
                    raise AtomicError("Inconsistent element or isotope.")
                elif not has_charge_info:
                    raise ChargeError("No integer charge provided.")
        return result
Esempio n. 5
0
def opposite(particle):
    try:
        opposite_particle = ~particle
    except Exception as exc:
        raise InvalidParticleError(
            f"The unary ~ (invert) operator is unable to find the "
            f"antiparticle of {particle}.") from exc
    return opposite_particle
Esempio n. 6
0
    def _atomic_number_to_symbol(atomic_numb: numbers.Integral):
        """
        Return the atomic symbol associated with an integer
        representing an atomic number, or raises an
        `~plasmapy.utils.InvalidParticleError` if the atomic number does
        not represent a known element.
        """

        if atomic_numb in _atomic_numbers_to_symbols.keys():
            return _atomic_numbers_to_symbols[atomic_numb]
        else:
            raise InvalidParticleError(
                f"{atomic_numb} is not a valid atomic number.")
Esempio n. 7
0
    def _get_element(element_info: str) -> str:
        """
        Receive a `str` representing an element's symbol or
        name, and returns a `str` representing the atomic symbol.
        """

        if element_info.lower() in _element_names_to_symbols.keys():
            element = _element_names_to_symbols[element_info.lower()]
        elif element_info in _atomic_numbers_to_symbols.values():
            element = element_info
        else:
            raise InvalidParticleError(
                f"The string '{element_info}' does not correspond to "
                f"a valid element.")

        return element
Esempio n. 8
0
def stable_isotopes(argument: Union[str, numbers.Integral] = None,
                    unstable: bool = False) -> List[str]:
    """
    Return a list of all stable isotopes of an element, or if no input is
    provided, a list of all such isotopes for every element.

    Parameters
    ----------
    argument: `int` or `str`
        A string or integer representing an atomic number or element,
        or a string representing an isotope.

    unstable: `bool`
        If set to `True`, this function will return a list of the
        unstable isotopes instead of the stable isotopes.

    Returns
    -------
    StableIsotopes: `list` of strings or empty list
        List of all stable isotopes of an element, sorted from lowest
        mass number.  If an element has no stable isotopes, this
        function returns an empty list.

    Raises
    ------
    `~plasmapy.utils.InvalidElementError`
        If the argument is a valid particle but not a valid element.

    `~plasmapy.utils.InvalidParticleError`
        If the argument does not correspond to a valid particle.

    `TypeError`
        If the argument is not a string or integer.

    Notes
    -----
    There are 254 isotopes for which no radioactive decay has been
    observed.  It is possible that some isotopes will be discovered to
    be unstable but with extremely long half-lives.  For example,
    bismuth-209 was recently discovered to have a half-life of about
    1.9e19 years.  However, such isotopes can be regarded as virtually
    stable for most applications.

    See Also
    --------
    `~plasmapy.atomic.known_isotopes` : returns a list of isotopes that
        have been discovered

    `~plasmapy.atomic.common_isotopes` : returns isotopes with non-zero
        isotopic abundances

    Examples
    --------
    >>> stable_isotopes('H')
    ['H-1', 'D']
    >>> stable_isotopes(44)
    ['Ru-96', 'Ru-98', 'Ru-99', 'Ru-100', 'Ru-101', 'Ru-102', 'Ru-104']
    >>> stable_isotopes('beryllium')
    ['Be-9']
    >>> stable_isotopes('Pb-209')
    ['Pb-204', 'Pb-206', 'Pb-207', 'Pb-208']
    >>> stable_isotopes(118)
    []

    Find unstable isotopes using the ``unstable`` keyword.

    >>> stable_isotopes('U', unstable=True)[:5]  # only first five
    ['U-217', 'U-218', 'U-219', 'U-220', 'U-221']

    """

    # TODO: Allow Particle objects representing elements to be inputs

    def stable_isotopes_for_element(argument: Union[str, int],
                                    stable_only: Optional[bool]) -> List[str]:
        KnownIsotopes = known_isotopes(argument)
        StableIsotopes = [
            isotope for isotope in KnownIsotopes
            if _Isotopes[isotope]['stable'] == stable_only
        ]
        return StableIsotopes

    if argument is not None:
        try:
            element = atomic_symbol(argument)
            isotopes_list = stable_isotopes_for_element(element, not unstable)
        except InvalidParticleError:
            raise InvalidParticleError("Invalid particle in stable_isotopes")
        except InvalidElementError:
            raise InvalidElementError(
                "stable_isotopes is unable to get isotopes "
                f"from an input of: {argument}")
    elif argument is None:
        isotopes_list = []
        for atomic_numb in range(1, 119):
            isotopes_list += stable_isotopes_for_element(
                atomic_numb, not unstable)

    return isotopes_list
Esempio n. 9
0
def common_isotopes(argument: Union[str, numbers.Integral] = None,
                    most_common_only: bool = False) -> List[str]:
    """
    Return a list of isotopes of an element with an isotopic abundances
    greater than zero, or if no input is provided, a list of all such
    isotopes for every element.

    Parameters
    ----------
    argument: `int` or `str`, optional
        A string or integer representing an atomic number or element,
        or a string representing an isotope.

    most_common_only: `bool`
        If set to `True`, return only the most common isotope.

    Returns
    -------
    isotopes_list: `list` of `str` or empty `list`
        List of all isotopes of an element with isotopic abundances
        greater than zero, sorted from most abundant to least
        abundant.  If no isotopes have isotopic abundances greater
        than zero, this function will return an empty list.  If no
        arguments are provided, then a list of all common isotopes of
        all elements will be provided that is sorted by atomic number,
        with entries for each element sorted from most abundant to
        least abundant.

    Raises
    ------
    `~plasmapy.utils.InvalidElementError`
        If the argument is a valid particle but not a valid element.

    `~plasmapy.utils.InvalidParticleError`
        If the argument does not correspond to a valid particle.

    `TypeError`
        If the argument is not a string or integer.

    Notes
    -----
    The isotopic abundances are based on the terrestrial environment
    and may not be appropriate for space and astrophysical applications.

    See Also
    --------
    `~plasmapy.utils.known_isotopes` : returns a list of isotopes that
        have been discovered.

    `~plasmapy.utils.stable_isotopes` : returns isotopes that are stable
        against radioactive decay.

    `~plasmapy.utils.isotopic_abundance` : returns the relative isotopic
         abundance.

    Examples
    --------
    >>> common_isotopes('H')
    ['H-1', 'D']
    >>> common_isotopes(44)
    ['Ru-102', 'Ru-104', 'Ru-101', 'Ru-99', 'Ru-100', 'Ru-96', 'Ru-98']
    >>> common_isotopes('beryllium 2+')
    ['Be-9']
    >>> common_isotopes('Fe')
    ['Fe-56', 'Fe-54', 'Fe-57', 'Fe-58']
    >>> common_isotopes('Fe', most_common_only=True)
    ['Fe-56']
    >>> common_isotopes()[0:7]
    ['H-1', 'D', 'He-4', 'He-3', 'Li-7', 'Li-6', 'Be-9']

    """

    # TODO: Allow Particle objects representing elements to be inputs

    def common_isotopes_for_element(
            argument: Union[str, int],
            most_common_only: Optional[bool]) -> List[str]:

        isotopes = known_isotopes(argument)

        CommonIsotopes = [
            isotope for isotope in isotopes
            if 'abundance' in _Isotopes[isotope].keys()
        ]

        isotopic_abundances = [
            _Isotopes[isotope]['abundance'] for isotope in CommonIsotopes
        ]

        sorted_isotopes = [
            iso_comp
            for (isotope,
                 iso_comp) in sorted(zip(isotopic_abundances, CommonIsotopes))
        ]

        sorted_isotopes.reverse()

        if most_common_only and len(sorted_isotopes) > 1:
            sorted_isotopes = sorted_isotopes[0:1]

        return sorted_isotopes

    if argument is not None:

        try:
            element = atomic_symbol(argument)
            isotopes_list = common_isotopes_for_element(
                element, most_common_only)
        except InvalidParticleError:
            raise InvalidParticleError("Invalid particle")
        except InvalidElementError:
            raise InvalidElementError(
                "common_isotopes is unable to get isotopes "
                f"from an input of: {argument}")

    elif argument is None:
        isotopes_list = []
        for atomic_numb in range(1, 119):
            isotopes_list += common_isotopes_for_element(
                atomic_numb, most_common_only)

    return isotopes_list
Esempio n. 10
0
def known_isotopes(argument: Union[str, numbers.Integral] = None) -> List[str]:
    """Return a list of all known isotopes of an element, or a list
    of all known isotopes of every element if no input is provided.

    Parameters
    ----------
    argument: `int` or `str`, optional
        A string representing an element, isotope, or ion or an
        integer representing an atomic number

    Returns
    -------
    isotopes_list: `list` containing `str` items or an empty `list`
        List of all of the isotopes of an element that have been
        discovered, sorted from lowest mass number to highest mass
        number.  If no argument is provided, then a list of all known
        isotopes of every element will be returned that is sorted by
        atomic number, with entries for each element sorted by mass
        number.

    Raises
    ------
    `~plasmapy.utils.InvalidElementError`
        If the argument is a valid particle but not a valid element.

    `~plasmapy.utils.InvalidParticleError`
        If the argument does not correspond to a valid particle.

    `TypeError`
        If the argument is not a `str` or `int`.

    Notes
    -----
    This list returns both natural and artificially produced isotopes.

    See Also
    --------
    `~plasmapy.atomic.common_isotopes` : returns isotopes with non-zero
        isotopic abundances.

    `~plasmapy.atomic.stable_isotopes` : returns isotopes that are
        stable against radioactive decay.

    Examples
    --------
    >>> known_isotopes('H')
    ['H-1', 'D', 'T', 'H-4', 'H-5', 'H-6', 'H-7']
    >>> known_isotopes('helium 1+')
    ['He-3', 'He-4', 'He-5', 'He-6', 'He-7', 'He-8', 'He-9', 'He-10']
    >>> known_isotopes()[0:10]
    ['H-1', 'D', 'T', 'H-4', 'H-5', 'H-6', 'H-7', 'He-3', 'He-4', 'He-5']
    >>> len(known_isotopes())  # the number of known isotopes
    3352

    """

    # TODO: Allow Particle objects representing elements to be inputs

    def known_isotopes_for_element(argument):
        element = atomic_symbol(argument)
        isotopes = []
        for isotope in _Isotopes.keys():
            if element + '-' in isotope and isotope[0:len(element)] == element:
                isotopes.append(isotope)
        if element == 'H':
            isotopes.insert(1, 'D')
            isotopes.insert(2, 'T')
        mass_numbers = [mass_number(isotope) for isotope in isotopes]
        sorted_isotopes = [
            mass_number
            for (isotope, mass_number) in sorted(zip(mass_numbers, isotopes))
        ]
        return sorted_isotopes

    if argument is not None:
        try:
            element = atomic_symbol(argument)
            isotopes_list = known_isotopes_for_element(element)
        except InvalidElementError:
            raise InvalidElementError("known_isotopes is unable to get "
                                      f"isotopes from an input of: {argument}")
        except InvalidParticleError:
            raise InvalidParticleError("Invalid particle in known_isotopes.")
    elif argument is None:
        isotopes_list = []
        for atomic_numb in range(1, len(_Elements.keys()) + 1):
            isotopes_list += known_isotopes_for_element(atomic_numb)

    return isotopes_list
Esempio n. 11
0
def _parse_and_check_atomic_input(argument: Union[str, numbers.Integral],
                                  mass_numb: numbers.Integral = None,
                                  Z: numbers.Integral = None):
    """
    Parse information about a particle into a dictionary of standard
    symbols, and check the validity of the particle.

    Parameters
    ----------
    argument : `str` or `int`
        String containing information for an element, isotope, or ion
        in any of the allowed formats; or an integer representing an
        atomic number.

    mass_numb : `int`, optional
        The mass number of an isotope.

    Z : `int`, optional
        The integer charge of an ion.

    Returns
    -------
    nomenclature_dict : `dict`
        A dictionary containing information about the element, isotope,
        or ion.  The key ``'symbol'`` corresponds to the particle symbol
        containing the most information, ``'element'`` corresponds to
        the atomic symbol, ``'isotope'`` corresponds to the isotope
        symbol, ``'ion'`` corresponds to the ion symbol, ``'mass_numb'``
        corresponds to the mass number, and ``'Z'`` corresponds to the
        integer charge.  The corresponding items will be given by `None`
        if the necessary information is not provided.

    Raises
    ------
    `~plasmapy.utils.InvalidParticleError`
        If the arguments do not correspond to a valid particle or
        antiparticle.

    `~plasmapy.utils.InvalidElementError`
        If the particle is valid but does not correspond to an element,
        ion, or isotope.

    `TypeError`
        If the argument or any of the keyword arguments is not of the
        correct type.

    """
    def _atomic_number_to_symbol(atomic_numb: numbers.Integral):
        """
        Return the atomic symbol associated with an integer
        representing an atomic number, or raises an
        `~plasmapy.utils.InvalidParticleError` if the atomic number does
        not represent a known element.
        """

        if atomic_numb in _atomic_numbers_to_symbols.keys():
            return _atomic_numbers_to_symbols[atomic_numb]
        else:
            raise InvalidParticleError(
                f"{atomic_numb} is not a valid atomic number.")

    def _extract_charge(arg: str):
        """
        Receive a `str` representing an element, isotope, or ion.
        Return a `tuple` containing a `str` that should represent an
        element or isotope, and either an `int` representing the
        charge or `None` if no charge information is provided.  Raise
        an `~plasmapy.utils.InvalidParticleError` if charge information
        is inputted incorrectly.
        """

        invalid_charge_errmsg = (
            f"Invalid charge information in the particle string '{arg}'.")

        if arg.count(' ') == 1:  # Cases like 'H 1-' and 'Fe-56 1+'
            isotope_info, charge_info = arg.split(' ')

            sign_indicator_only_on_one_end = (charge_info.endswith(
                ('-', '+')) ^ charge_info.startswith(('-', '+')))

            just_one_sign_indicator = (
                (charge_info.count('-') == 1 and charge_info.count('+') == 0)
                or
                (charge_info.count('-') == 0 and charge_info.count('+') == 1))

            if not sign_indicator_only_on_one_end and just_one_sign_indicator:
                raise InvalidParticleError(invalid_charge_errmsg) from None

            charge_str = charge_info.strip('+-')

            try:
                if roman.is_roman_numeral(charge_info):
                    Z_from_arg = roman.from_roman(charge_info) - 1
                elif '-' in charge_info:
                    Z_from_arg = -int(charge_str)
                elif '+' in charge_info:
                    Z_from_arg = int(charge_str)
                else:
                    raise InvalidParticleError(invalid_charge_errmsg) from None
            except ValueError:
                raise InvalidParticleError(invalid_charge_errmsg) from None

        elif arg.endswith(('-', '+')):  # Cases like 'H-' and 'Pb-209+++'
            char = arg[-1]
            match = re.match(f"[{char}]*", arg[::-1])
            Z_from_arg = match.span()[1]
            isotope_info = arg[0:len(arg) - match.span()[1]]

            if char == '-':
                Z_from_arg = -Z_from_arg
            if isotope_info.endswith(('-', '+')):
                raise InvalidParticleError(invalid_charge_errmsg) from None
        else:
            isotope_info = arg
            Z_from_arg = None

        return isotope_info, Z_from_arg

    def _extract_mass_number(isotope_info: str):
        """
        Receives a string representing an element or isotope.
        Return a tuple containing a string that should represent
        an element, and either an integer representing the mass
        number or None if no mass number is available.  Raises an
        `~plasmapy.utils.InvalidParticleError` if the mass number
        information is inputted incorrectly.
        """

        if isotope_info == 'D':
            element_info, mass_numb = 'H', 2
        elif isotope_info == 'T':
            element_info = 'H'
            mass_numb = 3
        elif isotope_info == 'p':
            element_info = 'H'
            mass_numb = 1
        elif '-' not in isotope_info:
            element_info = isotope_info
            mass_numb = None
        elif isotope_info.count('-') == 1:
            element_info, mass_numb_str = isotope_info.split('-')
            try:
                mass_numb = int(mass_numb_str)
            except ValueError:
                raise InvalidParticleError(
                    f"Invalid mass number in isotope string "
                    f"'{isotope_info}'.") from None

        return element_info, mass_numb

    def _get_element(element_info: str) -> str:
        """
        Receive a `str` representing an element's symbol or
        name, and returns a `str` representing the atomic symbol.
        """

        if element_info.lower() in _element_names_to_symbols.keys():
            element = _element_names_to_symbols[element_info.lower()]
        elif element_info in _atomic_numbers_to_symbols.values():
            element = element_info
        else:
            raise InvalidParticleError(
                f"The string '{element_info}' does not correspond to "
                f"a valid element.")

        return element

    def _reconstruct_isotope_symbol(element: str,
                                    mass_numb: numbers.Integral) -> str:
        """
        Receive a `str` representing an atomic symbol and an
        `int` representing a mass number.  Return the isotope symbol
        or `None` if no mass number information is available.  Raises an
        `~plasmapy.utils.InvalidParticleError` for isotopes that have
        not yet been discovered.
        """

        if mass_numb is not None:
            isotope = f"{element}-{mass_numb}"

            if isotope == 'H-2':
                isotope = 'D'
            elif isotope == 'H-3':
                isotope = 'T'

            if isotope not in _Isotopes.keys():
                raise InvalidParticleError(
                    f"The string '{isotope}' does not correspond to "
                    f"a valid isotope.")

        else:
            isotope = None

        return isotope

    def _reconstruct_ion_symbol(element: str,
                                isotope: numbers.Integral = None,
                                Z: numbers.Integral = None):
        """
        Receive a `str` representing an atomic symbol and/or a
        string representing an isotope, and an `int` representing the
        integer charge.  Return a `str` representing the ion symbol,
        or `None` if no charge information is available.
        """

        if Z is not None:
            if Z < 0:
                sign = '-'
            else:
                sign = '+'

            if isotope is None:
                base = element
            else:
                base = isotope

            ion = f"{base} {np.abs(Z)}{sign}"
        else:
            ion = None

        if ion == 'H-1 1+':
            ion = 'p+'

        return ion

    if not isinstance(argument, (str, numbers.Integral)):  # coverage: ignore
        raise TypeError(
            f"The argument {argument} is not an integer or string.")

    arg = _dealias_particle_aliases(argument)

    if arg in ParticleZoo.everything - {'p+'}:
        if (mass_numb is not None) or (Z is not None):
            raise InvalidParticleError(
                f"The keywords mass_numb and Z should not be specified "
                f"for particle '{argument}', which is a special particle.")
        else:
            raise InvalidElementError(f"{argument} is not a valid element.")

    if isinstance(arg, str) and arg.isdigit():
        arg = int(arg)

    if isinstance(arg, numbers.Integral):
        element = _atomic_number_to_symbol(arg)
        Z_from_arg = None
        mass_numb_from_arg = None
    elif isinstance(arg, str):
        isotope_info, Z_from_arg = _extract_charge(arg)
        element_info, mass_numb_from_arg = \
            _extract_mass_number(isotope_info)
        element = _get_element(element_info)

    if mass_numb is not None and mass_numb_from_arg is not None:
        if mass_numb != mass_numb_from_arg:
            raise InvalidParticleError(
                "The mass number extracted from the particle string "
                f"'{argument}' is inconsistent with the keyword mass_numb = "
                f"{mass_numb}.")
        else:
            warnings.warn(
                "Redundant mass number information for particle "
                f"'{argument}' with mass_numb = {mass_numb}.", AtomicWarning)

    if mass_numb_from_arg is not None:
        mass_numb = mass_numb_from_arg

    if Z is not None and Z_from_arg is not None:
        if Z != Z_from_arg:
            raise InvalidParticleError(
                "The integer charge extracted from the particle string "
                f"'{argument}' is inconsistent with the keyword Z = {Z}.")
        else:
            warnings.warn(
                "Redundant charge information for particle "
                f"'{argument}' with Z = {Z}.", AtomicWarning)

    if Z_from_arg is not None:
        Z = Z_from_arg

    if isinstance(Z, numbers.Integral):
        if Z > _Elements[element]['atomic number']:
            raise InvalidParticleError(
                f"The integer charge Z = {Z} cannot exceed the atomic number "
                f"of {element}, which is {_Elements[element]['atomic number']}."
            )
        elif Z <= -3:
            warnings.warn(
                f"Particle '{argument}' has an integer charge "
                f"of Z = {Z}, which is unlikely to occur in "
                f"nature.", AtomicWarning)

    isotope = _reconstruct_isotope_symbol(element, mass_numb)
    ion = _reconstruct_ion_symbol(element, isotope, Z)

    if ion:
        symbol = ion
    elif isotope:
        symbol = isotope
    else:
        symbol = element

    nomenclature_dict = {
        'particle': symbol,
        'element': element,
        'isotope': isotope,
        'ion': ion,
        'mass number': mass_numb,
        'integer charge': Z,
    }

    return nomenclature_dict
Esempio n. 12
0
    def __eq__(self, other) -> bool:
        """
        Determine if two objects correspond to the same particle.

        This method will return `True` if ``other`` is an identical
        `~plasmapy.atomic.Particle` instance or a `str` representing the
        same particle, and return `False` if ``other`` is a different
        `~plasmapy.atomic.Particle` or a `str` representing a different
        particle.

        If ``other`` is not a `str` or `~plasmapy.atomic.Particle`
        instance, then this method will raise a `TypeError`.  If
        ``other.particle`` equals ``self.particle`` but the attributes
        differ, then this method will raise a
        `~plasmapy.utils.AtomicError`.

        Examples
        --------
        >>> electron = Particle('e-')
        >>> positron = Particle('e+')
        >>> electron == positron
        False
        >>> electron == 'e-'
        True

        """
        if isinstance(other, str):
            try:
                other_particle = Particle(other)
                return self.particle == other_particle.particle
            except InvalidParticleError as exc:
                raise InvalidParticleError(
                    f"{other} is not a particle and cannot be "
                    f"compared to {self}.") from exc

        if not isinstance(other, self.__class__):
            raise TypeError(
                f"The equality of a Particle object with a {type(other)} is undefined.")

        no_particle_attr = 'particle' not in dir(self) or 'particle' not in dir(other)
        no_attributes_attr = '_attributes' not in dir(self) or '_attributes' not in dir(other)

        if no_particle_attr or no_attributes_attr:  # coverage: ignore
            raise TypeError(f"The equality of {self} with {other} is undefined.")

        same_particle = self.particle == other.particle

        # The following two loops are a hack to enable comparisons
        # between defaultdicts.  By accessing all of the defined keys in
        # each of the defaultdicts, this makes sure that
        # self._attributes and other._attributes have the same keys.

        # TODO: create function in utils to account for equality between
        # defaultdicts, and implement it here

        for attribute in self._attributes.keys():
            other._attributes[attribute]

        for attribute in other._attributes.keys():
            self._attributes[attribute]

        same_attributes = self._attributes == other._attributes

        if same_particle and not same_attributes:  # coverage: ignore
            raise AtomicError(
                f"{self} and {other} should be the same Particle, but "
                f"have differing attributes.\n\n"
                f"The attributes of {self} are:\n\n{self._attributes}\n\n"
                f"The attributes of {other} are:\n\n{other._attributes}\n")

        return same_particle
Esempio n. 13
0
    def __init__(
            self,
            argument: Union[str, Integral],
            mass_numb: Integral = None,
            Z: Integral = None):
        """
        Instantiate a `~plasmapy.atomic.Particle` object and set private
        attributes.
        """

        if not isinstance(argument, (Integral, np.integer, str, Particle)):
            raise TypeError(
                "The first positional argument when creating a "
                "Particle object must be either an integer, string, or "
                "another Particle object.")

        # If argument is a Particle instance, then we will construct a
        # new Particle instance for the same Particle (essentially a
        # copy).

        if isinstance(argument, Particle):
            argument = argument.particle

        if mass_numb is not None and not isinstance(mass_numb, Integral):
            raise TypeError("mass_numb is not an integer")

        if Z is not None and not isinstance(Z, Integral):
            raise TypeError("Z is not an integer.")

        self._attributes = defaultdict(lambda: None)
        attributes = self._attributes

        # Use this set to keep track of particle categories such as
        # 'lepton' for use with the is_category method later on.

        self._categories = set()
        categories = self._categories

        # If the argument corresponds to one of the case-sensitive or
        # case-insensitive aliases for particles, return the standard
        # symbol. Otherwise, return the original argument.

        particle = _dealias_particle_aliases(argument)

        if particle in _Particles.keys():  # special particles

            attributes['particle'] = particle

            for attribute in _Particles[particle].keys():
                attributes[attribute] = _Particles[particle][attribute]

            particle_taxonomy = ParticleZoo._taxonomy_dict
            all_categories = particle_taxonomy.keys()

            for category in all_categories:
                if particle in particle_taxonomy[category]:
                    categories.add(category)

            if attributes['name'] in _specific_particle_categories:
                categories.add(attributes['name'])

            if particle == 'p+':
                categories.update({'element', 'isotope', 'ion'})

            if mass_numb is not None or Z is not None:
                if particle == 'p+' and (mass_numb == 1 or Z == 1):
                    warnings.warn("Redundant mass number or charge information.", AtomicWarning)
                else:
                    raise InvalidParticleError(
                        "The keywords 'mass_numb' and 'Z' cannot be used when "
                        "creating Particle objects for special particles. To "
                        f"create a Particle object for {attributes['name']}s, "
                        f"use:  Particle({repr(attributes['particle'])})")

        else:  # elements, isotopes, and ions (besides protons)
            try:
                nomenclature = _parse_and_check_atomic_input(argument, mass_numb=mass_numb, Z=Z)
            except Exception as exc:
                errmsg = _invalid_particle_errmsg(argument, mass_numb=mass_numb, Z=Z)
                raise InvalidParticleError(errmsg) from exc

            for key in nomenclature.keys():
                attributes[key] = nomenclature[key]

            element = attributes['element']
            isotope = attributes['isotope']
            ion = attributes['ion']

            if element:
                categories.add('element')
            if isotope:
                categories.add('isotope')
            if self.element and self._attributes['integer charge']:
                categories.add('ion')

            # Element properties

            Element = _Elements[element]

            attributes['atomic number'] = Element['atomic number']
            attributes['element name'] = Element['element name']

            # Set the lepton number to zero for elements, isotopes, and
            # ions.  The lepton number will probably come up primarily
            # during nuclear reactions.

            attributes['lepton number'] = 0

            if isotope:

                Isotope = _Isotopes[isotope]

                attributes['baryon number'] = Isotope['mass number']
                attributes['isotope mass'] = Isotope.get('mass', None)
                attributes['isotopic abundance'] = Isotope.get('abundance', 0.0)

                if Isotope['stable']:
                    attributes['half-life'] = np.inf * u.s
                else:
                    attributes['half-life'] = Isotope.get('half-life', None)

            if element and not isotope:
                attributes['standard atomic weight'] = Element.get('atomic mass', None)

            if ion in _special_ion_masses.keys():
                attributes['mass'] = _special_ion_masses[ion]

            attributes['periodic table'] = _PeriodicTable(
                group=Element['group'],
                period=Element['period'],
                block=Element['block'],
                category=Element['category'],
            )

            categories.add(Element['category'])

        if attributes['integer charge'] == 1:
            attributes['charge'] = const.e.si
        elif attributes['integer charge'] is not None:
            attributes['charge'] = attributes['integer charge'] * const.e.si

        if attributes['integer charge']:
            categories.add('charged')
        elif attributes['integer charge'] == 0:
            categories.add('uncharged')

        if attributes['half-life'] is not None:
            if isinstance(attributes['half-life'], str):
                categories.add('unstable')
            elif attributes['half-life'] == np.inf * u.s:
                categories.add('stable')
            else:
                categories.add('unstable')

        self.__name__ = self.__repr__()
Esempio n. 14
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