• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

materialsproject / pymatgen / 4075885785

pending completion
4075885785

push

github

Shyue Ping Ong
Merge branch 'master' of github.com:materialsproject/pymatgen

96 of 96 new or added lines in 27 files covered. (100.0%)

81013 of 102710 relevant lines covered (78.88%)

0.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.0
/pymatgen/core/periodic_table.py
1
# Copyright (c) Pymatgen Development Team.
2
# Distributed under the terms of the MIT License.
3

4
"""Module contains classes presenting Element and Species (Element + oxidation state) and PeriodicTable."""
1✔
5

6
from __future__ import annotations
1✔
7

8
import ast
1✔
9
import functools
1✔
10
import json
1✔
11
import re
1✔
12
import warnings
1✔
13
from collections import Counter
1✔
14
from enum import Enum
1✔
15
from itertools import combinations, product
1✔
16
from pathlib import Path
1✔
17
from typing import Any, Callable, Literal
1✔
18

19
import numpy as np
1✔
20
from monty.json import MSONable
1✔
21

22
from pymatgen.core.units import SUPPORTED_UNIT_NAMES, FloatWithUnit, Length, Mass, Unit
1✔
23
from pymatgen.util.string import Stringify, formula_double_format
1✔
24

25
# Loads element data from json file
26
with open(str(Path(__file__).absolute().parent / "periodic_table.json")) as f:
1✔
27
    _pt_data = json.load(f)
1✔
28

29
_pt_row_sizes = (2, 8, 8, 18, 18, 32, 32)
1✔
30

31

32
@functools.total_ordering
1✔
33
class ElementBase(Enum):
1✔
34
    """Element class defined without any enum values so it can be subclassed."""
35

36
    def __init__(self, symbol: str):
1✔
37
        """
38
        Basic immutable element object with all relevant properties.
39

40
        Only one instance of Element for each symbol is stored after creation,
41
        ensuring that a particular element behaves like a singleton. For all
42
        attributes, missing data (i.e., data for which is not available) is
43
        represented by a None unless otherwise stated.
44

45
        Args:
46
            symbol (str): Element symbol, e.g., "H", "Fe"
47

48
        .. attribute:: Z
49

50
            Atomic number
51

52
        .. attribute:: symbol
53

54
            Element symbol
55

56
        .. attribute:: long_name
57

58
           Long name for element. E.g., "Hydrogen".
59

60
        .. attribute:: atomic_radius_calculated
61

62
            Calculated atomic radius for the element. This is the empirical value.
63
            Data is obtained from
64
            http://en.wikipedia.org/wiki/Atomic_radii_of_the_elements_(data_page).
65

66
        .. attribute:: van_der_waals_radius
67

68
            Van der Waals radius for the element. This is the empirical
69
            value determined from critical reviews of X-ray diffraction, gas kinetic
70
            collision cross-section, and other experimental data by Bondi and later
71
            workers. The uncertainty in these values is on the order of 0.1 â„«.
72

73
            Data are obtained from
74

75
            "Atomic Radii of the Elements" in CRC Handbook of Chemistry and Physics,
76
                91st Ed.; Haynes, W.M., Ed.; CRC Press: Boca Raton, FL, 2010.
77

78
        .. attribute:: mendeleev_no
79

80
            Mendeleev number from definition given by Pettifor, D. G. (1984).
81
            A chemical scale for crystal-structure maps. Solid State Communications,
82
            51 (1), 31-34
83

84
        .. attribute:: electrical_resistivity
85

86
            Electrical resistivity
87

88
        .. attribute:: velocity_of_sound
89

90
            Velocity of sound
91

92
        .. attribute:: reflectivity
93

94
            Reflectivity
95

96
        .. attribute:: refractive_index
97

98
            Refractice index
99

100
        .. attribute:: poissons_ratio
101

102
            Poisson's ratio
103

104
        .. attribute:: molar_volume
105

106
            Molar volume
107

108
        .. attribute:: electronic_structure
109

110
            Electronic structure.
111
            E.g., The electronic structure for Fe is represented as
112
            [Ar].3d6.4s2
113

114
        .. attribute:: atomic_orbitals
115

116
            Atomic Orbitals. Energy of the atomic orbitals as a dict.
117
            E.g., The orbitals energies in eV are represented as
118
            {'1s': -1.0, '2s': -0.1}
119
            Data is obtained from
120
            https://www.nist.gov/pml/data/atomic-reference-data-electronic-structure-calculations
121
            The LDA values for neutral atoms are used
122

123
        .. attribute:: thermal_conductivity
124

125
            Thermal conductivity
126

127
        .. attribute:: boiling_point
128

129
            Boiling point
130

131
        .. attribute:: melting_point
132

133
            Melting point
134

135
        .. attribute:: critical_temperature
136

137
            Critical temperature
138

139
        .. attribute:: superconduction_temperature
140

141
            Superconduction temperature
142

143
        .. attribute:: liquid_range
144

145
            Liquid range
146

147
        .. attribute:: bulk_modulus
148

149
            Bulk modulus
150

151
        .. attribute:: youngs_modulus
152

153
            Young's modulus
154

155
        .. attribute:: brinell_hardness
156

157
            Brinell hardness
158

159
        .. attribute:: rigidity_modulus
160

161
            Rigidity modulus
162

163
        .. attribute:: mineral_hardness
164

165
            Mineral hardness
166

167
        .. attribute:: vickers_hardness
168

169
            Vicker's hardness
170

171
        .. attribute:: density_of_solid
172

173
            Density of solid phase
174

175
        .. attribute:: coefficient_of_linear_thermal_expansion
176

177
            Coefficient of linear thermal expansion
178

179
        .. attribute:: ground_level
180

181
            Ground level for element
182

183
        .. attribute:: ionization_energies
184

185
            List of ionization energies. First value is the first ionization energy, second is the second ionization
186
            energy, etc. Note that this is zero-based indexing! So Element.ionization_energies[0] refer to the 1st
187
            ionization energy. Values are from the NIST Atomic Spectra Database. Missing values are None.
188
        """
189
        self.symbol = symbol
1✔
190
        d = _pt_data[symbol]
1✔
191

192
        # Store key variables for quick access
193
        self.Z = d["Atomic no"]
1✔
194

195
        at_r = d.get("Atomic radius", "no data")
1✔
196
        if str(at_r).startswith("no data"):
1✔
197
            self._atomic_radius = None
1✔
198
        else:
199
            self._atomic_radius = Length(at_r, "ang")
1✔
200
        self._atomic_mass = Mass(d["Atomic mass"], "amu")
1✔
201
        self.long_name = d["Name"]
1✔
202
        self._data = d
1✔
203

204
    @property
1✔
205
    def X(self) -> float:
1✔
206
        """
207
        :return: Electronegativity of element. Note that if an element does not
208
            have an electronegativity, a NaN float is returned.
209
        """
210
        if "X" in self._data:
1✔
211
            return self._data["X"]
1✔
212
        warnings.warn(
1✔
213
            f"No electronegativity for {self.symbol}. Setting to NaN. This has no physical meaning, "
214
            "and is mainly done to avoid errors caused by the code expecting a float."
215
        )
216
        return float("NaN")
1✔
217

218
    @property
1✔
219
    def atomic_radius(self) -> FloatWithUnit | None:
1✔
220
        """
221
        Returns:
222
            float | None: The atomic radius of the element in Ã…ngstroms. Can be None for
223
                some elements like noble gases.
224
        """
225
        return self._atomic_radius
1✔
226

227
    @property
1✔
228
    def atomic_mass(self) -> FloatWithUnit:
1✔
229
        """
230
        Returns:
231
            float: The atomic mass of the element in amu.
232
        """
233
        return self._atomic_mass
1✔
234

235
    def __getattr__(self, item: str) -> Any:
1✔
236
        """Key access to available element data.
237

238
        Args:
239
            item (str): Attribute name.
240

241
        Raises:
242
            AttributeError: If item not in _pt_data.
243
        """
