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 [">", "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("Ω", "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