1# coding: utf-8
2# Copyright (c) Pymatgen Development Team.
3# Distributed under the terms of the MIT License.
4
5"""Module contains classes presenting Element and Species (Element + oxidation state) and PeriodicTable."""
6import ast
7import json
8import re
9import sys
10import warnings
11from collections import Counter
12from enum import Enum
13from io import open
14from itertools import combinations, product
15from pathlib import Path
16from typing import Callable, Dict, List, Optional, Tuple, Union
17
18import numpy as np
19from monty.json import MSONable
20
21from pymatgen.core.units import SUPPORTED_UNIT_NAMES, FloatWithUnit, Length, Mass, Unit
22from pymatgen.util.string import Stringify, formula_double_format
23
24if sys.version_info >= (3, 8):
25    from typing import Literal
26else:
27    from typing_extensions import Literal
28
29# Loads element data from json file
30with open(str(Path(__file__).absolute().parent / "periodic_table.json"), "rt") as f:
31    _pt_data = json.load(f)
32
33_pt_row_sizes = (2, 8, 8, 18, 18, 32, 32)
34
35
36class ElementBase(Enum):
37    """Element class defined without any enum values so it can be subclassed."""
38
39    def __init__(self, symbol: str):
40        """
41        Basic immutable element object with all relevant properties.
42
43        Only one instance of Element for each symbol is stored after creation,
44        ensuring that a particular element behaves like a singleton. For all
45        attributes, missing data (i.e., data for which is not available) is
46        represented by a None unless otherwise stated.
47
48        Args:
49            symbol (str): Element symbol, e.g., "H", "Fe"
50
51        .. attribute:: Z
52
53            Atomic number
54
55        .. attribute:: symbol
56
57            Element symbol
58
59        .. attribute:: long_name
60
61           Long name for element. E.g., "Hydrogen".
62
63        .. attribute:: atomic_radius_calculated
64
65            Calculated atomic radius for the element. This is the empirical value.
66            Data is obtained from
67            http://en.wikipedia.org/wiki/Atomic_radii_of_the_elements_(data_page).
68
69        .. attribute:: van_der_waals_radius
70
71            Van der Waals radius for the element. This is the empirical
72            value determined from critical reviews of X-ray diffraction, gas kinetic
73            collision cross-section, and other experimental data by Bondi and later
74            workers. The uncertainty in these values is on the order of 0.1 Å.
75
76            Data are obtained from
77
78            "Atomic Radii of the Elements" in CRC Handbook of Chemistry and Physics,
79                91st Ed.; Haynes, W.M., Ed.; CRC Press: Boca Raton, FL, 2010.
80
81        .. attribute:: mendeleev_no
82
83            Mendeleev number from definition given by Pettifor, D. G. (1984).
84            A chemical scale for crystal-structure maps. Solid State Communications,
85            51 (1), 31-34
86
87        .. attribute:: electrical_resistivity
88
89            Electrical resistivity
90
91        .. attribute:: velocity_of_sound
92
93            Velocity of sound
94
95        .. attribute:: reflectivity
96
97            Reflectivity
98
99        .. attribute:: refractive_index
100
101            Refractice index
102
103        .. attribute:: poissons_ratio
104
105            Poisson's ratio
106
107        .. attribute:: molar_volume
108
109            Molar volume
110
111        .. attribute:: electronic_structure
112
113            Electronic structure.
114            E.g., The electronic structure for Fe is represented as
115            [Ar].3d6.4s2
116
117        .. attribute:: atomic_orbitals
118
119            Atomic Orbitals. Energy of the atomic orbitals as a dict.
120            E.g., The orbitals energies in eV are represented as
121            {'1s': -1.0, '2s': -0.1}
122            Data is obtained from
123            https://www.nist.gov/pml/data/atomic-reference-data-electronic-structure-calculations
124            The LDA values for neutral atoms are used
125
126        .. attribute:: thermal_conductivity
127
128            Thermal conductivity
129
130        .. attribute:: boiling_point
131
132            Boiling point
133
134        .. attribute:: melting_point
135
136            Melting point
137
138        .. attribute:: critical_temperature
139
140            Critical temperature
141
142        .. attribute:: superconduction_temperature
143
144            Superconduction temperature
145
146        .. attribute:: liquid_range
147
148            Liquid range
149
150        .. attribute:: bulk_modulus
151
152            Bulk modulus
153
154        .. attribute:: youngs_modulus
155
156            Young's modulus
157
158        .. attribute:: brinell_hardness
159
160            Brinell hardness
161
162        .. attribute:: rigidity_modulus
163
164            Rigidity modulus
165
166        .. attribute:: mineral_hardness
167
168            Mineral hardness
169
170        .. attribute:: vickers_hardness
171
172            Vicker's hardness
173
174        .. attribute:: density_of_solid
175
176            Density of solid phase
177
178        .. attribute:: coefficient_of_linear_thermal_expansion
179
180            Coefficient of linear thermal expansion
181
182        .. attribute:: ground_level
183
184            Ground level for element
185
186        .. attribute:: ionization_energies
187
188            List of ionization energies. First value is the first ionization energy, second is the second ionization
189            energy, etc. Note that this is zero-based indexing! So Element.ionization_energies[0] refer to the 1st
190            ionization energy. Values are from the NIST Atomic Spectra Database. Missing values are None.
191        """
192        self.symbol = "%s" % symbol
193        d = _pt_data[symbol]
194
195        # Store key variables for quick access
196        self.Z = d["Atomic no"]
197
198        at_r = d.get("Atomic radius", "no data")
199        if str(at_r).startswith("no data"):
200            self._atomic_radius = None
201        else:
202            self._atomic_radius = Length(at_r, "ang")
203        self._atomic_mass = Mass(d["Atomic mass"], "amu")
204        self.long_name = d["Name"]
205        self._data = d
206
207    @property
208    def X(self) -> float:
209        """
210        :return: Electronegativity of element. Note that if an element does not
211            have an electronegativity, a NaN float is returned.
212        """
213        if "X" in self._data:
214            return self._data["X"]
215        warnings.warn(
216            "No electronegativity for %s. Setting to NaN. "
217            "This has no physical meaning, and is mainly done to "
218            "avoid errors caused by the code expecting a float." % self.symbol
219        )
220        return float("NaN")
221
222    @property
223    def atomic_radius(self) -> Optional[FloatWithUnit]:
224        """
225        Returns: The atomic radius of the element in Ångstroms.
226        """
227        return self._atomic_radius
228
229    @property
230    def atomic_mass(self) -> Optional[FloatWithUnit]:
231        """
232        Returns: The atomic mass of the element in amu.
233        """
234        return self._atomic_mass
235
236    def __getattr__(self, item):
237        if item in [
238            "mendeleev_no",
239            "electrical_resistivity",
240            "velocity_of_sound",
241            "reflectivity",
242            "refractive_index",
243            "poissons_ratio",
244            "molar_volume",
245            "thermal_conductivity",
246            "boiling_point",
247            "melting_point",
248            "critical_temperature",
249            "superconduction_temperature",
250            "liquid_range",
251            "bulk_modulus",
252            "youngs_modulus",
253            "brinell_hardness",
254            "rigidity_modulus",
255            "mineral_hardness",
256            "vickers_hardness",
257            "density_of_solid",
258            "atomic_radius_calculated",
259            "van_der_waals_radius",
260            "atomic_orbitals",
261            "coefficient_of_linear_thermal_expansion",
262            "ground_state_term_symbol",
263            "valence",
264            "ground_level",
265            "ionization_energies",
266        ]:
267            kstr = item.capitalize().replace("_", " ")
268            val = self._data.get(kstr, None)
269            if str(val).startswith("no data"):
270                val = None
271            elif isinstance(val, (list, dict)):
272                pass
273            else:
274                try:
275                    val = float(val)
276                except ValueError:
277                    nobracket = re.sub(r"\(.*\)", "", val)
278                    toks = nobracket.replace("about", "").strip().split(" ", 1)
279                    if len(toks) == 2:
280                        try:
281                            if "10<sup>" in toks[1]:
282                                base_power = re.findall(r"([+-]?\d+)", toks[1])
283                                factor = "e" + base_power[1]
284                                if toks[0] in ["&gt;", "high"]:
285                                    toks[0] = "1"  # return the border value
286                                toks[0] += factor
287                                if item == "electrical_resistivity":
288                                    unit = "ohm m"
289                                elif item == "coefficient_of_linear_thermal_expansion":
290                                    unit = "K^-1"
291                                else:
292                                    unit = toks[1]
293                                val = FloatWithUnit(toks[0], unit)
294                            else:
295                                unit = toks[1].replace("<sup>", "^").replace("</sup>", "").replace("&Omega;", "ohm")
296                                units = Unit(unit)
297                                if set(units.keys()).issubset(SUPPORTED_UNIT_NAMES):
298                                    val = FloatWithUnit(toks[0], unit)
299                        except ValueError:
300                            # Ignore error. val will just remain a string.
301                            pass
302            return val
303        raise AttributeError("Element has no attribute %s!" % item)
304
305    @property
306    def data(self) -> dict:
307        """
308        Returns dict of data for element.
309        """
310        return self._data.copy()
311
312    @property
313    def ionization_energy(self) -> float:
314        """
315        First ionization energy of element.
316        """
317        return self._data["Ionization energies"][0]
318
319    @property
320    def electron_affinity(self) -> float:
321        """
322        First ionization energy of element.
323        """
324        return self._data["Electron affinity"]
325
326    @property
327    def electronic_structure(self) -> str:
328        """
329        Electronic structure as string, with only valence electrons.
330        E.g., The electronic structure for Fe is represented as '[Ar].3d6.4s2'
331        """
332        return re.sub("</*sup>", "", self._data["Electronic structure"])
333
334    @property
335    def average_ionic_radius(self) -> FloatWithUnit:
336        """
337        Average ionic radius for element (with units). The average is taken
338        over all oxidation states of the element for which data is present.
339        """
340        if "Ionic radii" in self._data:
341            radii = self._data["Ionic radii"]
342            radius = sum(radii.values()) / len(radii)
343        else:
344            radius = 0.0
345        return FloatWithUnit(radius, "ang")
346
347    @property
348    def average_cationic_radius(self) -> FloatWithUnit:
349        """
350        Average cationic radius for element (with units). The average is
351        taken over all positive oxidation states of the element for which
352        data is present.
353        """
354        if "Ionic radii" in self._data:
355            radii = [v for k, v in self._data["Ionic radii"].items() if int(k) > 0]
356            if radii:
357                return FloatWithUnit(sum(radii) / len(radii), "ang")
358        return FloatWithUnit(0.0, "ang")
359
360    @property
361    def average_anionic_radius(self) -> float:
362        """
363        Average anionic radius for element (with units). The average is
364        taken over all negative oxidation states of the element for which
365        data is present.
366        """
367        if "Ionic radii" in self._data:
368            radii = [v for k, v in self._data["Ionic radii"].items() if int(k) < 0]
369            if radii:
370                return FloatWithUnit(sum(radii) / len(radii), "ang")
371        return FloatWithUnit(0.0, "ang")
372
373    @property
374    def ionic_radii(self) -> Dict[int, float]:
375        """
376        All ionic radii of the element as a dict of
377        {oxidation state: ionic radii}. Radii are given in ang.
378        """
379        if "Ionic radii" in self._data:
380            return {int(k): FloatWithUnit(v, "ang") for k, v in self._data["Ionic radii"].items()}
381        return {}
382
383    @property
384    def number(self) -> int:
385        """Alternative attribute for atomic number"""
386        return self.Z
387
388    @property
389    def max_oxidation_state(self) -> float:
390        """Maximum oxidation state for element"""
391        if "Oxidation states" in self._data:
392            return max(self._data["Oxidation states"])
393        return 0
394
395    @property
396    def min_oxidation_state(self) -> float:
397        """Minimum oxidation state for element"""
398        if "Oxidation states" in self._data:
399            return min(self._data["Oxidation states"])
400        return 0
401
402    @property
403    def oxidation_states(self) -> Tuple:
404        """Tuple of all known oxidation states"""
405        return tuple(self._data.get("Oxidation states", []))
406
407    @property
408    def common_oxidation_states(self) -> Tuple:
409        """Tuple of common oxidation states"""
410        return tuple(self._data.get("Common oxidation states", []))
411
412    @property
413    def icsd_oxidation_states(self) -> Tuple:
414        """Tuple of all oxidation states with at least 10 instances in
415        ICSD database AND at least 1% of entries for that element"""
416        return tuple(self._data.get("ICSD oxidation states", []))
417
418    @property
419    def metallic_radius(self) -> float:
420        """
421        Metallic radius of the element. Radius is given in ang.
422        """
423        return FloatWithUnit(self._data["Metallic radius"], "ang")
424
425    @property
426    def full_electronic_structure(self) -> List[Tuple[int, str, int]]:
427        """
428        Full electronic structure as tuple.
429        E.g., The electronic structure for Fe is represented as:
430        [(1, "s", 2), (2, "s", 2), (2, "p", 6), (3, "s", 2), (3, "p", 6),
431        (3, "d", 6), (4, "s", 2)]
432        """
433        estr = self.electronic_structure
434
435        def parse_orbital(orbstr):
436            m = re.match(r"(\d+)([spdfg]+)(\d+)", orbstr)
437            if m:
438                return int(m.group(1)), m.group(2), int(m.group(3))
439            return orbstr
440
441        data = [parse_orbital(s) for s in estr.split(".")]
442        if data[0][0] == "[":
443            sym = data[0].replace("[", "").replace("]", "")
444            data = list(Element(sym).full_electronic_structure) + data[1:]
445        return data  # type: ignore
446
447    @property
448    def valence(self):
449        """
450        From full electron config obtain valence subshell angular moment (L) and number of valence e- (v_e)
451        """
452        # The number of valence of noble gas is 0
453        if self.group == 18:
454            return np.nan, 0
455
456        L_symbols = "SPDFGHIKLMNOQRTUVWXYZ"
457        valence = []
458        full_electron_config = self.full_electronic_structure
459        last_orbital = full_electron_config[-1]
460        for n, l_symbol, ne in full_electron_config:
461            l = L_symbols.lower().index(l_symbol)
462            if ne < (2 * l + 1) * 2:
463                valence.append((l, ne))
464            # check for full last shell (e.g. column 2)
465            elif (n, l_symbol, ne) == last_orbital and ne == (2 * l + 1) * 2 and len(valence) == 0:
466                valence.append((l, ne))
467        if len(valence) > 1:
468            raise ValueError("Ambiguous valence")
469
470        return valence[0]
471
472    @property
473    def term_symbols(self) -> List[List[str]]:
474        """
475        All possible  Russell-Saunders term symbol of the Element
476        eg. L = 1, n_e = 2 (s2)
477        returns
478           [['1D2'], ['3P0', '3P1', '3P2'], ['1S0']]
479
480        """
481        L_symbols = "SPDFGHIKLMNOQRTUVWXYZ"
482
483        L, v_e = self.valence
484
485        # for one electron in subshell L
486        ml = list(range(-L, L + 1))
487        ms = [1 / 2, -1 / 2]
488        # all possible configurations of ml,ms for one e in subshell L
489        ml_ms = list(product(ml, ms))
490
491        # Number of possible configurations for r electrons in subshell L.
492        n = (2 * L + 1) * 2
493        # the combination of n_e electrons configurations
494        # C^{n}_{n_e}
495        e_config_combs = list(combinations(range(n), v_e))
496
497        # Total ML = sum(ml1, ml2), Total MS = sum(ms1, ms2)
498        TL = [sum([ml_ms[comb[e]][0] for e in range(v_e)]) for comb in e_config_combs]
499        TS = [sum([ml_ms[comb[e]][1] for e in range(v_e)]) for comb in e_config_combs]
500        comb_counter = Counter(zip(TL, TS))
501
502        term_symbols = []
503        while sum(comb_counter.values()) > 0:
504            # Start from the lowest freq combination,
505            # which corresponds to largest abs(L) and smallest abs(S)
506            L, S = min(comb_counter)
507
508            J = list(np.arange(abs(L - S), abs(L) + abs(S) + 1))
509            term_symbols.append([str(int(2 * (abs(S)) + 1)) + L_symbols[abs(L)] + str(j) for j in J])
510            # Without J
511            # term_symbols.append(str(int(2 * (abs(S)) + 1)) \
512            #                     + L_symbols[abs(L)])
513
514            # Delete all configurations included in this term
515            for ML in range(-L, L - 1, -1):
516                for MS in np.arange(S, -S + 1, 1):
517                    if (ML, MS) in comb_counter:
518
519                        comb_counter[(ML, MS)] -= 1
520                        if comb_counter[(ML, MS)] == 0:
521                            del comb_counter[(ML, MS)]
522        return term_symbols
523
524    @property
525    def ground_state_term_symbol(self):
526        """
527        Ground state term symbol
528        Selected based on Hund's Rule
529
530        """
531        L_symbols = "SPDFGHIKLMNOQRTUVWXYZ"
532
533        term_symbols = self.term_symbols
534        term_symbol_flat = {
535            term: {
536                "multiplicity": int(term[0]),
537                "L": L_symbols.index(term[1]),
538                "J": float(term[2:]),
539            }
540            for term in sum(term_symbols, [])
541        }
542
543        multi = [int(item["multiplicity"]) for terms, item in term_symbol_flat.items()]
544        max_multi_terms = {
545            symbol: item for symbol, item in term_symbol_flat.items() if item["multiplicity"] == max(multi)
546        }
547
548        Ls = [item["L"] for terms, item in max_multi_terms.items()]
549        max_L_terms = {symbol: item for symbol, item in term_symbol_flat.items() if item["L"] == max(Ls)}
550
551        J_sorted_terms = sorted(max_L_terms.items(), key=lambda k: k[1]["J"])
552        L, v_e = self.valence
553        if v_e <= (2 * L + 1):
554            return J_sorted_terms[0][0]
555        return J_sorted_terms[-1][0]
556
557    def __eq__(self, other):
558        return isinstance(other, Element) and self.Z == other.Z
559
560    def __ne__(self, other):
561        return not self.__eq__(other)
562
563    def __hash__(self):
564        return self.Z
565
566    def __repr__(self):
567        return "Element " + self.symbol
568
569    def __str__(self):
570        return self.symbol
571
572    def __lt__(self, other):
573        """
574        Sets a default sort order for atomic species by electronegativity. Very
575        useful for getting correct formulas.  For example, FeO4PLi is
576        automatically sorted into LiFePO4.
577        """
578        x1 = float("inf") if self.X != self.X else self.X
579        x2 = float("inf") if other.X != other.X else other.X
580        if x1 != x2:
581            return x1 < x2
582
583        # There are cases where the electronegativity are exactly equal.
584        # We then sort by symbol.
585        return self.symbol < other.symbol
586
587    @staticmethod
588    def from_Z(z: int) -> "Element":
589        """
590        Get an element from an atomic number.
591
592        Args:
593            z (int): Atomic number
594
595        Returns:
596            Element with atomic number z.
597        """
598        for sym, data in _pt_data.items():
599            if data["Atomic no"] == z:
600                return Element(sym)
601        raise ValueError("No element with this atomic number %s" % z)
602
603    @staticmethod
604    def from_row_and_group(row: int, group: int) -> "Element":
605        """
606        Returns an element from a row and group number.
607
608        Args:
609            row (int): Row number
610            group (int): Group number
611
612        .. note::
613            The 18 group number system is used, i.e., Noble gases are group 18.
614        """
615        for sym in _pt_data.keys():
616            el = Element(sym)
617            if el.row == row and el.group == group:
618                return el
619        raise ValueError("No element with this row and group!")
620
621    @staticmethod
622    def is_valid_symbol(symbol: str) -> bool:
623        """
624        Returns true if symbol is a valid element symbol.
625
626        Args:
627            symbol (str): Element symbol
628
629        Returns:
630            True if symbol is a valid element (e.g., "H"). False otherwise
631            (e.g., "Zebra").
632        """
633        return symbol in Element.__members__
634
635    @property
636    def row(self) -> int:
637        """
638        Returns the periodic table row of the element.
639        """
640        z = self.Z
641        total = 0
642        if 57 <= z <= 71:
643            return 8
644        if 89 <= z <= 103:
645            return 9
646        for i, size in enumerate(_pt_row_sizes):
647            total += size
648            if total >= z:
649                return i + 1
650        return 8
651
652    @property
653    def group(self) -> int:
654        """
655        Returns the periodic table group of the element.
656        """
657        z = self.Z
658        if z == 1:
659            return 1
660        if z == 2:
661            return 18
662        if 3 <= z <= 18:
663            if (z - 2) % 8 == 0:
664                return 18
665            if (z - 2) % 8 <= 2:
666                return (z - 2) % 8
667            return 10 + (z - 2) % 8
668
669        if 19 <= z <= 54:
670            if (z - 18) % 18 == 0:
671                return 18
672            return (z - 18) % 18
673
674        if (z - 54) % 32 == 0:
675            return 18
676        if (z - 54) % 32 >= 18:
677            return (z - 54) % 32 - 14
678        return (z - 54) % 32
679
680    @property
681    def block(self) -> str:
682        """
683        Return the block character "s,p,d,f"
684        """
685        if (self.is_actinoid or self.is_lanthanoid) and self.Z not in [71, 103]:
686            return "f"
687        if self.is_actinoid or self.is_lanthanoid:
688            return "d"
689        if self.group in [1, 2]:
690            return "s"
691        if self.group in range(13, 19):
692            return "p"
693        if self.group in range(3, 13):
694            return "d"
695        raise ValueError("unable to determine block")
696
697    @property
698    def is_noble_gas(self) -> bool:
699        """
700        True if element is noble gas.
701        """
702        return self.Z in (2, 10, 18, 36, 54, 86, 118)
703
704    @property
705    def is_transition_metal(self) -> bool:
706        """
707        True if element is a transition metal.
708        """
709        ns = list(range(21, 31))
710        ns.extend(list(range(39, 49)))
711        ns.append(57)
712        ns.extend(list(range(72, 81)))
713        ns.append(89)
714        ns.extend(list(range(104, 113)))
715        return self.Z in ns
716
717    @property
718    def is_post_transition_metal(self) -> bool:
719        """
720        True if element is a post-transition or poor metal.
721        """
722        return self.symbol in ("Al", "Ga", "In", "Tl", "Sn", "Pb", "Bi")
723
724    @property
725    def is_rare_earth_metal(self) -> bool:
726        """
727        True if element is a rare earth metal.
728        """
729        return self.is_lanthanoid or self.is_actinoid
730
731    @property
732    def is_metal(self) -> bool:
733        """
734        :return: True if is a metal.
735        """
736        return (
737            self.is_alkali
738            or self.is_alkaline
739            or self.is_post_transition_metal
740            or self.is_transition_metal
741            or self.is_lanthanoid
742            or self.is_actinoid
743        )
744
745    @property
746    def is_metalloid(self) -> bool:
747        """
748        True if element is a metalloid.
749        """
750        return self.symbol in ("B", "Si", "Ge", "As", "Sb", "Te", "Po")
751
752    @property
753    def is_alkali(self) -> bool:
754        """
755        True if element is an alkali metal.
756        """
757        return self.Z in (3, 11, 19, 37, 55, 87)
758
759    @property
760    def is_alkaline(self) -> bool:
761        """
762        True if element is an alkaline earth metal (group II).
763        """
764        return self.Z in (4, 12, 20, 38, 56, 88)
765
766    @property
767    def is_halogen(self) -> bool:
768        """
769        True if element is a halogen.
770        """
771        return self.Z in (9, 17, 35, 53, 85)
772
773    @property
774    def is_chalcogen(self) -> bool:
775        """
776        True if element is a chalcogen.
777        """
778        return self.Z in (8, 16, 34, 52, 84)
779
780    @property
781    def is_lanthanoid(self) -> bool:
782        """
783        True if element is a lanthanoid.
784        """
785        return 56 < self.Z < 72
786
787    @property
788    def is_actinoid(self) -> bool:
789        """
790        True if element is a actinoid.
791        """
792        return 88 < self.Z < 104
793
794    @property
795    def is_quadrupolar(self) -> bool:
796        """
797        Checks if this element can be quadrupolar
798        """
799        return len(self.data.get("NMR Quadrupole Moment", {})) > 0
800
801    @property
802    def nmr_quadrupole_moment(self) -> dict:
803        """
804        Get a dictionary the nuclear electric quadrupole moment in units of
805        e*millibarns for various isotopes
806        """
807        return {k: FloatWithUnit(v, "mbarn") for k, v in self.data.get("NMR Quadrupole Moment", {}).items()}
808
809    @property
810    def iupac_ordering(self):
811        """
812        Ordering according to Table VI of "Nomenclature of Inorganic Chemistry
813        (IUPAC Recommendations 2005)". This ordering effectively follows the
814        groups and rows of the periodic table, except the Lanthanides, Actanides
815        and hydrogen.
816        """
817        return self._data["IUPAC ordering"]
818
819    def __deepcopy__(self, memo):
820        return Element(self.symbol)
821
822    @staticmethod
823    def from_dict(d) -> "Element":
824        """
825        Makes Element obey the general json interface used in pymatgen for
826        easier serialization.
827        """
828        return Element(d["element"])
829
830    def as_dict(self) -> dict:
831        """
832        Makes Element obey the general json interface used in pymatgen for
833        easier serialization.
834        """
835        return {
836            "@module": self.__class__.__module__,
837            "@class": self.__class__.__name__,
838            "element": self.symbol,
839        }
840
841    @staticmethod
842    def print_periodic_table(filter_function: Optional[Callable] = None):
843        """
844        A pretty ASCII printer for the periodic table, based on some
845        filter_function.
846
847        Args:
848            filter_function: A filtering function taking an Element as input
849                and returning a boolean. For example, setting
850                filter_function = lambda el: el.X > 2 will print a periodic
851                table containing only elements with electronegativity > 2.
852        """
853        for row in range(1, 10):
854            rowstr = []
855            for group in range(1, 19):
856                try:
857                    el = Element.from_row_and_group(row, group)
858                except ValueError:
859                    el = None  # type: ignore
860                if el and ((not filter_function) or filter_function(el)):
861                    rowstr.append("{:3s}".format(el.symbol))
862                else:
863                    rowstr.append("   ")
864            print(" ".join(rowstr))
865
866
867class Element(ElementBase):
868    """Enum representing an element in the periodic table."""
869
870    # This name = value convention is redundant and dumb, but unfortunately is
871    # necessary to preserve backwards compatibility with a time when Element is
872    # a regular object that is constructed with Element(symbol).
873    H = "H"
874    He = "He"
875    Li = "Li"
876    Be = "Be"
877    B = "B"
878    C = "C"
879    N = "N"
880    O = "O"
881    F = "F"
882    Ne = "Ne"
883    Na = "Na"
884    Mg = "Mg"
885    Al = "Al"
886    Si = "Si"
887    P = "P"
888    S = "S"
889    Cl = "Cl"
890    Ar = "Ar"
891    K = "K"
892    Ca = "Ca"
893    Sc = "Sc"
894    Ti = "Ti"
895    V = "V"
896    Cr = "Cr"
897    Mn = "Mn"
898    Fe = "Fe"
899    Co = "Co"
900    Ni = "Ni"
901    Cu = "Cu"
902    Zn = "Zn"
903    Ga = "Ga"
904    Ge = "Ge"
905    As = "As"
906    Se = "Se"
907    Br = "Br"
908    Kr = "Kr"
909    Rb = "Rb"
910    Sr = "Sr"
911    Y = "Y"
912    Zr = "Zr"
913    Nb = "Nb"
914    Mo = "Mo"
915    Tc = "Tc"
916    Ru = "Ru"
917    Rh = "Rh"
918    Pd = "Pd"
919    Ag = "Ag"
920    Cd = "Cd"
921    In = "In"
922    Sn = "Sn"
923    Sb = "Sb"
924    Te = "Te"
925    I = "I"
926    Xe = "Xe"
927    Cs = "Cs"
928    Ba = "Ba"
929    La = "La"
930    Ce = "Ce"
931    Pr = "Pr"
932    Nd = "Nd"
933    Pm = "Pm"
934    Sm = "Sm"
935    Eu = "Eu"
936    Gd = "Gd"
937    Tb = "Tb"
938    Dy = "Dy"
939    Ho = "Ho"
940    Er = "Er"
941    Tm = "Tm"
942    Yb = "Yb"
943    Lu = "Lu"
944    Hf = "Hf"
945    Ta = "Ta"
946    W = "W"
947    Re = "Re"
948    Os = "Os"
949    Ir = "Ir"
950    Pt = "Pt"
951    Au = "Au"
952    Hg = "Hg"
953    Tl = "Tl"
954    Pb = "Pb"
955    Bi = "Bi"
956    Po = "Po"
957    At = "At"
958    Rn = "Rn"
959    Fr = "Fr"
960    Ra = "Ra"
961    Ac = "Ac"
962    Th = "Th"
963    Pa = "Pa"
964    U = "U"
965    Np = "Np"
966    Pu = "Pu"
967    Am = "Am"
968    Cm = "Cm"
969    Bk = "Bk"
970    Cf = "Cf"
971    Es = "Es"
972    Fm = "Fm"
973    Md = "Md"
974    No = "No"
975    Lr = "Lr"
976    Rf = "Rf"
977    Db = "Db"
978    Sg = "Sg"
979    Bh = "Bh"
980    Hs = "Hs"
981    Mt = "Mt"
982    Ds = "Ds"
983    Rg = "Rg"
984    Cn = "Cn"
985    Nh = "Nh"
986    Fl = "Fl"
987    Mc = "Mc"
988    Lv = "Lv"
989    Ts = "Ts"
990    Og = "Og"
991
992
993class Species(MSONable, Stringify):
994    """
995    An extension of Element with an oxidation state and other optional
996    properties. Properties associated with Species should be "idealized"
997    values, not calculated values. For example, high-spin Fe2+ may be
998    assigned an idealized spin of +5, but an actual Fe2+ site may be
999    calculated to have a magmom of +4.5. Calculated properties should be
1000    assigned to Site objects, and not Species.
1001    """
1002
1003    STRING_MODE = "SUPERSCRIPT"
1004    supported_properties = ("spin",)
1005
1006    def __init__(
1007        self,
1008        symbol: str,
1009        oxidation_state: Optional[float] = 0.0,
1010        properties: Optional[dict] = None,
1011    ):
1012        """
1013        Initializes a Species.
1014
1015        Args:
1016            symbol (str): Element symbol, e.g., Fe
1017            oxidation_state (float): Oxidation state of element, e.g., 2 or -2
1018            properties: Properties associated with the Species, e.g.,
1019                {"spin": 5}. Defaults to None. Properties must be one of the
1020                Species supported_properties.
1021
1022        .. attribute:: oxi_state
1023
1024            Oxidation state associated with Species
1025
1026        .. attribute:: ionic_radius
1027
1028            Ionic radius of Species (with specific oxidation state).
1029
1030        .. versionchanged:: 2.6.7
1031
1032            Properties are now checked when comparing two Species for equality.
1033        """
1034        self._el = Element(symbol)
1035        self._oxi_state = oxidation_state
1036        self._properties = properties if properties else {}
1037        for k, _ in self._properties.items():
1038            if k not in Species.supported_properties:
1039                raise ValueError("{} is not a supported property".format(k))
1040
1041    def __getattr__(self, a):
1042        # overriding getattr doesn't play nice with pickle, so we
1043        # can't use self._properties
1044        p = object.__getattribute__(self, "_properties")
1045        if a in p:
1046            return p[a]
1047        return getattr(self._el, a)
1048
1049    def __eq__(self, other):
1050        """
1051        Species is equal to other only if element and oxidation states are
1052        exactly the same.
1053        """
1054        return (
1055            isinstance(other, Species)
1056            and self.symbol == other.symbol
1057            and self.oxi_state == other.oxi_state
1058            and self._properties == other._properties
1059        )
1060
1061    def __ne__(self, other):
1062        return not self.__eq__(other)
1063
1064    def __hash__(self):
1065        """
1066        Equal Species should have the same str representation, hence
1067        should hash equally. Unequal Species will have differnt str
1068        representations.
1069        """
1070        return self.__str__().__hash__()
1071
1072    def __lt__(self, other):
1073        """
1074        Sets a default sort order for atomic species by electronegativity,
1075        followed by oxidation state, followed by spin.
1076        """
1077        x1 = float("inf") if self.X != self.X else self.X
1078        x2 = float("inf") if other.X != other.X else other.X
1079        if x1 != x2:
1080            return x1 < x2
1081        if self.symbol != other.symbol:
1082            # There are cases where the electronegativity are exactly equal.
1083            # We then sort by symbol.
1084            return self.symbol < other.symbol
1085        if self.oxi_state:
1086            other_oxi = 0 if (isinstance(other, Element) or other.oxi_state is None) else other.oxi_state
1087            return self.oxi_state < other_oxi
1088        if getattr(self, "spin", False):
1089            other_spin = getattr(other, "spin", 0)
1090            return self.spin < other_spin
1091        return False
1092
1093    @property
1094    def element(self):
1095        """
1096        Underlying element object
1097        """
1098        return self._el
1099
1100    @property
1101    def ionic_radius(self) -> Optional[float]:
1102        """
1103        Ionic radius of specie. Returns None if data is not present.
1104        """
1105
1106        if self._oxi_state in self.ionic_radii:
1107            return self.ionic_radii[self._oxi_state]
1108        if self._oxi_state:
1109            d = self._el.data
1110            oxstr = str(int(self._oxi_state))
1111            if oxstr in d.get("Ionic radii hs", {}):
1112                warnings.warn("No default ionic radius for %s. Using hs data." % self)
1113                return d["Ionic radii hs"][oxstr]
1114            if oxstr in d.get("Ionic radii ls", {}):
1115                warnings.warn("No default ionic radius for %s. Using ls data." % self)
1116                return d["Ionic radii ls"][oxstr]
1117        warnings.warn("No ionic radius for {}!".format(self))
1118        return None
1119
1120    @property
1121    def oxi_state(self) -> Optional[float]:
1122        """
1123        Oxidation state of Species.
1124        """
1125        return self._oxi_state
1126
1127    @staticmethod
1128    def from_string(species_string: str) -> "Species":
1129        """
1130        Returns a Species from a string representation.
1131
1132        Args:
1133            species_string (str): A typical string representation of a
1134                species, e.g., "Mn2+", "Fe3+", "O2-".
1135
1136        Returns:
1137            A Species object.
1138
1139        Raises:
1140            ValueError if species_string cannot be intepreted.
1141        """
1142
1143        # e.g. Fe2+,spin=5
1144        # 1st group: ([A-Z][a-z]*)    --> Fe
1145        # 2nd group: ([0-9.]*)        --> "2"
1146        # 3rd group: ([+\-])          --> +
1147        # 4th group: (.*)             --> everything else, ",spin=5"
1148
1149        m = re.search(r"([A-Z][a-z]*)([0-9.]*)([+\-]*)(.*)", species_string)
1150        if m:
1151
1152            # parse symbol
1153            sym = m.group(1)
1154
1155            # parse oxidation state (optional)
1156            if not m.group(2) and not m.group(3):
1157                oxi = None
1158            else:
1159                oxi = 1 if m.group(2) == "" else float(m.group(2))
1160                oxi = -oxi if m.group(3) == "-" else oxi
1161
1162            # parse properties (optional)
1163            properties = None
1164            if m.group(4):
1165                toks = m.group(4).replace(",", "").split("=")
1166                properties = {toks[0]: ast.literal_eval(toks[1])}
1167
1168            # but we need either an oxidation state or a property
1169            if oxi is None and properties is None:
1170                raise ValueError("Invalid Species String")
1171
1172            return Species(sym, 0 if oxi is None else oxi, properties)
1173        raise ValueError("Invalid Species String")
1174
1175    def __repr__(self):
1176        return "Species " + self.__str__()
1177
1178    def __str__(self):
1179        output = self.symbol
1180        if self.oxi_state is not None:
1181            if self.oxi_state >= 0:
1182                output += formula_double_format(self.oxi_state) + "+"
1183            else:
1184                output += formula_double_format(-self.oxi_state) + "-"
1185        for p, v in self._properties.items():
1186            output += ",%s=%s" % (p, v)
1187        return output
1188
1189    def to_pretty_string(self) -> str:
1190        """
1191        :return: String without properties.
1192        """
1193        output = self.symbol
1194        if self.oxi_state is not None:
1195            if self.oxi_state >= 0:
1196                output += formula_double_format(self.oxi_state) + "+"
1197            else:
1198                output += formula_double_format(-self.oxi_state) + "-"
1199        return output
1200
1201    def get_nmr_quadrupole_moment(self, isotope: Optional[str] = None) -> float:
1202        """
1203        Gets the nuclear electric quadrupole moment in units of
1204        e*millibarns
1205
1206        Args:
1207            isotope (str): the isotope to get the quadrupole moment for
1208                default is None, which gets the lowest mass isotope
1209        """
1210
1211        quad_mom = self._el.nmr_quadrupole_moment
1212
1213        if not quad_mom:
1214            return 0.0
1215
1216        if isotope is None:
1217            isotopes = list(quad_mom.keys())
1218            isotopes.sort(key=lambda x: int(x.split("-")[1]), reverse=False)
1219            return quad_mom.get(isotopes[0], 0.0)
1220
1221        if isotope not in quad_mom:
1222            raise ValueError("No quadrupole moment for isotope {}".format(isotope))
1223        return quad_mom.get(isotope, 0.0)
1224
1225    def get_shannon_radius(
1226        self,
1227        cn: str,
1228        spin: Literal["", "Low Spin", "High Spin"] = "",
1229        radius_type: Literal["ionic", "crystal"] = "ionic",
1230    ) -> float:
1231        """
1232        Get the local environment specific ionic radius for species.
1233
1234        Args:
1235            cn (str): Coordination using roman letters. Supported values are
1236                I-IX, as well as IIIPY, IVPY and IVSQ.
1237            spin (str): Some species have different radii for different
1238                spins. You can get specific values using "High Spin" or
1239                "Low Spin". Leave it as "" if not available. If only one spin
1240                data is available, it is returned and this spin parameter is
1241                ignored.
1242            radius_type (str): Either "crystal" or "ionic" (default).
1243
1244        Returns:
1245            Shannon radius for specie in the specified environment.
1246        """
1247        radii = self._el.data["Shannon radii"]
1248        radii = radii[str(int(self._oxi_state))][cn]  # type: ignore
1249        if len(radii) == 1:  # type: ignore
1250            k, data = list(radii.items())[0]  # type: ignore
1251            if k != spin:
1252                warnings.warn(
1253                    f"Specified spin state of {spin} not consistent with database "
1254                    f"spin of {k}. Only one spin data available, and that value is returned."
1255                )
1256        else:
1257            data = radii[spin]
1258        return data["%s_radius" % radius_type]
1259
1260    def get_crystal_field_spin(
1261        self, coordination: Literal["oct", "tet"] = "oct", spin_config: Literal["low", "high"] = "high"
1262    ) -> float:
1263        """
1264        Calculate the crystal field spin based on coordination and spin
1265        configuration. Only works for transition metal species.
1266
1267        Args:
1268            coordination ("oct" | "tet"): Tetrahedron or octahedron crystal site coordination
1269            spin_config ("low" | "high"): Whether the species is in a high or low spin state
1270
1271        Returns:
1272            Crystal field spin in Bohr magneton.
1273
1274        Raises:
1275            AttributeError if species is not a valid transition metal or has
1276                an invalid oxidation state.
1277            ValueError if invalid coordination or spin_config.
1278        """
1279        if coordination not in ("oct", "tet") or spin_config not in ("high", "low"):
1280            raise ValueError("Invalid coordination or spin config.")
1281        elec = self.full_electronic_structure
1282        if len(elec) < 4 or elec[-1][1] != "s" or elec[-2][1] != "d":
1283            raise AttributeError("Invalid element {} for crystal field calculation.".format(self.symbol))
1284        nelectrons = elec[-1][2] + elec[-2][2] - self.oxi_state
1285        if nelectrons < 0 or nelectrons > 10:
1286            raise AttributeError("Invalid oxidation state {} for element {}".format(self.oxi_state, self.symbol))
1287        if spin_config == "high":
1288            if nelectrons <= 5:
1289                return nelectrons
1290            return 10 - nelectrons
1291        if spin_config == "low":
1292            if coordination == "oct":
1293                if nelectrons <= 3:
1294                    return nelectrons
1295                if nelectrons <= 6:
1296                    return 6 - nelectrons
1297                if nelectrons <= 8:
1298                    return nelectrons - 6
1299                return 10 - nelectrons
1300            if coordination == "tet":
1301                if nelectrons <= 2:
1302                    return nelectrons
1303                if nelectrons <= 4:
1304                    return 4 - nelectrons
1305                if nelectrons <= 7:
1306                    return nelectrons - 4
1307                return 10 - nelectrons
1308        raise RuntimeError()
1309
1310    def __deepcopy__(self, memo):
1311        return Species(self.symbol, self.oxi_state, self._properties)
1312
1313    def as_dict(self) -> dict:
1314        """
1315        :return: Json-able dictionary representation.
1316        """
1317        d = {
1318            "@module": self.__class__.__module__,
1319            "@class": self.__class__.__name__,
1320            "element": self.symbol,
1321            "oxidation_state": self._oxi_state,
1322        }
1323        if self._properties:
1324            d["properties"] = self._properties
1325        return d
1326
1327    @classmethod
1328    def from_dict(cls, d) -> "Species":
1329        """
1330        :param d: Dict representation.
1331        :return: Species.
1332        """
1333        return cls(d["element"], d["oxidation_state"], d.get("properties", None))
1334
1335
1336class DummySpecies(Species):
1337    """
1338    A special specie for representing non-traditional elements or species. For
1339    example, representation of vacancies (charged or otherwise), or special
1340    sites, etc.
1341
1342    .. attribute:: oxi_state
1343
1344        Oxidation state associated with Species.
1345
1346    .. attribute:: Z
1347
1348        DummySpecies is always assigned an atomic number equal to the hash
1349        number of the symbol. Obviously, it makes no sense whatsoever to use
1350        the atomic number of a Dummy specie for anything scientific. The purpose
1351        of this is to ensure that for most use cases, a DummySpecies behaves no
1352        differently from an Element or Species.
1353
1354    .. attribute:: X
1355
1356        DummySpecies is always assigned an electronegativity of 0.
1357    """
1358
1359    def __init__(
1360        self,
1361        symbol: str = "X",
1362        oxidation_state: Optional[float] = 0,
1363        properties: Optional[dict] = None,
1364    ):
1365        """
1366        Args:
1367            symbol (str): An assigned symbol for the dummy specie. Strict
1368                rules are applied to the choice of the symbol. The dummy
1369                symbol cannot have any part of first two letters that will
1370                constitute an Element symbol. Otherwise, a composition may
1371                be parsed wrongly. E.g., "X" is fine, but "Vac" is not
1372                because Vac contains V, a valid Element.
1373            oxidation_state (float): Oxidation state for dummy specie.
1374                Defaults to zero.
1375        """
1376        # enforce title case to match other elements, reduces confusion
1377        # when multiple DummySpecies in a "formula" string
1378        symbol = symbol.title()
1379
1380        for i in range(1, min(2, len(symbol)) + 1):
1381            if Element.is_valid_symbol(symbol[:i]):
1382                raise ValueError("{} contains {}, which is a valid element " "symbol.".format(symbol, symbol[:i]))
1383
1384        # Set required attributes for DummySpecies to function like a Species in
1385        # most instances.
1386        self._symbol = symbol
1387        self._oxi_state = oxidation_state
1388        self._properties = properties if properties else {}
1389        for k, _ in self._properties.items():
1390            if k not in Species.supported_properties:
1391                raise ValueError("{} is not a supported property".format(k))
1392
1393    def __getattr__(self, a):
1394        # overriding getattr doens't play nice with pickle, so we
1395        # can't use self._properties
1396        p = object.__getattribute__(self, "_properties")
1397        if a in p:
1398            return p[a]
1399        raise AttributeError(a)
1400
1401    def __hash__(self):
1402        return self.symbol.__hash__()
1403
1404    def __eq__(self, other):
1405        """
1406        Species is equal to other only if element and oxidation states are
1407        exactly the same.
1408        """
1409        if not isinstance(other, DummySpecies):
1410            return False
1411        return (
1412            isinstance(other, Species)
1413            and self.symbol == other.symbol
1414            and self.oxi_state == other.oxi_state
1415            and self._properties == other._properties
1416        )
1417
1418    def __ne__(self, other):
1419        return not self.__eq__(other)
1420
1421    def __lt__(self, other):
1422        """
1423        Sets a default sort order for atomic species by electronegativity,
1424        followed by oxidation state.
1425        """
1426        if self.X != other.X:
1427            return self.X < other.X
1428        if self.symbol != other.symbol:
1429            # There are cases where the electronegativity are exactly equal.
1430            # We then sort by symbol.
1431            return self.symbol < other.symbol
1432        other_oxi = 0 if isinstance(other, Element) else other.oxi_state
1433        return self.oxi_state < other_oxi
1434
1435    @property
1436    def Z(self) -> int:
1437        """
1438        DummySpecies is always assigned an atomic number equal to the hash of
1439        the symbol. The expectation is that someone would be an actual dummy
1440        to use atomic numbers for a Dummy specie.
1441        """
1442        return self.symbol.__hash__()
1443
1444    @property
1445    def oxi_state(self) -> Optional[float]:
1446        """
1447        Oxidation state associated with DummySpecies
1448        """
1449        return self._oxi_state
1450
1451    @property
1452    def X(self) -> float:
1453        """
1454        DummySpecies is always assigned an electronegativity of 0. The effect of
1455        this is that DummySpecies are always sorted in front of actual Species.
1456        """
1457        return 0.0
1458
1459    @property
1460    def symbol(self) -> str:
1461        """
1462        :return: Symbol for DummySpecies.
1463        """
1464        return self._symbol
1465
1466    def __deepcopy__(self, memo):
1467        return DummySpecies(self.symbol, self._oxi_state)
1468
1469    @staticmethod
1470    def from_string(species_string: str) -> "DummySpecies":
1471        """
1472        Returns a Dummy from a string representation.
1473
1474        Args:
1475            species_string (str): A string representation of a dummy
1476                species, e.g., "X2+", "X3+".
1477
1478        Returns:
1479            A DummySpecies object.
1480
1481        Raises:
1482            ValueError if species_string cannot be intepreted.
1483        """
1484        m = re.search(r"([A-ZAa-z]*)([0-9.]*)([+\-]*)(.*)", species_string)
1485        if m:
1486            sym = m.group(1)
1487            if m.group(2) == "" and m.group(3) == "":
1488                oxi = 0.0
1489            else:
1490                oxi = 1.0 if m.group(2) == "" else float(m.group(2))
1491                oxi = -oxi if m.group(3) == "-" else oxi
1492            properties = None
1493            if m.group(4):
1494                toks = m.group(4).split("=")
1495                properties = {toks[0]: float(toks[1])}
1496            return DummySpecies(sym, oxi, properties)
1497        raise ValueError("Invalid DummySpecies String")
1498
1499    def as_dict(self) -> dict:
1500        """
1501        :return: MSONAble dict representation.
1502        """
1503        d = {
1504            "@module": self.__class__.__module__,
1505            "@class": self.__class__.__name__,
1506            "element": self.symbol,
1507            "oxidation_state": self._oxi_state,
1508        }
1509        if self._properties:
1510            d["properties"] = self._properties  # type: ignore
1511        return d
1512
1513    @classmethod
1514    def from_dict(cls, d) -> "DummySpecies":
1515        """
1516        :param d: Dict representation
1517        :return: DummySpecies
1518        """
1519        return cls(d["element"], d["oxidation_state"], d.get("properties", None))
1520
1521    def __repr__(self):
1522        return "DummySpecies " + self.__str__()
1523
1524    def __str__(self):
1525        output = self.symbol
1526        if self.oxi_state is not None:
1527            if self.oxi_state >= 0:
1528                output += formula_double_format(self.oxi_state) + "+"
1529            else:
1530                output += formula_double_format(-self.oxi_state) + "-"
1531        for p, v in self._properties.items():
1532            output += ",%s=%s" % (p, v)
1533        return output
1534
1535
1536class Specie(Species):
1537    """
1538    This maps the historical grammatically inaccurate Specie to Species
1539    to maintain backwards compatibility.
1540    """
1541
1542    pass
1543
1544
1545class DummySpecie(DummySpecies):
1546    """
1547    This maps the historical grammatically inaccurate DummySpecie to DummySpecies
1548    to maintain backwards compatibility.
1549    """
1550
1551    pass
1552
1553
1554def get_el_sp(obj) -> Union[Element, Species, DummySpecies]:
1555    """
1556    Utility method to get an Element or Species from an input obj.
1557    If obj is in itself an element or a specie, it is returned automatically.
1558    If obj is an int or a string representing an integer, the Element
1559    with the atomic number obj is returned.
1560    If obj is a string, Species parsing will be attempted (e.g., Mn2+), failing
1561    which Element parsing will be attempted (e.g., Mn), failing which
1562    DummyElement parsing will be attempted.
1563
1564    Args:
1565        obj (Element/Species/str/int): An arbitrary object.  Supported objects
1566            are actual Element/Species objects, integers (representing atomic
1567            numbers) or strings (element symbols or species strings).
1568
1569    Returns:
1570        Species or Element, with a bias for the maximum number of properties
1571        that can be determined.
1572
1573    Raises:
1574        ValueError if obj cannot be converted into an Element or Species.
1575    """
1576    if isinstance(obj, (Element, Species, DummySpecies)):
1577        return obj
1578
1579    try:
1580        c = float(obj)
1581        i = int(c)
1582        i = i if i == c else None  # type: ignore
1583    except (ValueError, TypeError):
1584        i = None  # type: ignore
1585
1586    if i is not None:
1587        return Element.from_Z(i)
1588
1589    try:
1590        return Species.from_string(obj)
1591    except (ValueError, KeyError):
1592        try:
1593            return Element(obj)
1594        except (ValueError, KeyError):
1595            try:
1596                return DummySpecies.from_string(obj)
1597            except Exception:
1598                raise ValueError("Can't parse Element or String from type" " %s: %s." % (type(obj), obj))
1599