244
        if item in [
1✔
245
            "mendeleev_no",
246
            "electrical_resistivity",
247
            "velocity_of_sound",
248
            "reflectivity",
249
            "refractive_index",
250
            "poissons_ratio",
251
            "molar_volume",
252
            "thermal_conductivity",
253
            "boiling_point",
254
            "melting_point",
255
            "critical_temperature",
256
            "superconduction_temperature",
257
            "liquid_range",
258
            "bulk_modulus",
259
            "youngs_modulus",
260
            "brinell_hardness",
261
            "rigidity_modulus",
262
            "mineral_hardness",
263
            "vickers_hardness",
264
            "density_of_solid",
265
            "atomic_radius_calculated",
266
            "van_der_waals_radius",
267
            "atomic_orbitals",
268
            "coefficient_of_linear_thermal_expansion",
269
            "ground_state_term_symbol",
270
            "valence",
271
            "ground_level",
272
            "ionization_energies",
273
        ]:
274
            kstr = item.capitalize().replace("_", " ")
1✔
275
            val = self._data.get(kstr, None)
1✔
276
            if str(val).startswith("no data"):
1✔
277
                val = None
×
278
            elif isinstance(val, (list, dict)):
1✔
279
                pass
1✔
280
            else:
281
                try:
1✔
282
                    val = float(val)
1✔
283
                except ValueError:
1✔
284
                    nobracket = re.sub(r"\(.*\)", "", val)
1✔
285
                    toks = nobracket.replace("about", "").strip().split(" ", 1)
1✔
286
                    if len(toks) == 2:
1✔
287
                        try:
1✔
288
                            if "10<sup>" in toks[1]:
1✔
289
                                base_power = re.findall(r"([+-]?\d+)", toks[1])
1✔
290
                                factor = "e" + base_power[1]
1✔
291
                                if toks[0] in ["&gt;", "high"]:
1✔
292
                                    toks[0] = "1"  # return the border value
1✔
293
                                toks[0] += factor
1✔
294
                                if item == "electrical_resistivity":
1✔
295
                                    unit = "ohm m"
1✔
296
                                elif item == "coefficient_of_linear_thermal_expansion":
1✔
297
                                    unit = "K^-1"
1✔
298
                                else:
299
                                    unit = toks[1]
×
300
                                val = FloatWithUnit(toks[0], unit)
1✔
301
                            else:
302
                                unit = toks[1].replace("<sup>", "^").replace("</sup>", "").replace("&Omega;", "ohm")
1✔
303
                                units = Unit(unit)
1✔
304
                                if set(units).issubset(SUPPORTED_UNIT_NAMES):
1✔
305
                                    val = FloatWithUnit(toks[0], unit)
1✔
306
                        except ValueError:
1✔
307
                            # Ignore error. val will just remain a string.
308
                            pass
1✔
309
            return val
1✔
310
        raise AttributeError(f"Element has no attribute {item}!")
1✔
311

312
    @property
1✔
313
    def data(self) -> dict[str, Any]:
1✔
314
        """
315
        Returns dict of data for element.
316
        """
317
        return self._data.copy()
1✔
318

319
    @property
1✔
320
    def ionization_energy(self) -> float:
1✔
321
        """
322
        First ionization energy of element.
323
        """
324
        return self._data["Ionization energies"][0]
1✔
325

326
    @property
1✔
327
    def electron_affinity(self) -> float:
1✔
328
        """
329
        The amount of energy released when an electron is attached to a neutral atom.
330
        """
331
        return self._data["Electron affinity"]
1✔
332

333
    @property
1✔
334
    def electronic_structure(self) -> str:
1✔
335
        """
336
        Electronic structure as string, with only valence electrons.
337
        E.g., The electronic structure for Fe is represented as '[Ar].3d6.4s2'
338
        """
339
        return re.sub("</*sup>", "", self._data["Electronic structure"])
1✔
340

341
    @property
1✔
342
    def average_ionic_radius(self) -> FloatWithUnit:
1✔
343
        """
344
        Average ionic radius for element (with units). The average is taken
345
        over all oxidation states of the element for which data is present.
346
        """
347
        if "Ionic radii" in self._data:
×
348
            radii = self._data["Ionic radii"]
×
349
            radius = sum(radii.values()) / len(radii)
×
350
        else:
351
            radius = 0.0
×
352
        return FloatWithUnit(radius, "ang")
×
353

354
    @property
1✔
355
    def average_cationic_radius(self) -> FloatWithUnit:
1✔
356
        """
357
        Average cationic radius for element (with units). The average is
358
        taken over all positive oxidation states of the element for which
359
        data is present.
360
        """
361
        if "Ionic radii" in self._data:
1✔
362
            radii = [v for k, v in self._data["Ionic radii"].items() if int(k) > 0]
×
363
            if radii:
×
364
                return FloatWithUnit(sum(radii) / len(radii), "ang")
×
365
        return FloatWithUnit(0.0, "ang")
1✔
366

367
    @property
1✔
368
    def average_anionic_radius(self) -> float:
1✔
369
        """
370
        Average anionic radius for element (with units). The average is
371
        taken over all negative oxidation states of the element for which
372
        data is present.
373
        """
374
        if "Ionic radii" in self._data:
1✔
375
            radii = [v for k, v in self._data["Ionic radii"].items() if int(k) < 0]
×
376
            if radii:
×
377
                return FloatWithUnit(sum(radii) / len(radii), "ang")
×
378
        return FloatWithUnit(0.0, "ang")
1✔
379

380
    @property
1✔
381
    def ionic_radii(self) -> dict[int, float]:
1✔
382
        """
383
        All ionic radii of the element as a dict of
384
        {oxidation state: ionic radii}. Radii are given in angstrom.
385
        """
386
        if "Ionic radii" in self._data:
1✔
387
            return {int(k): FloatWithUnit(v, "ang") for k, v in self._data["Ionic radii"].items()}
1✔
388
        return {}
1✔
389

390
    @property
1✔
391
    def number(self) -> int:
1✔
392
        """Alternative attribute for atomic number Z"""
393
        return self.Z
1✔
394

395
    @property
1✔
396
    def max_oxidation_state(self) -> float:
1✔
397
        """Maximum oxidation state for element"""
398
        if "Oxidation states" in self._data:
1✔
399
            return max(self._data["Oxidation states"])
1✔
400
        return 0
1✔
401

402
    @property
1✔
403
    def min_oxidation_state(self) -> float:
1✔
404
        """Minimum oxidation state for element"""
405
        if "Oxidation states" in self._data:
1✔
406
            return min(self._data["Oxidation states"])
1✔
407
        return 0
1✔
408

409
    @property
1✔
410
    def oxidation_states(self) -> tuple[int, ...]:
1✔
411
        """Tuple of all known oxidation states"""
412
        return tuple(int(x) for x in self._data.get("Oxidation states", []))
1✔
413

414
    @property
1✔
415
    def common_oxidation_states(self) -> tuple[int, ...]:
1✔
416
        """Tuple of common oxidation states"""
417
        return tuple(self._data.get("Common oxidation states", []))
1✔
418

419
    @property
1✔
420
    def icsd_oxidation_states(self) -> tuple[int, ...]:
1✔
421
        """Tuple of all oxidation states with at least 10 instances in
422
        ICSD database AND at least 1% of entries for that element"""
423
        return tuple(self._data.get("ICSD oxidation states", []))
1✔
424

425
    @property
1✔
426
    def metallic_radius(self) -> float:
1✔
427
        """
428
        Metallic radius of the element. Radius is given in ang.
429
        """
430
        return FloatWithUnit(self._data["Metallic radius"], "ang")
1✔
431

432
    @property
1✔
433
    def full_electronic_structure(self) -> list[tuple[int, str, int]]:
1✔
434
        """
435
        Full electronic structure as tuple.
436
        E.g., The electronic structure for Fe is represented as:
437
        [(1, "s", 2), (2, "s", 2), (2, "p", 6), (3, "s", 2), (3, "p", 6),
438
        (3, "d", 6), (4, "s", 2)]
439
        """
440
        estr = self.electronic_structure
1✔
441

442
        def parse_orbital(orbstr):
1✔
443
            m = re.match(r"(\d+)([spdfg]+)(\d+)", orbstr)
1✔
444
            if m:
1✔
445
                return int(m.group(1)), m.group(2), int(m.group(3))
1✔
446
            return orbstr
1✔
447

448
        data = [parse_orbital(s) for s in estr.split(".")]
1✔
449
        if data[0][0] == "[":
1✔
450
            sym = data[0].replace("[", "").replace("]", "")
1✔
451
            data = list(Element(sym).full_electronic_structure) + data[1:]
1✔
452
        return data
1✔
453

454
    @property
1✔
455
    def valence(self):
1✔
456
        """
457
        From full electron config obtain valence subshell angular moment (L) and number of valence e- (v_e)
458
        """
459
        # The number of valence of noble gas is 0
460
        if self.group == 18:
1✔
461
            return np.nan, 0
1✔
462

463
        L_symbols = "SPDFGHIKLMNOQRTUVWXYZ"
1✔
464
        valence = []
1✔
465
        full_electron_config = self.full_electronic_structure
1✔
466
        last_orbital = full_electron_config[-1]
1✔
467
        for n, l_symbol, ne in full_electron_config:
1✔
468
            l = L_symbols.lower().index(l_symbol)
1✔
469
            if ne < (2 * l + 1) * 2:
1✔
470
                valence.append((l, ne))
1✔
471
            # check for full last shell (e.g. column 2)
472
            elif (n, l_symbol, ne) == last_orbital and ne == (2 * l + 1) * 2 and len(valence) == 0:
1✔
473
                valence.append((l, ne))
1✔
474
        if len(valence) > 1:
1✔
475
            raise ValueError("Ambiguous valence")
1✔
476

477
        return valence[0]
1✔
478

479
    @property
1✔
480
    def term_symbols(self) -> list[list[str]]:
1✔
481
        """
482
        All possible  Russell-Saunders term symbol of the Element.
483
        eg. L = 1, n_e = 2 (s2) returns [['1D2'], ['3P0', '3P1', '3P2'], ['1S0']]
484
        """
485
        L_symbols = "SPDFGHIKLMNOQRTUVWXYZ"
1✔
486

487
        L, v_e = self.valence
1✔
488

489
        # for one electron in subshell L
490
        ml = list(range(-L, L + 1))
1✔
491
        ms = [1 / 2, -1 / 2]
1✔
492
        # all possible configurations of ml,ms for one e in subshell L
493
        ml_ms = list(product(ml, ms))
1✔
494

495
        # Number of possible configurations for r electrons in subshell L.
496
        n = (2 * L + 1) * 2
1✔
497
        # the combination of n_e electrons configurations
498
        # C^{n}_{n_e}
499
        e_config_combs = list(combinations(range(n), v_e))
1✔
500

501
        # Total ML = sum(ml1, ml2), Total MS = sum(ms1, ms2)
502
        TL = [sum(ml_ms[comb[e]][0] for e in range(v_e)) for comb in e_config_combs]
1✔
503
        TS = [sum(ml_ms[comb[e]][1] for e in range(v_e)) for comb in e_config_combs]
1✔
504
        comb_counter = Counter(zip(TL, TS))
1✔
505

506
        term_symbols = []
1✔
507
        while sum(comb_counter.values()) > 0:
1✔
508
            # Start from the lowest freq combination,
509
            # which corresponds to largest abs(L) and smallest abs(S)
510
            L, S = min(comb_counter)
1✔
511

512
            J = list(np.arange(abs(L - S), abs(L) + abs(S) + 1))
1✔
513
            term_symbols.append([str(int(2 * (abs(S)) + 1)) + L_symbols[abs(L)] + str(j) for j in J])
1✔
514

515
            # Delete all configurations included in this term
516
            for ML in range(-L, L - 1, -1):
1✔
517
                for MS in np.arange(S, -S + 1, 1):
1✔
518
                    if (ML, MS) in comb_counter:
1✔
519
                        comb_counter[(ML, MS)] -= 1
1✔
520
                        if comb_counter[(ML, MS)] == 0:
1✔
521
                            del comb_counter[(ML, MS)]
1✔
522
        return term_symbols
1✔
523

524
    @property
1✔
525
    def ground_state_term_symbol(self):
1✔
526
        """
527
        Ground state term symbol
528
        Selected based on Hund's Rule
529
        """
530
        L_symbols = "SPDFGHIKLMNOQRTUVWXYZ"
1✔
531

532
        term_symbols = self.term_symbols
1✔
533
        term_symbol_flat = {
1✔
534
            term: {
535
                "multiplicity": int(term[0]),
536
                "L": L_symbols.index(term[1]),
537
                "J": float(term[2:]),
538
            }
539
            for term in sum(term_symbols, [])
540
        }
541

542
        multi = [int(item["multiplicity"]) for terms, item in term_symbol_flat.items()]
1✔
543
        max_multi_terms = {
1✔
544
            symbol: item for symbol, item in term_symbol_flat.items() if item["multiplicity"] == max(multi)
545
        }
546

547
        Ls = [item["L"] for terms, item in max_multi_terms.items()]
1✔
548
        max_L_terms = {symbol: item for symbol, item in term_symbol_flat.items() if item["L"] == max(Ls)}
1✔
549

550
        J_sorted_terms = sorted(max_L_terms.items(), key=lambda k: k[1]["J"])
1✔
551
        L, v_e = self.valence
1✔
552
        if v_e <= (2 * L + 1):
1✔
553
            return J_sorted_terms[0][0]
1✔
554
        return J_sorted_terms[-1][0]
1✔
555

556
    def __eq__(self, other: object) -> bool:
1✔
557
        return isinstance(other, Element) and self.Z == other.Z
1✔
558

559
    def __hash__(self):
1✔
560
        return self.Z
1✔
561

562
    def __repr__(self):
1✔
563
        return "Element " + self.symbol
1✔
564

565
    def __str__(self):
1✔
566
        return self.symbol
1✔
567

568
    def __lt__(self, other):
1✔
569
        """
570
        Sets a default sort order for atomic species by electronegativity. Very
571
        useful for getting correct formulas. For example, FeO4PLi is
572
        automatically sorted into LiFePO4.
573
        """
574
        if not hasattr(other, "X") or not hasattr(other, "symbol"):
1✔
575
            return NotImplemented
1✔
576
        x1 = float("inf") if self.X != self.X else self.X
1✔
577
        x2 = float("inf") if other.X != other.X else other.X
1✔
578
        if x1 != x2:
1✔
579
            return x1 < x2
1✔
580

581
        # There are cases where the electronegativity are exactly equal.
582
        # We then sort by symbol.
583
        return self.symbol < other.symbol
1✔
584

585
    @staticmethod
1✔
586
    def from_Z(Z: int) -> Element:
1✔
587
        """
588
        Get an element from an atomic number.
589

590
        Args:
591
            Z (int): Atomic number
592

593
        Returns:
594
            Element with atomic number Z.
595
        """
596
        for sym, data in _pt_data.items():
1✔
597
            if data["Atomic no"] == Z:
1✔
598
                return Element(sym)
1✔
599
        raise ValueError(f"No element with this atomic number {Z}")
1✔
600

601
    @staticmethod
1✔
602
    def from_name(name: str) -> Element:
1✔
603
        """
604
        Get an element from its long name.
605

606
        Args:
607
            name: Long name of the element, e.g. 'Hydrogen' or
608
                  'Iron'. Not case-sensitive.
609
        Returns:
610
            Element with the name 'name'
611
        """
612
        for sym, data in _pt_data.items():
1✔
613
            if data["Name"] == name.capitalize():
1✔
614
                return Element(sym)
1✔
615
        raise ValueError(f"No element with the name {name}")
×
616

617
    @staticmethod
1✔
618
    def from_row_and_group(row: int, group: int) -> Element:
1✔
619
        """
620
        Returns an element from a row and group number.
621
        Important Note: For lanthanoids and actinoids, the row number must
622
        be 8 and 9, respectively, and the group number must be
623
        between 3 (La, Ac) and 17 (Lu, Lr). This is different than the
624
        value for Element(symbol).row and Element(symbol).group for these
625
        elements.
626

627
        Args:
628
            row (int): (pseudo) row number. This is the
629
                standard row number except for the lanthanoids
630
                and actinoids for which it is 8 or 9, respectively.
631
            group (int): (pseudo) group number. This is the
632
                standard group number except for the lanthanoids
633
                and actinoids for which it is 3 (La, Ac) to 17 (Lu, Lr).
634

635
        .. note::
636
            The 18 group number system is used, i.e., Noble gases are group 18.
637
        """
638
        for sym in _pt_data:
1✔
639
            el = Element(sym)
1✔
640
            if 57 <= el.Z <= 71:
1✔
641
                el_pseudorow = 8
1✔
642
                el_pseudogroup = (el.Z - 54) % 32
1✔
643
            elif 89 <= el.Z <= 103:
1✔
644
                el_pseudorow = 9
1✔
645
                el_pseudogroup = (el.Z - 54) % 32
1✔
646
            else:
647
                el_pseudorow = el.row
1✔
648
                el_pseudogroup = el.group
1✔
649
            if el_pseudorow == row and el_pseudogroup == group:
1✔
650
                return el
1✔
651
        raise ValueError("No element with this row and group!")
1✔
652

653
    @staticmethod
1✔
654
    def is_valid_symbol(symbol: str) -> bool:
1✔
655
        """
656
        Returns true if symbol is a valid element symbol.
657

658
        Args:
659
            symbol (str): Element symbol
660

661
        Returns:
662
            True if symbol is a valid element (e.g., "H"). False otherwise
663
            (e.g., "Zebra").
664
        """
665
        return symbol in Element.__members__
1✔
666

667
    @property
1✔
668
    def row(self) -> int:
1✔
669
        """
670
        Returns the periodic table row of the element.
671
        Note: For lanthanoids and actinoids, the row is always 6 or 7,
672
        respectively.
673
        """
674
        z = self.Z
1✔
675
        total = 0
1✔
676
        if 57 <= z <= 71:
1✔
677
            return 6
1✔
678
        if 89 <= z <= 103:
1✔
679
            return 7
1✔
680
        for i, size in enumerate(_pt_row_sizes):
1✔
681
            total += size
1✔
682
            if total >= z:
1✔
683
                return i + 1
1✔
684
        return 8
×
685

686
    @property
1✔
687
    def group(self) -> int:
1✔
688
        """
689
        Returns the periodic table group of the element.
690
        Note: For lanthanoids and actinoids, the group is always 3.
691
        """
692
        z = self.Z
1✔
693
        if z == 1:
1✔
694
            return 1
1✔
695
        if z == 2:
1✔
696
            return 18
1✔
697

698
        if 3 <= z <= 18:
1✔
699
            if (z - 2) % 8 == 0:
1✔
700
                return 18
1✔
701
            if (z - 2) % 8 <= 2:
1✔
702
                return (z - 2) % 8
1✔
703
            return 10 + (z - 2) % 8
1✔
704

705
        if 19 <= z <= 54:
1✔
706
            if (z - 18) % 18 == 0:
1✔
707
                return 18
1✔
708
            return (z - 18) % 18
1✔
709

710
        if (57 <= z <= 71) or (89 <= z <= 103):
1✔
711
            return 3
1✔
712

713
        if (z - 54) % 32 == 0:
1✔
714
            return 18
1✔
715
        if (z - 54) % 32 >= 18:
1✔
716
            return (z - 54) % 32 - 14
1✔
717
        return (z - 54) % 32
1✔
718

719
    @property
1✔
720
    def block(self) -> str:
1✔
721
        """
722
        Return the block character "s,p,d,f"
723
        """
724
        if (self.is_actinoid or self.is_lanthanoid) and self.Z not in [71, 103]:
1✔
725
            return "f"
1✔
726
        if self.is_actinoid or self.is_lanthanoid:
1✔
727
            return "d"
1✔
728
        if self.group in [1, 2]:
1✔
729
            return "s"
1✔
730
        if self.group in range(13, 19):
1✔
731
            return "p"
1✔
732
        if self.group in range(3, 13):
1✔
733
            return "d"
1✔
734
        raise ValueError("unable to determine block")
×
735

736
    @property
1✔
737
    def is_noble_gas(self) -> bool:
1✔
738
        """
739
        True if element is noble gas.
740
        """
741
        return self.Z in (2, 10, 18, 36, 54, 86, 118)
1✔
742

743
    @property
1✔
744
    def is_transition_metal(self) -> bool:
1✔
745
        """
746
        True if element is a transition metal.
747
        """
748
        ns = list(range(21, 31))
1✔
749
        ns.extend(list(range(39, 49)))
1✔
750
        ns.append(57)
1✔
751
        ns.extend(list(range(72, 81)))
1✔
752
        ns.append(89)
1✔
753
        ns.extend(list(range(104, 113)))
1✔
754
        return self.Z in ns
1✔
755

756
    @property
1✔
757
    def is_post_transition_metal(self) -> bool:
1✔
758
        """
759
        True if element is a post-transition or poor metal.
760
        """
761
        return self.symbol in ("Al", "Ga", "In", "Tl", "Sn", "Pb", "Bi")
1✔
762

763
    @property
1✔
764
    def is_rare_earth_metal(self) -> bool:
1✔
765
        """
766
        True if element is a rare earth metal.
767
        """
768
        return self.is_lanthanoid or self.is_actinoid
×
769

770
    @property
1✔
771
    def is_metal(self) -> bool:
1✔
772
        """
773
        True if is a metal.
774
        """
775
        return (
1✔
776
            self.is_alkali
777
            or self.is_alkaline
778
            or self.is_post_transition_metal
779
            or self.is_transition_metal
780
            or self.is_lanthanoid
781
            or self.is_actinoid
782
        )
783

784
    @property
1✔
785
    def is_metalloid(self) -> bool:
1✔
786
        """
787
        True if element is a metalloid.
788
        """
789
        return self.symbol in ("B", "Si", "Ge", "As", "Sb", "Te", "Po")
1✔
790

791
    @property
1✔
792
    def is_alkali(self) -> bool:
1✔
793
        """
794
        True if element is an alkali metal.
795
        """
796
        return self.Z in (3, 11, 19, 37, 55, 87)
1✔
797

798
    @property
1✔
799
    def is_alkaline(self) -> bool:
1✔
800
        """
801
        True if element is an alkaline earth metal (group II).
802
        """
803
        return self.Z in (4, 12, 20, 38, 56, 88)
1✔
804

805
    @property
1✔
806
    def is_halogen(self) -> bool:
1✔
807
        """
808
        True if element is a halogen.
809
        """
810
        return self.Z in (9, 17, 35, 53, 85)
1✔
811

812
    @property
1✔
813
    def is_chalcogen(self) -> bool:
1✔
814
        """
815
        True if element is a chalcogen.
816
        """
817
        return self.Z in (8, 16, 34, 52, 84)
1✔
818

819
    @property
1✔
820
    def is_lanthanoid(self) -> bool:
1✔
821
        """
822
        True if element is a lanthanoid.
823
        """
824
        return 56 < self.Z < 72
1✔
825

826
    @property
1✔
827
    def is_actinoid(self) -> bool:
1✔
828
        """
829
        True if element is a actinoid.
830
        """
831
        return 88 < self.Z < 104
1✔
832

833
    @property
1✔
834
    def is_quadrupolar(self) -> bool:
1✔
835
        """
836
        Checks if this element can be quadrupolar.
837
        """
838
        return len(self.data.get("NMR Quadrupole Moment", {})) > 0
×
839

840
    @property
1✔
841
    def nmr_quadrupole_moment(self) -> dict[str, FloatWithUnit]:
1✔
842
        """
843
        Get a dictionary the nuclear electric quadrupole moment in units of
844
        e*millibarns for various isotopes
845
        """
846
        return {k: FloatWithUnit(v, "mbarn") for k, v in self.data.get("NMR Quadrupole Moment", {}).items()}
1✔
847

848
    @property
1✔
849
    def iupac_ordering(self):
1✔
850
        """
851
        Ordering according to Table VI of "Nomenclature of Inorganic Chemistry
852
        (IUPAC Recommendations 2005)". This ordering effectively follows the
853
        groups and rows of the periodic table, except the Lanthanides, Actinides
854
        and hydrogen.
855
        """
856
        return self._data["IUPAC ordering"]
1✔
857

858
    def __deepcopy__(self, memo):
1✔
859
        return Element(self.symbol)
1✔
860

861
    @staticmethod
1✔
862
    def from_dict(d) -> Element:
1✔
863
        """
864
        Makes Element obey the general json interface used in pymatgen for
865
        easier serialization.
866
        """
867
        return Element(d["element"])
1✔
868

869
    def as_dict(self) -> dict[Literal["element", "@module", "@class"], str]:
1✔
870
        """
871
        Makes Element obey the general json interface used in pymatgen for
872
        easier serialization.
873
        """
874
        return {
1✔
875
            "@module": type(self).__module__,
876
            "@class": type(self).__name__,
877
            "element": self.symbol,
878
        }
879

880
    @staticmethod
1✔
881
    def print_periodic_table(filter_function: Callable | None = None):
1✔
882
        """
883
        A pretty ASCII printer for the periodic table, based on some
884
        filter_function.
885

886
        Args:
887
            filter_function: A filtering function taking an Element as input
888
                and returning a boolean. For example, setting
889
                filter_function = lambda el: el.X > 2 will print a periodic
890
                table containing only elements with electronegativity > 2.
891
        """
892
        for row in range(1, 10):
1✔
893
            rowstr = []
1✔
894
            for group in range(1, 19):
1✔
895
                try:
1✔
896
                    el = Element.from_row_and_group(row, group)
1✔
897
                except ValueError:
1✔
898
                    el = None
1✔
899
                if el and ((not filter_function) or filter_function(el)):
1✔
900
                    rowstr.append(f"{el.symbol:3s}")
1✔
901
                else:
902
                    rowstr.append("   ")
1✔
903
            print(" ".join(rowstr))
1✔
904

905

906
@functools.total_ordering
1✔
907
class Element(ElementBase):
1✔
908
    """Enum representing an element in the periodic table."""
909

910
    # This name = value convention is redundant and dumb, but unfortunately is
911
    # necessary to preserve backwards compatibility with a time when Element is
912
    # a regular object that is constructed with Element(symbol).
913
    H = "H"
1✔
914
    He = "He"
1✔
915
    Li = "Li"
1✔
916
    Be = "Be"
1✔
917
    B = "B"
1✔
918
    C = "C"
1✔
919
    N = "N"
1✔
920
    O = "O"
1✔
921
    F = "F"
1✔
922
    Ne = "Ne"
1✔
923
    Na = "Na"
1✔
924
    Mg = "Mg"
1✔
925
    Al = "Al"
1✔
926
    Si = "Si"
1✔
927
    P = "P"
1✔
928
    S = "S"
1✔
929
    Cl = "Cl"
1✔
930
    Ar = "Ar"
1✔
931
    K = "K"
1✔
932
    Ca = "Ca"
1✔
933
    Sc = "Sc"
1✔
934
    Ti = "Ti"
1✔
935
    V = "V"
1✔
936
    Cr = "Cr"
1✔
937
    Mn = "Mn"
1✔
938
    Fe = "Fe"
1✔
939
    Co = "Co"
1✔
940
    Ni = "Ni"
1✔
941
    Cu = "Cu"
1✔
942
    Zn = "Zn"
1✔
943
    Ga = "Ga"
1✔
944
    Ge = "Ge"
1✔
945
    As = "As"
1✔
946
    Se = "Se"
1✔
947
    Br = "Br"
1✔
948
    Kr = "Kr"
1✔
949
    Rb = "Rb"
1✔
950
    Sr = "Sr"
1✔
951
    Y = "Y"
1✔
952
    Zr = "Zr"
1✔
953
    Nb = "Nb"
1✔
954
    Mo = "Mo"
1✔
955
    Tc = "Tc"
1✔
956
    Ru = "Ru"
1✔
957
    Rh = "Rh"
1✔
958
    Pd = "Pd"
1✔
959
    Ag = "Ag"
1✔
960
    Cd = "Cd"
1✔
961
    In = "In"
1✔
962
    Sn = "Sn"
1✔
963
    Sb = "Sb"
1✔
964
    Te = "Te"
1✔
965
    I = "I"
1✔
966
    Xe = "Xe"
1✔
967
    Cs = "Cs"
1✔
968
    Ba = "Ba"
1✔
969
    La = "La"
1✔
970
    Ce = "Ce"
1✔
971
    Pr = "Pr"
1✔
972
    Nd = "Nd"
1✔
973
    Pm = "Pm"
1✔
974
    Sm = "Sm"
1✔
975
    Eu = "Eu"
1✔
976
    Gd = "Gd"
1✔
977
    Tb = "Tb"
1✔
978
    Dy = "Dy"
1✔
979
    Ho = "Ho"
1✔
980
    Er = "Er"
1✔
981
    Tm = "Tm"
1✔
982
    Yb = "Yb"
1✔
983
    Lu = "Lu"
1✔
984
    Hf = "Hf"
1✔
985
    Ta = "Ta"
1✔
986
    W = "W"
1✔
987
    Re = "Re"
1✔
988
    Os = "Os"
1✔
989
    Ir = "Ir"
1✔
990
    Pt = "Pt"
1✔
991
    Au = "Au"
1✔
992
    Hg = "Hg"
1✔
993
    Tl = "Tl"
1✔
994
    Pb = "Pb"
1✔
995
    Bi = "Bi"
1✔
996
    Po = "Po"
1✔
997
    At = "At"
1✔
998
    Rn = "Rn"
1✔
999
    Fr = "Fr"
1✔
1000
    Ra = "Ra"
1✔
1001
    Ac = "Ac"
1✔
1002
    Th = "Th"
1✔
1003
    Pa = "Pa"
1✔
1004
    U = "U"
1✔
1005
    Np = "Np"
1✔
1006
    Pu = "Pu"
1✔
1007
    Am = "Am"
1✔
1008
    Cm = "Cm"
1✔
1009
    Bk = "Bk"
1✔
1010
    Cf = "Cf"
1✔
1011
    Es = "Es"
1✔
1012
    Fm = "Fm"
1✔
1013
    Md = "Md"
1✔
1014
    No = "No"
1✔
1015
    Lr = "Lr"
1✔
1016
    Rf = "Rf"
1✔
1017
    Db = "Db"
1✔
1018
    Sg = "Sg"
1✔
1019
    Bh = "Bh"
1✔
1020
    Hs = "Hs"
1✔
1021
    Mt = "Mt"
1✔
1022
    Ds = "Ds"
1✔
1023
    Rg = "Rg"
1✔
1024
    Cn = "Cn"
1✔
1025
    Nh = "Nh"
1✔
1026
    Fl = "Fl"
1✔
1027
    Mc = "Mc"
1✔
1028
    Lv = "Lv"
1✔
1029
    Ts = "Ts"
1✔
1030
    Og = "Og"
1✔
1031

1032

1033
@functools.total_ordering
1✔
1034
class Species(MSONable, Stringify):
1✔
1035
    """
1036
    An extension of Element with an oxidation state and other optional
1037
    properties. Properties associated with Species should be "idealized"
1038
    values, not calculated values. For example, high-spin Fe2+ may be
1039
    assigned an idealized spin of +5, but an actual Fe2+ site may be
1040
    calculated to have a magmom of +4.5. Calculated properties should be
1041
    assigned to Site objects, and not Species.
1042
    """
1043

1044
    STRING_MODE = "SUPERSCRIPT"
1✔
1045
    supported_properties = ("spin",)
1✔
1046

1047
    def __init__(
1✔
1048
        self,
1049
        symbol: str,
1050
        oxidation_state: float | None = 0.0,
1051
        properties: dict | None = None,
1052
    ):
1053
        """
1054
        Initializes a Species.
1055

1056
        Args:
1057
            symbol (str): Element symbol, e.g., Fe
1058
            oxidation_state (float): Oxidation state of element, e.g., 2 or -2
1059
            properties: Properties associated with the Species, e.g.,
1060
                {"spin": 5}. Defaults to None. Properties must be one of the
1061
                Species supported_properties.
1062

1063
        .. attribute:: oxi_state
1064

1065
            Oxidation state associated with Species
1066

1067
        .. attribute:: ionic_radius
1068

1069
            Ionic radius of Species (with specific oxidation state).
1070

1071
        .. versionchanged:: 2.6.7
1072

1073
            Properties are now checked when comparing two Species for equality.
1074
        """
1075
        self._el = Element(symbol)
1✔
1076
        self._oxi_state = oxidation_state
1✔
1077
        self._properties = properties or {}
1✔
1078
        for k, _ in self._properties.items():
1✔
1079
            if k not in Species.supported_properties:
1✔
1080
                raise ValueError(f"{k} is not a supported property")
1✔
1081

1082
    def __getattr__(self, a):
1✔
1083
        # overriding getattr doesn't play nice with pickle, so we
1084
        # can't use self._properties
1085
        p = object.__getattribute__(self, "_properties")
1✔
1086
        if a in p:
1✔
1087
            return p[a]
1✔
1088
        return getattr(self._el, a)
1✔
1089

1090
    def __eq__(self, other: object) -> bool:
1✔
1091
        """
1092
        Species is equal to other only if element and oxidation states are exactly the same.
1093
        """
1094
        if not hasattr(other, "oxi_state") or not hasattr(other, "symbol") or not hasattr(other, "_properties"):
1✔
1095
            return NotImplemented
1✔
1096

1097
        return all(getattr(self, attr) == getattr(other, attr) for attr in ["symbol", "oxi_state", "_properties"])
1✔
1098

1099
    def __hash__(self):
1✔
1100
        """
1101
        Equal Species should have the same str representation, hence
1102
        should hash equally. Unequal Species will have different str
1103
        representations.
1104
        """
1105
        return hash(str(self))
1✔
1106

1107
    def __lt__(self, other: object) -> bool:
1✔
1108
        """
1109
        Sets a default sort order for atomic species by electronegativity,
1110
        followed by oxidation state, followed by spin.
1111
        """
1112
        if not isinstance(other, type(self)):
1✔
1113
            return NotImplemented
1✔
1114

1115
        x1 = float("inf") if self.X != self.X else self.X
1✔
1116
        x2 = float("inf") if other.X != other.X else other.X
1✔
1117
        if x1 != x2:
1✔
1118
            return x1 < x2
1✔
1119
        if self.symbol != other.symbol:
1✔
1120
            # There are cases where the electronegativity are exactly equal.
1121
            # We then sort by symbol.
1122
            return self.symbol < other.symbol
1✔
1123
        if self.oxi_state:
1✔
1124
            other_oxi = 0 if (isinstance(other, Element) or other.oxi_state is None) else other.oxi_state
1✔
1125
            return self.oxi_state < other_oxi
1✔
1126
        if getattr(self, "spin", False):
×
1127
            other_spin = getattr(other, "spin", 0)
×
1128
            return self.spin < other_spin
×
1129
        return False
×
1130

1131
    @property
1✔
1132
    def element(self):
1✔
1133
        """
1134
        Underlying element object
1135
        """
1136
        return self._el
1✔
1137

1138
    @property
1✔
1139
    def ionic_radius(self) -> float | None:
1✔
1140
        """
1141
        Ionic radius of specie. Returns None if data is not present.
1142
        """
1143
        if self._oxi_state in self.ionic_radii:
1✔
1144
            return self.ionic_radii[self._oxi_state]
1✔
1145
        if self._oxi_state:
×
1146
            d = self._el.data
×
1147
            oxstr = str(int(self._oxi_state))
×
1148
            if oxstr in d.get("Ionic radii hs", {}):
×
1149
                warnings.warn(f"No default ionic radius for {self}. Using hs data.")
×
1150
                return d["Ionic radii hs"][oxstr]
×
1151
            if oxstr in d.get("Ionic radii ls", {}):
×
1152
                warnings.warn(f"No default ionic radius for {self}. Using ls data.")
×
1153
                return d["Ionic radii ls"][oxstr]
×
1154
        warnings.warn(f"No ionic radius for {self}!")
×
1155
        return None
×
1156

1157
    @property
1✔
1158
    def oxi_state(self) -> float | None:
1✔
1159
        """
1160
        Oxidation state of Species.
1161
        """
1162
        return self._oxi_state
1✔
1163

1164
    @staticmethod
1✔
1165
    def from_string(species_string: str) -> Species:
1✔
1166
        """
1167
        Returns a Species from a string representation.
1168

1169
        Args:
1170
            species_string (str): A typical string representation of a
1171
                species, e.g., "Mn2+", "Fe3+", "O2-".
1172

1173
        Returns:
1174
            A Species object.
1175

1176
        Raises:
1177
            ValueError if species_string cannot be interpreted.
1178
        """
1179
        # e.g. Fe2+,spin=5
1180
        # 1st group: ([A-Z][a-z]*)    --> Fe
1181
        # 2nd group: ([0-9.]*)        --> "2"
1182
        # 3rd group: ([+\-])          --> +
1183
        # 4th group: (.*)             --> everything else, ",spin=5"
1184

1185
        m = re.search(r"([A-Z][a-z]*)([0-9.]*)([+\-]*)(.*)", species_string)
1✔
1186
        if m:
1✔
1187
            # parse symbol
1188
            sym = m.group(1)
1✔
1189

1190
            # parse oxidation state (optional)
1191
            if not m.group(2) and not m.group(3):
1✔
1192
                oxi = None
1✔
1193
            else:
1194
                oxi = 1 if m.group(2) == "" else float(m.group(2))
1✔
1195
                oxi = -oxi if m.group(3) == "-" else oxi
1✔
1196

1197
            # parse properties (optional)
1198
            properties = None
1✔
1199
            if m.group(4):
1✔
1200
                toks = m.group(4).replace(",", "").split("=")
1✔
1201
                properties = {toks[0]: ast.literal_eval(toks[1])}
1✔
1202

1203
            # but we need either an oxidation state or a property
1204
            if oxi is None and properties is None:
1✔
1205
                raise ValueError("Invalid Species String")
1✔
1206

1207
            return Species(sym, 0 if oxi is None else oxi, properties)
1✔
1208
        raise ValueError("Invalid Species String")
1✔
1209

1210
    def __repr__(self):
1✔
1211
        return f"Species {self}"
1✔
1212

1213
    def __str__(self):
1✔
1214
        output = self.symbol
1✔
1215
        if self.oxi_state is not None:
1✔
1216
            if self.oxi_state >= 0:
1✔
1217
                output += formula_double_format(self.oxi_state) + "+"
1✔
1218
            else:
1219
                output += formula_double_format(-self.oxi_state) + "-"
1✔
1220
        for p, v in self._properties.items():
1✔
1221
            output += f",{p}={v}"
1✔
1222
        return output
1✔
1223

1224
    def to_pretty_string(self) -> str:
1✔
1225
        """
1226
        :return: String without properties.
1227
        """
1228
        output = self.symbol
1✔
1229
        if self.oxi_state is not None:
1✔
1230
            if self.oxi_state >= 0:
1✔
1231
                output += formula_double_format(self.oxi_state) + "+"
1✔
1232
            else:
1233
                output += formula_double_format(-self.oxi_state) + "-"
1✔
1234
        return output
1✔
1235

1236
    def get_nmr_quadrupole_moment(self, isotope: str | None = None) -> float:
1✔
1237
        """
1238
        Gets the nuclear electric quadrupole moment in units of
1239
        e*millibarns
1240

1241
        Args:
1242
            isotope (str): the isotope to get the quadrupole moment for
1243
                default is None, which gets the lowest mass isotope
1244
        """
1245
        quad_mom = self._el.nmr_quadrupole_moment
1✔
1246

1247
        if not quad_mom:
1✔
1248
            return 0.0
1✔
1249

1250
        if isotope is None:
1✔
1251
            isotopes = list(quad_mom)
1✔
1252
            isotopes.sort(key=lambda x: int(x.split("-")[1]), reverse=False)
1✔
1253
            return quad_mom.get(isotopes[0], 0.0)
1✔
1254

1255
        if isotope not in quad_mom:
1✔
1256
            raise ValueError(f"No quadrupole moment for isotope {isotope}")
1✔
1257
        return quad_mom.get(isotope, 0.0)
1✔
1258

1259
    def get_shannon_radius(
1✔
1260
        self,
1261
        cn: str,
1262
        spin: Literal["", "Low Spin", "High Spin"] = "",
1263
        radius_type: Literal["ionic", "crystal"] = "ionic",
1264
    ) -> float:
1265
        """
1266
        Get the local environment specific ionic radius for species.
1267

1268
        Args:
1269
            cn (str): Coordination using roman letters. Supported values are
1270
                I-IX, as well as IIIPY, IVPY and IVSQ.
1271
            spin (str): Some species have different radii for different
1272
                spins. You can get specific values using "High Spin" or
1273
                "Low Spin". Leave it as "" if not available. If only one spin
1274
                data is available, it is returned and this spin parameter is
1275
                ignored.
1276
            radius_type (str): Either "crystal" or "ionic" (default).
1277

1278
        Returns:
1279
            Shannon radius for specie in the specified environment.
1280
        """
1281
        radii = self._el.data["Shannon radii"]
1✔
1282
        radii = radii[str(int(self._oxi_state))][cn]  # type: ignore
1✔
1283
        if len(radii) == 1:
1✔
1284
            k, data = list(radii.items())[0]
1✔
1285
            if k != spin:
1✔
1286
                warnings.warn(
1✔
1287
                    f"Specified spin state of {spin} not consistent with database "
1288
                    f"spin of {k}. Only one spin data available, and that value is returned."
1289
                )
1290
        else:
1291
            data = radii[spin]
1✔
1292
        return data[f"{radius_type}_radius"]
1✔
1293

1294
    def get_crystal_field_spin(
1✔
1295
        self, coordination: Literal["oct", "tet"] = "oct", spin_config: Literal["low", "high"] = "high"
1296
    ) -> float:
1297
        """
1298
        Calculate the crystal field spin based on coordination and spin
1299
        configuration. Only works for transition metal species.
1300

1301
        Args:
1302
            coordination ("oct" | "tet"): Tetrahedron or octahedron crystal site coordination
1303
            spin_config ("low" | "high"): Whether the species is in a high or low spin state
1304

1305
        Returns:
1306
            Crystal field spin in Bohr magneton.
1307

1308
        Raises:
1309
            AttributeError if species is not a valid transition metal or has
1310
                an invalid oxidation state.
1311
            ValueError if invalid coordination or spin_config.
1312
        """
1313
        if coordination not in ("oct", "tet") or spin_config not in ("high", "low"):
1✔
1314
            raise ValueError("Invalid coordination or spin config.")
1✔
1315
        elec = self.full_electronic_structure
1✔
1316
        if len(elec) < 4 or elec[-1][1] != "s" or elec[-2][1] != "d":
1✔
1317
            raise AttributeError(f"Invalid element {self.symbol} for crystal field calculation.")
1✔
1318
        nelectrons = elec[-1][2] + elec[-2][2] - self.oxi_state
1✔
1319
        if nelectrons < 0 or nelectrons > 10:
1✔
1320
            raise AttributeError(f"Invalid oxidation state {self.oxi_state} for element {self.symbol}")
1✔
1321
        if spin_config == "high":
1✔
1322
            if nelectrons <= 5:
1✔
1323
                return nelectrons
1✔
1324
            return 10 - nelectrons
1✔
1325
        if spin_config == "low":
1✔
1326
            if coordination == "oct":
1✔
1327
                if nelectrons <= 3:
1✔
1328
                    return nelectrons
×
1329
                if nelectrons <= 6:
1✔
1330
                    return 6 - nelectrons
1✔
1331
                if nelectrons <= 8:
1✔
1332
                    return nelectrons - 6
1✔
1333
                return 10 - nelectrons
×
1334
            if coordination == "tet":
1✔
1335
                if nelectrons <= 2:
1✔
1336
                    return nelectrons
×
1337
                if nelectrons <= 4:
1✔
1338
                    return 4 - nelectrons
×
1339
                if nelectrons <= 7:
1✔
1340
                    return nelectrons - 4
1✔
1341
                return 10 - nelectrons
×
1342
        raise RuntimeError()
×
1343

1344
    def __deepcopy__(self, memo):
1✔
1345
        return Species(self.symbol, self.oxi_state, self._properties)
1✔
1346

1347
    def as_dict(self) -> dict:
1✔
1348
        """
1349
        :return: Json-able dictionary representation.
1350
        """
1351
        d = {
1✔
1352
            "@module": type(self).__module__,
1353
            "@class": type(self).__name__,
1354
            "element": self.symbol,
1355
            "oxidation_state": self._oxi_state,
1356
        }
1357
        if self._properties:
1✔
1358
            d["properties"] = self._properties
1✔
1359
        return d
1✔
1360

1361
    @classmethod
1✔
1362
    def from_dict(cls, d) -> Species:
1✔
1363
        """
1364
        :param d: Dict representation.
1365
        :return: Species.
1366
        """
1367
        return cls(d["element"], d["oxidation_state"], d.get("properties", None))
1✔
1368

1369

1370
@functools.total_ordering
1✔
1371
class DummySpecies(Species):
1✔
1372
    """
1373
    A special specie for representing non-traditional elements or species. For
1374
    example, representation of vacancies (charged or otherwise), or special
1375
    sites, etc.
1376

1377
    .. attribute:: oxi_state
1378

1379
        Oxidation state associated with Species.
1380

1381
    .. attribute:: Z
1382

1383
        DummySpecies is always assigned an atomic number equal to the hash
1384
        number of the symbol. Obviously, it makes no sense whatsoever to use
1385
        the atomic number of a Dummy specie for anything scientific. The purpose
1386
        of this is to ensure that for most use cases, a DummySpecies behaves no
1387
        differently from an Element or Species.
1388

1389
    .. attribute:: X
1390

1391
        DummySpecies is always assigned an electronegativity of 0.
1392
    """
1393

1394
    def __init__(
1✔
1395
        self,
1396
        symbol: str = "X",
1397
        oxidation_state: float | None = 0,
1398
        properties: dict | None = None,
1399
    ):
1400
        """
1401
        Args:
1402
            symbol (str): An assigned symbol for the dummy specie. Strict
1403
                rules are applied to the choice of the symbol. The dummy
1404
                symbol cannot have any part of first two letters that will
1405
                constitute an Element symbol. Otherwise, a composition may
1406
                be parsed wrongly. E.g., "X" is fine, but "Vac" is not
1407
                because Vac contains V, a valid Element.
1408
            oxidation_state (float): Oxidation state for dummy specie.
1409
                Defaults to zero.
1410
        """
1411
        # enforce title case to match other elements, reduces confusion
1412
        # when multiple DummySpecies in a "formula" string
1413
        symbol = symbol.title()
1✔
1414

1415
        for i in range(1, min(2, len(symbol)) + 1):
1✔
1416
            if Element.is_valid_symbol(symbol[:i]):
1✔
1417
                raise ValueError(f"{symbol} contains {symbol[:i]}, which is a valid element symbol.")
1✔
1418

1419
        # Set required attributes for DummySpecies to function like a Species in
1420
        # most instances.
1421
        self._symbol = symbol
1✔
1422
        self._oxi_state = oxidation_state
1✔
1423
        self._properties = properties or {}
1✔
1424
        for k, _ in self._properties.items():
1✔
1425
            if k not in Species.supported_properties:
1✔
1426
                raise ValueError(f"{k} is not a supported property")
×
1427

1428
    def __getattr__(self, a):
1✔
1429
        # overriding getattr doesn't play nice with pickle, so we
1430
        # can't use self._properties
1431
        p = object.__getattribute__(self, "_properties")
1✔
1432
        if a in p:
1✔
1433
            return p[a]
1✔
1434
        raise AttributeError(a)
1✔
1435

1436
    def __lt__(self, other):
1✔
1437
        """
1438
        Sets a default sort order for atomic species by electronegativity,
1439
        followed by oxidation state.
1440
        """
1441
        if self.X != other.X:
1✔
1442
            return self.X < other.X
1✔
1443
        if self.symbol != other.symbol:
1✔
1444
            # There are cases where the electronegativity are exactly equal.
1445
            # We then sort by symbol.
1446
            return self.symbol < other.symbol
1✔
1447
        other_oxi = 0 if isinstance(other, Element) else other.oxi_state
1✔
1448
        return self.oxi_state < other_oxi
1✔
1449

1450
    @property
1✔
1451
    def Z(self) -> int:
1✔
1452
        """
1453
        DummySpecies is always assigned an atomic number equal to the hash of
1454
        the symbol. The expectation is that someone would be an actual dummy
1455
        to use atomic numbers for a Dummy specie.
1456
        """
1457
        return hash(self.symbol)
×
1458

1459
    @property
1✔
1460
    def oxi_state(self) -> float | None:
1✔
1461
        """
1462
        Oxidation state associated with DummySpecies
1463
        """
1464
        return self._oxi_state
1✔
1465

1466
    @property
1✔
1467
    def X(self) -> float:
1✔
1468
        """
1469
        DummySpecies is always assigned an electronegativity of 0. The effect of
1470
        this is that DummySpecies are always sorted in front of actual Species.
1471
        """
1472
        return 0.0
1✔
1473

1474
    @property
1✔
1475
    def symbol(self) -> str:
1✔
1476
        """
1477
        :return: Symbol for DummySpecies.
1478
        """
1479
        return self._symbol
1✔
1480

1481
    def __deepcopy__(self, memo):
1✔
1482
        return DummySpecies(self.symbol, self._oxi_state)
1✔
1483

1484
    @staticmethod
1✔
1485
    def from_string(species_string: str) -> DummySpecies:
1✔
1486
        """
1487
        Returns a Dummy from a string representation.
1488

1489
        Args:
1490
            species_string (str): A string representation of a dummy
1491
                species, e.g., "X2+", "X3+".
1492

1493
        Returns:
1494
            A DummySpecies object.
1495

1496
        Raises:
1497
            ValueError if species_string cannot be interpreted.
1498
        """
1499
        m = re.search(r"([A-ZAa-z]*)([0-9.]*)([+\-]*)(.*)", species_string)
1✔
1500
        if m:
1✔
1501
            sym = m.group(1)
1✔
1502
            if m.group(2) == "" and m.group(3) == "":
1✔
1503
                oxi = 0.0
1✔
1504
            else:
1505
                oxi = 1.0 if m.group(2) == "" else float(m.group(2))
1✔
1506
                oxi = -oxi if m.group(3) == "-" else oxi
1✔
1507
            properties = None
1✔
1508
            if m.group(4):
1✔
1509
                toks = m.group(4).split("=")
1✔
1510
                properties = {toks[0]: float(toks[1])}
1✔
1511
            return DummySpecies(sym, oxi, properties)
1✔
1512
        raise ValueError("Invalid DummySpecies String")
×
1513

1514
    def as_dict(self) -> dict:
1✔
1515
        """
1516
        :return: MSONAble dict representation.
1517
        """
1518
        d = {
1✔
1519
            "@module": type(self).__module__,
1520
            "@class": type(self).__name__,
1521
            "element": self.symbol,
1522
            "oxidation_state": self._oxi_state,
1523
        }
1524
        if self._properties:
1✔
1525
            d["properties"] = self._properties  # type: ignore
×
1526
        return d
1✔
1527

1528
    @classmethod
1✔
1529
    def from_dict(cls, d) -> DummySpecies:
1✔
1530
        """
1531
        :param d: Dict representation
1532
        :return: DummySpecies
1533
        """
1534
        return cls(d["element"], d["oxidation_state"], d.get("properties", None))
1✔
1535

1536
    def __repr__(self):
1✔
1537
        return f"DummySpecies {self}"
×
1538

1539
    def __str__(self):
1✔
1540
        output = self.symbol
1✔
1541
        if self.oxi_state is not None:
1✔
1542
            if self.oxi_state >= 0:
1✔
1543
                output += formula_double_format(self.oxi_state) + "+"
1✔
1544
            else:
1545
                output += formula_double_format(-self.oxi_state) + "-"
1✔
1546
        for p, v in self._properties.items():
1✔
1547
            output += f",{p}={v}"
×
1548
        return output
1✔
1549

1550

1551
@functools.total_ordering
1✔
1552
class Specie(Species):
1✔
1553
    """
1554
    This maps the historical grammatically inaccurate Specie to Species
1555
    to maintain backwards compatibility.
1556
    """
1557

1558

1559
@functools.total_ordering
1✔
1560
class DummySpecie(DummySpecies):
1✔
1561
    """
1562
    This maps the historical grammatically inaccurate DummySpecie to DummySpecies
1563
    to maintain backwards compatibility.
1564
    """
1565

1566

1567
def get_el_sp(obj) -> Element | Species | DummySpecies:
1✔
1568
    """
1569
    Utility method to get an Element or Species from an input obj.
1570
    If obj is in itself an element or a specie, it is returned automatically.
1571
    If obj is an int or a string representing an integer, the Element
1572
    with the atomic number obj is returned.
1573
    If obj is a string, Species parsing will be attempted (e.g., Mn2+), failing
1574
    which Element parsing will be attempted (e.g., Mn), failing which
1575
    DummyElement parsing will be attempted.
1576

1577
    Args:
1578
        obj (Element/Species/str/int): An arbitrary object. Supported objects
1579
            are actual Element/Species objects, integers (representing atomic
1580
            numbers) or strings (element symbols or species strings).
1581

1582
    Returns:
1583
        Species or Element, with a bias for the maximum number of properties
1584
        that can be determined.
1585

1586
    Raises:
1587
        ValueError if obj cannot be converted into an Element or Species.
1588
    """
1589
    if isinstance(obj, (Element, Species, DummySpecies)):
1✔
1590
        return obj
1✔
1591

1592
    try:
1✔
1593
        c = float(obj)
1✔
1594
        i = int(c)
1✔
1595
        i = i if i == c else None  # type: ignore
1✔
1596
    except (ValueError, TypeError):
1✔
1597
        i = None
1✔
1598

1599
    if i is not None:
1✔
1600
        return Element.from_Z(i)
1✔
1601

1602
    try:
1✔
1603
        return Species.from_string(obj)
1✔
1604
    except (ValueError, KeyError):
1✔
1605
        try:
1✔
1606
            return Element(obj)
1✔
1607
        except (ValueError, KeyError):
1✔
1608
            try:
1✔
1609
                return DummySpecies.from_string(obj)
1✔
1610
            except Exception:
1✔
1611
                raise ValueError(f"Can't parse Element or String from type {type(obj)}: {obj}.")
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc