Source code for sisl.atom

# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
from __future__ import annotations

from collections import Counter
from collections.abc import Iterable
from numbers import Integral, Real

import numpy as np

from sisl._typing_ext.numpy import NDArray

from . import _array as _a
from ._dispatch_class import _Dispatchs
from ._dispatcher import AbstractDispatch, ClassDispatcher
from ._help import array_fill_repeat
from ._indices import list_index_le
from ._internal import set_module
from .messages import deprecation, info
from .orbital import Orbital
from .shape import Sphere

__all__ = ["PeriodicTable", "Atom", "AtomUnknown", "AtomGhost", "Atoms"]


@set_module("sisl")
class PeriodicTable:
    r"""Periodic table for creating an `Atom`, or retrieval of atomic information via atomic numbers

    Enables *lookup* of atomic numbers/names/labels to get
    the atomic number.

    Several quantities available to the atomic species are available
    from <https://en.wikipedia.org/wiki/Atomic_radii_of_the_elements_(data_page)>.

    The following values are accessible:

    * atomic mass (in atomic units)
    * empirical atomic radii (in Ang)
    * calculated atomic radii (in Ang)
    * van der Waals atomic radii (in Ang)

    For certain species the above quantities are not available
    and a negative number is returned.

    Examples
    --------
    >>> 79 == PeriodicTable().Z('Au')
    True
    >>> 79 == PeriodicTable().Z_int('Au')
    True
    >>> 'Au' == PeriodicTable().Z_short(79)
    True
    >>> 'Au' == PeriodicTable().Z_label(79)
    True
    >>> 'Au' == PeriodicTable().Z_label('Gold')
    True
    >>> 12.0107 == PeriodicTable().atomic_mass('C')
    True
    >>> 12.0107 == PeriodicTable().atomic_mass(6)
    True
    >>> 12.0107 == PeriodicTable().atomic_mass('Carbon')
    True
    >>> .67 == PeriodicTable().radii('Carbon')
    True
    >>> .67 == PeriodicTable().radii(6,'calc')
    True
    >>> .7  == PeriodicTable().radii(6,'empirical')
    True
    >>> 1.7 == PeriodicTable().radii(6,'vdw')
    True
    """

    # fmt: off
    _Z_int = {
        'Actinium': 89, 'Ac': 89, '89': 89, 89: 89,
        'Aluminum': 13, 'Al': 13, '13': 13, 13: 13,
        'Americium': 95, 'Am': 95, '95': 95, 95: 95,
        'Antimony': 51, 'Sb': 51, '51': 51, 51: 51,
        'Argon': 18, 'Ar': 18, '18': 18, 18: 18,
        'Arsenic': 33, 'As': 33, '33': 33, 33: 33,
        'Astatine': 85, 'At': 85, '85': 85, 85: 85,
        'Barium': 56, 'Ba': 56, '56': 56, 56: 56,
        'Berkelium': 97, 'Bk': 97, '97': 97, 97: 97,
        'Beryllium': 4, 'Be': 4, '4': 4, 4: 4,
        'Bismuth': 83, 'Bi': 83, '83': 83, 83: 83,
        'Bohrium': 107, 'Bh': 107, '107': 107, 107: 107,
        'Boron': 5, 'B': 5, '5': 5, 5: 5,
        'Bromine': 35, 'Br': 35, '35': 35, 35: 35,
        'Cadmium': 48, 'Cd': 48, '48': 48, 48: 48,
        'Calcium': 20, 'Ca': 20, '20': 20, 20: 20,
        'Californium': 98, 'Cf': 98, '98': 98, 98: 98,
        'Carbon': 6, 'C': 6, '6': 6, 6: 6,
        'Cerium': 58, 'Ce': 58, '58': 58, 58: 58,
        'Cesium': 55, 'Cs': 55, '55': 55, 55: 55,
        'Chlorine': 17, 'Cl': 17, '17': 17, 17: 17,
        'Chromium': 24, 'Cr': 24, '24': 24, 24: 24,
        'Cobalt': 27, 'Co': 27, '27': 27, 27: 27,
        'Copper': 29, 'Cu': 29, '29': 29, 29: 29,
        'Curium': 96, 'Cm': 96, '96': 96, 96: 96,
        'Darmstadtium': 110, 'Ds': 110, '110': 110, 110: 110,
        'Dubnium': 105, 'Db': 105, '105': 105, 105: 105,
        'Dysprosium': 66, 'Dy': 66, '66': 66, 66: 66,
        'Einsteinium': 99, 'Es': 99, '99': 99, 99: 99,
        'Erbium': 68, 'Er': 68, '68': 68, 68: 68,
        'Europium': 63, 'Eu': 63, '63': 63, 63: 63,
        'Fermium': 100, 'Fm': 100, '100': 100, 100: 100,
        'Fluorine': 9, 'F': 9, '9': 9, 9: 9,
        'Francium': 87, 'Fr': 87, '87': 87, 87: 87,
        'Gadolinium': 64, 'Gd': 64, '64': 64, 64: 64,
        'Gallium': 31, 'Ga': 31, '31': 31, 31: 31,
        'Germanium': 32, 'Ge': 32, '32': 32, 32: 32,
        'Gold': 79, 'Au': 79, '79': 79, 79: 79,
        'Hafnium': 72, 'Hf': 72, '72': 72, 72: 72,
        'Hassium': 108, 'Hs': 108, '108': 108, 108: 108,
        'Helium': 2, 'He': 2, '2': 2, 2: 2,
        'Holmium': 67, 'Ho': 67, '67': 67, 67: 67,
        'Hydrogen': 1, 'H': 1, '1': 1, 1: 1,
        'Indium': 49, 'In': 49, '49': 49, 49: 49,
        'Iodine': 53, 'I': 53, '53': 53, 53: 53,
        'Iridium': 77, 'Ir': 77, '77': 77, 77: 77,
        'Iron': 26, 'Fe': 26, '26': 26, 26: 26,
        'Krypton': 36, 'Kr': 36, '36': 36, 36: 36,
        'Lanthanum': 57, 'La': 57, '57': 57, 57: 57,
        'Lawrencium': 103, 'Lr': 103, '103': 103, 103: 103,
        'Lead': 82, 'Pb': 82, '82': 82, 82: 82,
        'Lithium': 3, 'Li': 3, '3': 3, 3: 3,
        'Lutetium': 71, 'Lu': 71, '71': 71, 71: 71,
        'Magnesium': 12, 'Mg': 12, '12': 12, 12: 12,
        'Manganese': 25, 'Mn': 25, '25': 25, 25: 25,
        'Meitnerium': 109, 'Mt': 109, '109': 109, 109: 109,
        'Mendelevium': 101, 'Md': 101, '101': 101, 101: 101,
        'Mercury': 80, 'Hg': 80, '80': 80, 80: 80,
        'Molybdenum': 42, 'Mo': 42, '42': 42, 42: 42,
        'Neodymium': 60, 'Nd': 60, '60': 60, 60: 60,
        'Neon': 10, 'Ne': 10, '10': 10, 10: 10,
        'Neptunium': 93, 'Np': 93, '93': 93, 93: 93,
        'Nickel': 28, 'Ni': 28, '28': 28, 28: 28,
        'Niobium': 41, 'Nb': 41, '41': 41, 41: 41,
        'Nitrogen': 7, 'N': 7, '7': 7, 7: 7,
        'Nobelium': 102, 'No': 102, '102': 102, 102: 102,
        'Osmium': 76, 'Os': 76, '76': 76, 76: 76,
        'Oxygen': 8, 'O': 8, '8': 8, 8: 8,
        'Palladium': 46, 'Pd': 46, '46': 46, 46: 46,
        'Phosphorus': 15, 'P': 15, '15': 15, 15: 15,
        'Platinum': 78, 'Pt': 78, '78': 78, 78: 78,
        'Plutonium': 94, 'Pu': 94, '94': 94, 94: 94,
        'Polonium': 84, 'Po': 84, '84': 84, 84: 84,
        'Potassium': 19, 'K': 19, '19': 19, 19: 19,
        'Praseodymium': 59, 'Pr': 59, '59': 59, 59: 59,
        'Promethium': 61, 'Pm': 61, '61': 61, 61: 61,
        'Protactinium': 91, 'Pa': 91, '91': 91, 91: 91,
        'Radium': 88, 'Ra': 88, '88': 88, 88: 88,
        'Radon': 86, 'Rn': 86, '86': 86, 86: 86,
        'Rhenium': 75, 'Re': 75, '75': 75, 75: 75,
        'Rhodium': 45, 'Rh': 45, '45': 45, 45: 45,
        'Rubidium': 37, 'Rb': 37, '37': 37, 37: 37,
        'Ruthenium': 44, 'Ru': 44, '44': 44, 44: 44,
        'Rutherfordium': 104, 'Rf': 104, '104': 104, 104: 104,
        'Samarium': 62, 'Sm': 62, '62': 62, 62: 62,
        'Scandium': 21, 'Sc': 21, '21': 21, 21: 21,
        'Seaborgium': 106, 'Sg': 106, '106': 106, 106: 106,
        'Selenium': 34, 'Se': 34, '34': 34, 34: 34,
        'Silicon': 14, 'Si': 14, '14': 14, 14: 14,
        'Silver': 47, 'Ag': 47, '47': 47, 47: 47,
        'Sodium': 11, 'Na': 11, '11': 11, 11: 11,
        'Strontium': 38, 'Sr': 38, '38': 38, 38: 38,
        'Sulfur': 16, 'S': 16, '16': 16, 16: 16,
        'Tantalum': 73, 'Ta': 73, '73': 73, 73: 73,
        'Technetium': 43, 'Tc': 43, '43': 43, 43: 43,
        'Tellurium': 52, 'Te': 52, '52': 52, 52: 52,
        'Terbium': 65, 'Tb': 65, '65': 65, 65: 65,
        'Thallium': 81, 'Tl': 81, '81': 81, 81: 81,
        'Thorium': 90, 'Th': 90, '90': 90, 90: 90,
        'Thulium': 69, 'Tm': 69, '69': 69, 69: 69,
        'Tin': 50, 'Sn': 50, '50': 50, 50: 50,
        'Titanium': 22, 'Ti': 22, '22': 22, 22: 22,
        'Tungsten': 74, 'W': 74, '74': 74, 74: 74,
        'Ununbium': 112, 'Uub': 112, '112': 112, 112: 112,
        'Ununhexium': 116, 'Uuh': 116, '116': 116, 116: 116,
        'Ununoctium': 118, 'Uuo': 118, '118': 118, 118: 118,
        'Ununpentium': 115, 'Uup': 115, '115': 115, 115: 115,
        'Ununquadium': 114, 'Uuq': 114, '114': 114, 114: 114,
        'Ununseptium': 117, 'Uus': 117, '117': 117, 117: 117,
        'Ununtrium': 113, 'Uut': 113, '113': 113, 113: 113,
        'Ununium': 111, 'Uuu': 111, '111': 111, 111: 111,
        'Uranium': 92, 'U': 92, '92': 92, 92: 92,
        'Vanadium': 23, 'V': 23, '23': 23, 23: 23,
        'Xenon': 54, 'Xe': 54, '54': 54, 54: 54,
        'Ytterbium': 70, 'Yb': 70, '70': 70, 70: 70,
        'Yttrium': 39, 'Y': 39, '39': 39, 39: 39,
        'Zinc': 30, 'Zn': 30, '30': 30, 30: 30,
        'Zirconium': 40, 'Zr': 40, '40': 40, 40: 40,
    }

    _Z_short = {
        'Actinium': 'Ac', 'Ac': 'Ac', '89': 'Ac', 89: 'Ac',
        'Aluminum': 'Al', 'Al': 'Al', '13': 'Al', 13: 'Al',
        'Americium': 'Am', 'Am': 'Am', '95': 'Am', 95: 'Am',
        'Antimony': 'Sb', 'Sb': 'Sb', '51': 'Sb', 51: 'Sb',
        'Argon': 'Ar', 'Ar': 'Ar', '18': 'Ar', 18: 'Ar',
        'Arsenic': 'As', 'As': 'As', '33': 'As', 33: 'As',
        'Astatine': 'At', 'At': 'At', '85': 'At', 85: 'At',
        'Barium': 'Ba', 'Ba': 'Ba', '56': 'Ba', 56: 'Ba',
        'Berkelium': 'Bk', 'Bk': 'Bk', '97': 'Bk', 97: 'Bk',
        'Beryllium': 'Be', 'Be': 'Be', '4': 'Be', 4: 'Be',
        'Bismuth': 'Bi', 'Bi': 'Bi', '83': 'Bi', 83: 'Bi',
        'Bohrium': 'Bh', 'Bh': 'Bh', '107': 'Bh', 107: 'Bh',
        'Boron': 'B', 'B': 'B', '5': 'B', 5: 'B',
        'Bromine': 'Br', 'Br': 'Br', '35': 'Br', 35: 'Br',
        'Cadmium': 'Cd', 'Cd': 'Cd', '48': 'Cd', 48: 'Cd',
        'Calcium': 'Ca', 'Ca': 'Ca', '20': 'Ca', 20: 'Ca',
        'Californium': 'Cf', 'Cf': 'Cf', '98': 'Cf', 98: 'Cf',
        'Carbon': 'C', 'C': 'C', '6': 'C', 6: 'C',
        'Cerium': 'Ce', 'Ce': 'Ce', '58': 'Ce', 58: 'Ce',
        'Cesium': 'Cs', 'Cs': 'Cs', '55': 'Cs', 55: 'Cs',
        'Chlorine': 'Cl', 'Cl': 'Cl', '17': 'Cl', 17: 'Cl',
        'Chromium': 'Cr', 'Cr': 'Cr', '24': 'Cr', 24: 'Cr',
        'Cobalt': 'Co', 'Co': 'Co', '27': 'Co', 27: 'Co',
        'Copper': 'Cu', 'Cu': 'Cu', '29': 'Cu', 29: 'Cu',
        'Curium': 'Cm', 'Cm': 'Cm', '96': 'Cm', 96: 'Cm',
        'Darmstadtium': 'Ds', 'Ds': 'Ds', '110': 'Ds', 110: 'Ds',
        'Dubnium': 'Db', 'Db': 'Db', '105': 'Db', 105: 'Db',
        'Dysprosium': 'Dy', 'Dy': 'Dy', '66': 'Dy', 66: 'Dy',
        'Einsteinium': 'Es', 'Es': 'Es', '99': 'Es', 99: 'Es',
        'Erbium': 'Er', 'Er': 'Er', '68': 'Er', 68: 'Er',
        'Europium': 'Eu', 'Eu': 'Eu', '63': 'Eu', 63: 'Eu',
        'Fermium': 'Fm', 'Fm': 'Fm', '100': 'Fm', 100: 'Fm',
        'Fluorine': 'F', 'F': 'F', '9': 'F', 9: 'F',
        'Francium': 'Fr', 'Fr': 'Fr', '87': 'Fr', 87: 'Fr',
        'Gadolinium': 'Gd', 'Gd': 'Gd', '64': 'Gd', 64: 'Gd',
        'Gallium': 'Ga', 'Ga': 'Ga', '31': 'Ga', 31: 'Ga',
        'Germanium': 'Ge', 'Ge': 'Ge', '32': 'Ge', 32: 'Ge',
        'Gold': 'Au', 'Au': 'Au', '79': 'Au', 79: 'Au',
        'Hafnium': 'Hf', 'Hf': 'Hf', '72': 'Hf', 72: 'Hf',
        'Hassium': 'Hs', 'Hs': 'Hs', '108': 'Hs', 108: 'Hs',
        'Helium': 'He', 'He': 'He', '2': 'He', 2: 'He',
        'Holmium': 'Ho', 'Ho': 'Ho', '67': 'Ho', 67: 'Ho',
        'Hydrogen': 'H', 'H': 'H', '1': 'H', 1: 'H',
        'Indium': 'In', 'In': 'In', '49': 'In', 49: 'In',
        'Iodine': 'I', 'I': 'I', '53': 'I', 53: 'I',
        'Iridium': 'Ir', 'Ir': 'Ir', '77': 'Ir', 77: 'Ir',
        'Iron': 'Fe', 'Fe': 'Fe', '26': 'Fe', 26: 'Fe',
        'Krypton': 'Kr', 'Kr': 'Kr', '36': 'Kr', 36: 'Kr',
        'Lanthanum': 'La', 'La': 'La', '57': 'La', 57: 'La',
        'Lawrencium': 'Lr', 'Lr': 'Lr', '103': 'Lr', 103: 'Lr',
        'Lead': 'Pb', 'Pb': 'Pb', '82': 'Pb', 82: 'Pb',
        'Lithium': 'Li', 'Li': 'Li', '3': 'Li', 3: 'Li',
        'Lutetium': 'Lu', 'Lu': 'Lu', '71': 'Lu', 71: 'Lu',
        'Magnesium': 'Mg', 'Mg': 'Mg', '12': 'Mg', 12: 'Mg',
        'Manganese': 'Mn', 'Mn': 'Mn', '25': 'Mn', 25: 'Mn',
        'Meitnerium': 'Mt', 'Mt': 'Mt', '109': 'Mt', 109: 'Mt',
        'Mendelevium': 'Md', 'Md': 'Md', '101': 'Md', 101: 'Md',
        'Mercury': 'Hg', 'Hg': 'Hg', '80': 'Hg', 80: 'Hg',
        'Molybdenum': 'Mo', 'Mo': 'Mo', '42': 'Mo', 42: 'Mo',
        'Neodymium': 'Nd', 'Nd': 'Nd', '60': 'Nd', 60: 'Nd',
        'Neon': 'Ne', 'Ne': 'Ne', '10': 'Ne', 10: 'Ne',
        'Neptunium': 'Np', 'Np': 'Np', '93': 'Np', 93: 'Np',
        'Nickel': 'Ni', 'Ni': 'Ni', '28': 'Ni', 28: 'Ni',
        'Niobium': 'Nb', 'Nb': 'Nb', '41': 'Nb', 41: 'Nb',
        'Nitrogen': 'N', 'N': 'N', '7': 'N', 7: 'N',
        'Nobelium': 'No', 'No': 'No', '102': 'No', 102: 'No',
        'Osmium': 'Os', 'Os': 'Os', '76': 'Os', 76: 'Os',
        'Oxygen': 'O', 'O': 'O', '8': 'O', 8: 'O',
        'Palladium': 'Pd', 'Pd': 'Pd', '46': 'Pd', 46: 'Pd',
        'Phosphorus': 'P', 'P': 'P', '15': 'P', 15: 'P',
        'Platinum': 'Pt', 'Pt': 'Pt', '78': 'Pt', 78: 'Pt',
        'Plutonium': 'Pu', 'Pu': 'Pu', '94': 'Pu', 94: 'Pu',
        'Polonium': 'Po', 'Po': 'Po', '84': 'Po', 84: 'Po',
        'Potassium': 'K', 'K': 'K', '19': 'K', 19: 'K',
        'Praseodymium': 'Pr', 'Pr': 'Pr', '59': 'Pr', 59: 'Pr',
        'Promethium': 'Pm', 'Pm': 'Pm', '61': 'Pm', 61: 'Pm',
        'Protactinium': 'Pa', 'Pa': 'Pa', '91': 'Pa', 91: 'Pa',
        'Radium': 'Ra', 'Ra': 'Ra', '88': 'Ra', 88: 'Ra',
        'Radon': 'Rn', 'Rn': 'Rn', '86': 'Rn', 86: 'Rn',
        'Rhenium': 'Re', 'Re': 'Re', '75': 'Re', 75: 'Re',
        'Rhodium': 'Rh', 'Rh': 'Rh', '45': 'Rh', 45: 'Rh',
        'Rubidium': 'Rb', 'Rb': 'Rb', '37': 'Rb', 37: 'Rb',
        'Ruthenium': 'Ru', 'Ru': 'Ru', '44': 'Ru', 44: 'Ru',
        'Rutherfordium': 'Rf', 'Rf': 'Rf', '104': 'Rf', 104: 'Rf',
        'Samarium': 'Sm', 'Sm': 'Sm', '62': 'Sm', 62: 'Sm',
        'Scandium': 'Sc', 'Sc': 'Sc', '21': 'Sc', 21: 'Sc',
        'Seaborgium': 'Sg', 'Sg': 'Sg', '106': 'Sg', 106: 'Sg',
        'Selenium': 'Se', 'Se': 'Se', '34': 'Se', 34: 'Se',
        'Silicon': 'Si', 'Si': 'Si', '14': 'Si', 14: 'Si',
        'Silver': 'Ag', 'Ag': 'Ag', '47': 'Ag', 47: 'Ag',
        'Sodium': 'Na', 'Na': 'Na', '11': 'Na', 11: 'Na',
        'Strontium': 'Sr', 'Sr': 'Sr', '38': 'Sr', 38: 'Sr',
        'Sulfur': 'S', 'S': 'S', '16': 'S', 16: 'S',
        'Tantalum': 'Ta', 'Ta': 'Ta', '73': 'Ta', 73: 'Ta',
        'Technetium': 'Tc', 'Tc': 'Tc', '43': 'Tc', 43: 'Tc',
        'Tellurium': 'Te', 'Te': 'Te', '52': 'Te', 52: 'Te',
        'Terbium': 'Tb', 'Tb': 'Tb', '65': 'Tb', 65: 'Tb',
        'Thallium': 'Tl', 'Tl': 'Tl', '81': 'Tl', 81: 'Tl',
        'Thorium': 'Th', 'Th': 'Th', '90': 'Th', 90: 'Th',
        'Thulium': 'Tm', 'Tm': 'Tm', '69': 'Tm', 69: 'Tm',
        'Tin': 'Sn', 'Sn': 'Sn', '50': 'Sn', 50: 'Sn',
        'Titanium': 'Ti', 'Ti': 'Ti', '22': 'Ti', 22: 'Ti',
        'Tungsten': 'W', 'W': 'W', '74': 'W', 74: 'W',
        'Ununbium': 'Uub', 'Uub': 'Uub', '112': 'Uub', 112: 'Uub',
        'Ununhexium': 'Uuh', 'Uuh': 'Uuh', '116': 'Uuh', 116: 'Uuh',
        'Ununoctium': 'Uuo', 'Uuo': 'Uuo', '118': 'Uuo', 118: 'Uuo',
        'Ununpentium': 'Uup', 'Uup': 'Uup', '115': 'Uup', 115: 'Uup',
        'Ununquadium': 'Uuq', 'Uuq': 'Uuq', '114': 'Uuq', 114: 'Uuq',
        'Ununseptium': 'Uus', 'Uus': 'Uus', '117': 'Uus', 117: 'Uus',
        'Ununtrium': 'Uut', 'Uut': 'Uut', '113': 'Uut', 113: 'Uut',
        'Ununium': 'Uuu', 'Uuu': 'Uuu', '111': 'Uuu', 111: 'Uuu',
        'Uranium': 'U', 'U': 'U', '92': 'U', 92: 'U',
        'Vanadium': 'V', 'V': 'V', '23': 'V', 23: 'V',
        'Xenon': 'Xe', 'Xe': 'Xe', '54': 'Xe', 54: 'Xe',
        'Ytterbium': 'Yb', 'Yb': 'Yb', '70': 'Yb', 70: 'Yb',
        'Yttrium': 'Y', 'Y': 'Y', '39': 'Y', 39: 'Y',
        'Zinc': 'Zn', 'Zn': 'Zn', '30': 'Zn', 30: 'Zn',
        'Zirconium': 'Zr', 'Zr': 'Zr', '40': 'Zr', 40: 'Zr',
    }

    _atomic_mass = {
        1: 1.00794,
        2: 4.002602,
        3: 6.941,
        4: 9.012182,
        5: 10.811,
        6: 12.0107,
        7: 14.0067,
        8: 15.9994,
        9: 18.9984032,
        10: 20.1797,
        11: 22.98976928,
        12: 24.3050,
        13: 26.9815386,
        14: 28.0855,
        15: 30.973762,
        16: 32.065,
        17: 35.453,
        18: 39.948,
        19: 39.0983,
        20: 40.078,
        21: 44.955912,
        22: 47.867,
        23: 50.9415,
        24: 51.9961,
        25: 54.938045,
        26: 55.845,
        27: 58.933195,
        28: 58.6934,
        29: 63.546,
        30: 65.409,
        31: 69.723,
        32: 72.64,
        33: 74.92160,
        34: 78.96,
        35: 79.904,
        36: 83.798,
        37: 85.4678,
        38: 87.62,
        39: 88.90585,
        40: 91.224,
        41: 92.906,
        42: 95.94,
        43: 98.,
        44: 101.07,
        45: 102.905,
        46: 106.42,
        47: 107.8682,
        48: 112.411,
        49: 114.818,
        50: 118.710,
        51: 121.760,
        52: 127.60,
        53: 126.904,
        54: 131.293,
        55: 132.9054519,
        56: 137.327,
        57: 138.90547,
        58: 140.116,
        59: 140.90765,
        60: 144.242,
        61: 145.,
        62: 150.36,
        63: 151.964,
        64: 157.25,
        65: 158.92535,
        66: 162.500,
        67: 164.930,
        68: 167.259,
        69: 168.93421,
        70: 173.04,
        71: 174.967,
        72: 178.49,
        73: 180.94788,
        74: 183.84,
        75: 186.207,
        76: 190.23,
        77: 192.217,
        78: 195.084,
        79: 196.966569,
        80: 200.59,
        81: 204.3833,
        82: 207.2,
        83: 208.98040,
        84: 210.,
        85: 210.,
        86: 220.,
        87: 223.,
        88: 226.,
        89: 227.,
        91: 231.03588,
        90: 232.03806,
        93: 237.,
        92: 238.02891,
        95: 243.,
        94: 244.,
        96: 247.,
        97: 247.,
        98: 251.,
        99: 252.,
        100: 257.,
        101: 258.,
        102: 259.,
        103: 262.,
        104: 261.,
        105: 262.,
        106: 266.,
        107: 264.,
        108: 277.,
        109: 268.,
        110: 271.,
        111: 272.,
        112: 285.,
        113: 284.,
        114: 289.,
        115: 288.,
        116: 292.,
        118: 293.,
    }

    _radius_empirical = {
        1: 25,
        2: -1,
        3: 145,
        4: 105,
        5: 85,
        6: 70,
        7: 65,
        8: 60,
        9: 50,
        10: -1,
        11: 180,
        12: 150,
        13: 125,
        14: 110,
        15: 100,
        16: 100,
        17: 100,
        18: 71,
        19: 220,
        20: 180,
        21: 160,
        22: 140,
        23: 135,
        24: 140,
        25: 140,
        26: 140,
        27: 135,
        28: 135,
        29: 135,
        30: 135,
        31: 130,
        32: 125,
        33: 115,
        34: 115,
        35: 115,
        36: -1,
        37: 235,
        38: 200,
        39: 180,
        40: 155,
        41: 145,
        42: 145,
        43: 135,
        44: 130,
        45: 135,
        46: 140,
        47: 160,
        48: 155,
        49: 155,
        50: 145,
        51: 145,
        52: 140,
        53: 140,
        54: -1,
        55: 260,
        56: 215,
        57: 195,
        58: 185,
        59: 185,
        60: 185,
        61: 185,
        62: 185,
        63: 185,
        64: 180,
        65: 175,
        66: 175,
        67: 175,
        68: 175,
        69: 175,
        70: 175,
        71: 175,
        72: 155,
        73: 145,
        74: 135,
        75: 135,
        76: 130,
        77: 135,
        78: 135,
        79: 135,
        80: 150,
        81: 190,
        82: 180,
        83: 160,
        84: 190,
        85: -1,
        86: -1,
        87: -1,
        88: 215,
        89: 195,
        90: 180,
        91: 180,
        92: 175,
        93: 175,
        94: 175,
        95: 175,
        96: -1,
        97: -1,
        98: -1,
        99: -1,
        100: -1,
        101: -1,
        102: -1,
        103: -1,
        104: -1,
        105: -1,
        106: -1,
        107: -1,
        108: -1,
        109: -1,
        110: -1,
        111: -1,
        112: -1,
        113: -1,
        114: -1,
        115: -1,
        116: -1,
        117: -1,
        118: -1,
    }

    _radius_calc = {
        1: 53,
        2: 31,
        3: 167,
        4: 112,
        5: 87,
        6: 67,
        7: 56,
        8: 48,
        9: 42,
        10: 38,
        11: 190,
        12: 145,
        13: 118,
        14: 111,
        15: 98,
        16: 88,
        17: 79,
        18: 71,
        19: 243,
        20: 194,
        21: 184,
        22: 176,
        23: 171,
        24: 166,
        25: 161,
        26: 156,
        27: 152,
        28: 149,
        29: 145,
        30: 142,
        31: 136,
        32: 125,
        33: 114,
        34: 103,
        35: 94,
        36: 88,
        37: 265,
        38: 219,
        39: 212,
        40: 206,
        41: 198,
        42: 190,
        43: 183,
        44: 178,
        45: 173,
        46: 169,
        47: 165,
        48: 161,
        49: 156,
        50: 145,
        51: 133,
        52: 123,
        53: 115,
        54: 108,
        55: 298,
        56: 253,
        57: -1,
        58: -1,
        59: 247,
        60: 206,
        61: 205,
        62: 238,
        63: 231,
        64: 233,
        65: 225,
        66: 228,
        67: -1,
        68: 226,
        69: 222,
        70: 222,
        71: 217,
        72: 208,
        73: 200,
        74: 193,
        75: 188,
        76: 185,
        77: 180,
        78: 177,
        79: 174,
        80: 171,
        81: 156,
        82: 154,
        83: 143,
        84: 135,
        85: -1,
        86: 120,
        87: -1,
        88: -1,
        89: -1,
        90: -1,
        91: -1,
        92: -1,
        93: -1,
        94: -1,
        95: -1,
        96: -1,
        97: -1,
        98: -1,
        99: -1,
        100: -1,
        101: -1,
        102: -1,
        103: -1,
        104: -1,
        105: -1,
        106: -1,
        107: -1,
        108: -1,
        109: -1,
        110: -1,
        111: -1,
        112: -1,
        113: -1,
        114: -1,
        115: -1,
        116: -1,
        117: -1,
        118: -1,
    }

    _radius_vdw = {
        1: 120,
        2: 140,
        3: 182,
        4: 153,
        5: 192,
        6: 170,
        7: 155,
        8: 152,
        9: 147,
        10: 154,
        11: 227,
        12: 173,
        13: 184,
        14: 210,
        15: 180,
        16: 180,
        17: 175,
        18: 188,
        19: 275,
        20: 231,
        21: 211,
        22: -1,
        23: -1,
        24: -1,
        25: -1,
        26: -1,
        27: -1,
        28: 163,
        29: 140,
        30: 139,
        31: 187,
        32: 211,
        33: 185,
        34: 190,
        35: 185,
        36: 202,
        37: 303,
        38: 249,
        39: -1,
        40: -1,
        41: -1,
        42: -1,
        43: -1,
        44: -1,
        45: -1,
        46: 163,
        47: 172,
        48: 158,
        49: 193,
        50: 217,
        51: 206,
        52: 206,
        53: 198,
        54: 216,
        55: 343,
        56: 268,
        57: -1,
        58: -1,
        59: -1,
        60: -1,
        61: -1,
        62: -1,
        63: -1,
        64: -1,
        65: -1,
        66: -1,
        67: -1,
        68: -1,
        69: -1,
        70: -1,
        71: -1,
        72: -1,
        73: -1,
        74: -1,
        75: -1,
        76: -1,
        77: -1,
        78: 175,
        79: 166,
        80: 155,
        81: 196,
        82: 202,
        83: 207,
        84: 197,
        85: 202,
        86: 220,
        87: 348,
        88: 283,
        89: -1,
        90: -1,
        91: -1,
        92: 186,
        93: -1,
        94: -1,
        95: -1,
        96: -1,
        97: -1,
        98: -1,
        99: -1,
        100: -1,
        101: -1,
        102: -1,
        103: -1,
        104: -1,
        105: -1,
        106: -1,
        107: -1,
        108: -1,
        109: -1,
        110: -1,
        111: -1,
        112: -1,
        113: -1,
        114: -1,
        115: -1,
        116: -1,
        117: -1,
        118: -1,
    }
    # fmt: on

[docs] def Z(self, key): """Atomic number based on general input Return the atomic number corresponding to the `key` lookup. Parameters ---------- key : array_like or str or int Uses value to lookup the atomic number in the `PeriodicTable` object. Returns ------- numpy.ndarray or int atomic number corresponding to `key`, if `key` is array_like, so will the returned value be. Examples -------- >>> 79 == PeriodicTable().Z_int('Au') True >>> 79 == PeriodicTable().Z('Au') True >>> 6 == PeriodicTable().Z('Carbon') True """ key = np.asarray(key).ravel() get = self._Z_int.get if len(key) == 1: return get(key[0], key[0]) return _a.fromiteri(map(get, key, key))
Z_int = Z
[docs] def Z_label(self, key): """Atomic label of the corresponding atom Return the atomic short name corresponding to the `key` lookup. Parameters ---------- key : array_like or str or int Uses value to lookup the atomic short name in the `PeriodicTable` object. Returns ------- numpy.ndarray or str atomic short name corresponding to `key`, if `key` is array_like, so will the returned value be. """ ak = np.asarray(key).ravel() get = self._Z_short.get if len(ak) == 1: return get(ak[0], "fa") return [get(ia, "fa") for ia in ak]
Z_short = Z_label
[docs] def atomic_mass(self, key): """Atomic mass of the corresponding atom Return the atomic mass corresponding to the `key` lookup. Parameters ---------- key : array_like or str or int Uses value to lookup the atomic mass in the `PeriodicTable` object. Returns ------- numpy.ndarray or float atomic mass in atomic units corresponding to `key`, if `key` is array_like, so will the returned value be. """ Z = self.Z_int(key) get = self._atomic_mass.get if isinstance(Z, (Integral, Real)): return get(Z, 0.0) return _a.fromiterd(map(get, Z, iter(float, 1)))
[docs] def radius(self, key, method="calc"): """Atomic radii using different methods Return the atomic radii. Parameters ---------- key : array_like or str or int Uses value to lookup the atomic mass in the `PeriodicTable` object. method : {'calc', 'empirical', 'vdw'} There are 3 different radii stored: 1. ``calc``, the calculated atomic radii 2. ``empirical``, the empirically found values 3. ``vdw``, the van-der-Waals found values Returns ------- numpy.ndarray or float atomic radius in `Ang` """ Z = self.Z_int(key) func = getattr(self, f"_radius_{method}").get if isinstance(Z, Integral): return func(Z) / 100 return _a.fromiterd(map(func, Z)) / 100
radii = radius # Create a local instance of the periodic table to # faster look up _ptbl = PeriodicTable() class AtomMeta(type): """Meta class for key-lookup on the class.""" def __getitem__(cls, key): """Create a new atom object""" if isinstance(key, Atom): # if the key already is an atomic object # return it return key elif isinstance(key, dict): # The key is a dictionary, hence # we can return the atom directly return cls(**key) elif isinstance(key, (list, tuple)): # The key is a list, # we need to create a list of atoms return [cls[k] for k in key] # pylint: disable=E1136 # Index Z based return cls(key) # Note the with_metaclass which is required for python3 support. # The designation of metaclass in python3 is actually: # class ...(..., metaclass=MetaClass) # This below construct handles both python2 and python3 cases @set_module("sisl") class Atom( _Dispatchs, dispatchs=[("to", ClassDispatcher("to", type_dispatcher=None))], when_subclassing="keep", metaclass=AtomMeta, ): """Atomic information, mass, name number of orbitals and ranges Object to handle atomic mass, name, number of orbitals and orbital range. The `Atom` object handles the atomic species with information such as * atomic number * mass * number of orbitals * radius of each orbital The `Atom` object is `pickle`-able. Parameters ---------- Z : int or str key lookup for the atomic specie, `Atom[key]` orbitals : list of Orbital or float, optional all orbitals associated with this atom. Default to one orbital. mass : float, optional the atomic mass, if not specified uses the mass from `PeriodicTable` tag : str, optional arbitrary designation for user handling similar atoms with different settings (defaults to the label of the atom) """ def __new__(cls, *args, **kwargs): """Figure out which class to actually use""" # Handle the case where no arguments are passed (e.g. for serializing stuff) if len(args) == 0 and "Z" not in kwargs: return super().__new__(cls) # direct call if len(args) > 0: Z = args[0] else: Z = None Z = kwargs.get("Z", Z) if isinstance(Z, Atom): return super().__new__(Z.__class__) elif isinstance(Z, Integral) and not issubclass(cls, AtomGhost) and Z < 0: cls = AtomGhost elif Z not in _ptbl._Z_int and not issubclass(cls, AtomUnknown): cls = AtomUnknown return super().__new__(cls)
[docs] def __init__(self, Z, orbitals=None, mass=None, tag=None, **kwargs): if isinstance(Z, Atom): self._Z = Z.Z elif isinstance(Z, Integral): self._Z = Z else: self._Z = _ptbl.Z_int(Z) self._orbitals = None if isinstance(orbitals, (tuple, list, np.ndarray)): if len(orbitals) == 0: # This may be the same as only regarding `R` argument pass elif isinstance(orbitals[0], Orbital): # all is good self._orbitals = orbitals elif isinstance(orbitals[0], Real): # radius has been given self._orbitals = [Orbital(R) for R in orbitals] elif isinstance(orbitals[0], str): # radius has been given self._orbitals = [Orbital(-1, tag=tag) for tag in orbitals] elif all(orb is None for orb in orbitals): orbitals = None elif isinstance(orbitals, Orbital): self._orbitals = [orbitals] elif isinstance(orbitals, Real): self._orbitals = [Orbital(orbitals)] if self._orbitals is None: if orbitals is not None: raise ValueError( f"{self.__class__.__name__}.__init__ got unparseable 'orbitals' argument: {orbitals}" ) if "R" in kwargs: # backwards compatibility (possibly remove this in the future) R = _a.asarrayd(kwargs["R"]).ravel() self._orbitals = [Orbital(r) for r in R] else: self._orbitals = [Orbital(-1.0)] if mass is None: self._mass = _ptbl.atomic_mass(self.Z) else: self._mass = mass if tag is None: self._tag = self.symbol else: self._tag = tag
def __hash__(self): return hash((self._tag, self._mass, self._Z, *self._orbitals)) @property def Z(self) -> NDArray[np.int32]: """Atomic number""" return self._Z @property def orbitals(self): """List of orbitals""" return self._orbitals @property def mass(self) -> NDArray[np.float64]: """Atomic mass""" return self._mass @property def tag(self) -> str: """Tag for atom""" return self._tag @property def no(self) -> int: """Number of orbitals on this atom""" return len(self.orbitals)
[docs] def index(self, orbital): """Return the index of the orbital in the atom object""" if not isinstance(orbital, Orbital): orbital = self[orbital] for i, o in enumerate(self.orbitals): if o == orbital: return i raise KeyError("Could not find `orbital` in the list of orbitals.")
[docs] def sub(self, orbitals): """Return the same atom with only a subset of the orbitals present Parameters ---------- orbitals : array_like indices of the orbitals to retain Returns ------- Atom with only the subset of orbitals Raises ------ ValueError if the number of orbitals removed is too large or some indices are outside the allowed range """ orbitals = _a.arrayi(orbitals).ravel() if len(orbitals) > self.no: raise ValueError( f"{self.__class__.__name__}.sub tries to remove more than the number of orbitals on an atom." ) if np.any(orbitals >= self.no): raise ValueError( f"{self.__class__.__name__}.sub tries to remove a non-existing orbital io > no." ) orbs = [self.orbitals[o].copy() for o in orbitals] return self.copy(orbitals=orbs)
[docs] def remove(self, orbitals): """Return the same atom without a specific set of orbitals Parameters ---------- orbitals : array_like indices of the orbitals to remove Returns ------- Atom without the specified orbitals See Also -------- sub : retain a selected set of orbitals """ orbs = np.delete(_a.arangei(self.no), orbitals) return self.sub(orbs)
[docs] def copy(self, Z=None, orbitals=None, mass=None, tag=None): """Return copy of this object""" if orbitals is None: orbitals = [orb.copy() for orb in self] return self.__class__( self.Z if Z is None else Z, orbitals, self.mass if mass is None else mass, self.tag if tag is None else tag, )
[docs] def radius(self, method="calc"): """Return the atomic radii of the atom (in Ang) See `PeriodicTable.radius` for details on the argument. """ return _ptbl.radius(self.Z, method)
@property def symbol(self): """Return short atomic name (Au==79).""" return _ptbl.Z_short(self.Z) def __getitem__(self, key): """The orbital corresponding to index `key`""" if isinstance(key, slice): ol = key.indices(len(self)) return [self.orbitals[o] for o in range(*ol)] elif isinstance(key, Integral): return self.orbitals[key] elif isinstance(key, str): orbs = [orb for orb in self.orbitals if key in orb.name()] # In case none are found, None will be returned if not orbs: return None return orbs if len(orbs) != 1 else orbs[0] return [self.orbitals[o] for o in key]
[docs] def maxR(self): """Return the maximum range of orbitals.""" mR = -1e10 for o in self.orbitals: mR = max(mR, o.R) return mR
[docs] def scale(self, scale): """Scale the atomic radii and return an equivalent atom. Parameters ---------- scale : float the scale factor for the atomic radii """ new = self.copy() new._orbitals = [o.scale(scale) for o in self.orbitals] return new
def __iter__(self): """Loop on all orbitals in this atom""" yield from self.orbitals
[docs] def iter(self, group=False): """Loop on all orbitals in this atom Parameters ---------- group : bool, optional if two orbitals share the same radius one may be able to group two orbitals together Returns ------- Orbital current orbital, if `group` is ``True`` this is a list of orbitals, otherwise a single orbital is returned """ if group: i = 0 no = self.no - 1 while i <= no: # Figure out how many share the same radial part j = i + 1 while j <= no: if np.allclose(self.orbitals[i].R, self.orbitals[j].R): j += 1 else: break yield self.orbitals[i:j] i = j return yield from self.orbitals
def __str__(self): # Create orbitals output orbs = ",\n ".join([str(o) for o in self.orbitals]) return ( self.__class__.__name__ + "{{{0}, Z: {1:d}, mass(au): {2:.5f}, maxR: {3:.5f},\n {4}\n}}".format( self.tag, self.Z, self.mass, self.maxR(), orbs ) ) def __repr__(self): return f"<{self.__module__}.{self.__class__.__name__} {self.tag}, Z={self.Z}, M={self.mass}, maxR={self.maxR()}, no={len(self.orbitals)}>" def __len__(self): """Return number of orbitals in this atom""" return self.no def __getattr__(self, attr): """Pass attribute calls to the orbital classes and return lists/array Parameters ---------- attr : str """ # First we create a list of values that the orbitals have # Some may have it, others may not vals = [None] * len(self.orbitals) found = False is_Integral = is_Real = is_callable = True for io, orb in enumerate(self.orbitals): try: vals[io] = getattr(orb, attr) found = True is_callable &= callable(vals[io]) is_Integral &= isinstance(vals[io], Integral) is_Real &= isinstance(vals[io], Real) except AttributeError: pass if found == 0: # we never got any values, reraise the AttributeError raise AttributeError( f"'{self.__class__.__name__}.orbitals' objects has no attribute '{attr}'" ) # Now parse the data, currently we'll only allow Integral, Real, Complex if is_Integral: for io in range(len(vals)): if vals[io] is None: vals[io] = 0 return _a.arrayi(vals) elif is_Real: for io in range(len(vals)): if vals[io] is None: vals[io] = 0.0 return _a.arrayd(vals) elif is_callable: def _ret_none(*args, **kwargs): return None for io in range(len(vals)): if vals[io] is None: vals[io] = _ret_none # Now subclass the content and return values per method class ArrayCall: def __init__(self, methods): self.methods = methods def __call__(self, *args, **kwargs): return [m(*args, **kwargs) for m in self.methods] return ArrayCall(vals) # We don't know how to handle this, simply return... return vals
[docs] @deprecation( "toSphere is deprecated, please use shape.to.Sphere(...) instead.", "0.15" ) def toSphere(self, center=None): """Return a sphere with the maximum orbital radius equal Returns ------- ~sisl.shape.Sphere a sphere with radius equal to the maximum radius of the orbitals """ return self.to.Sphere(center=center)
[docs] def equal(self, other, R=True, psi=False): """True if `other` is the same as this atomic specie Parameters ---------- other : Atom the other object to check againts R : bool, optional if True the equality check also checks the orbital radii, else they are not compared psi : bool, optional if True, also check the wave-function component of the orbitals, see `Orbital.psi` """ if not isinstance(other, Atom): return False same = self.Z == other.Z same &= self.no == other.no if same and R: same &= all( [ self.orbitals[i].equal(other.orbitals[i], psi=psi) for i in range(self.no) ] ) same &= np.isclose(self.mass, other.mass) same &= self.tag == other.tag return same
# Check whether they are equal def __eq__(self, b): """Return true if the saved quantities are the same""" return self.equal(b) def __ne__(self, b): return not (self == b) # Create pickling routines def __getstate__(self): """Return the state of this object""" return { "Z": self.Z, "orbitals": self.orbitals, "mass": self.mass, "tag": self.tag, } def __setstate__(self, d): """Re-create the state of this object""" self.__init__(d["Z"], d["orbitals"], d["mass"], d["tag"]) @set_module("sisl") class AtomUnknown(Atom): def __init__(self, Z, *args, **kwargs): """Instantiate with overridden tag""" if len(args) < 3 and "tag" not in kwargs: kwargs["tag"] = "unknown" super().__init__(Z, *args, **kwargs) @set_module("sisl") class AtomGhost(AtomUnknown): def __init__(self, Z, *args, **kwargs): """Instantiate with overridden tag and taking the absolute value of Z""" if Z < 0: Z = -Z if len(args) < 3 and "tag" not in kwargs: kwargs["tag"] = "ghost" if len(args) < 2 and "mass" not in kwargs: # A very high default mass (unless specified) kwargs["mass"] = 1e40 super().__init__(Z, *args, **kwargs) class AtomToDispatch(AbstractDispatch): """Base dispatcher from class passing from an Atom class""" to_dispatch = Atom.to class ToSphereDispatch(AtomToDispatch): def dispatch(self, *args, center=None, **kwargs): return Sphere(self._get_object().maxR(), center) to_dispatch.register("Sphere", ToSphereDispatch) @set_module("sisl") class Atoms: """A list-like object to contain a list of different atoms with minimum data duplication. This holds multiple `Atom` objects which are indexed via a species index. This is convenient when having geometries with millions of atoms because it will not duplicate the `Atom` object, only a list index. Parameters ---------- atoms : str, Atom or list-like atoms to be contained in this list of atoms If a str, or a single `Atom` it will be the only atom in the resulting class repeated `na` times. If a list, it will create all unique atoms and retain these, each item in the list may a single argument passed to the `Atom` or a dictionary that is passed to `Atom`, see examples. na : int or None total number of atoms, if ``len(atom)`` is smaller than `na` it will be repeated to match `na`. Examples -------- Creating an atoms object consisting of 5 atoms, all the same. >>> atoms = Atoms("H", na=5) Creating a set of 4 atoms, 2 Hydrogen, 2 Helium, in an alternate ordere >>> Z = [1, 2, 1, 2] >>> atoms = Atoms(Z) Creating individual atoms using dictionary entries, two Hydrogen atoms, one with a tag H_ghost. >>> Atoms([dict(Z=1, tag="H_ghost"), 1]) """ # Using the slots should make this class slightly faster. __slots__ = ("_atom", "_specie", "_firsto")
[docs] def __init__(self, atoms="H", na=None): # Default value of the atom object if atoms is None: atoms = Atom("H") # Correct the atoms input to Atom if isinstance(atoms, Atom): uatoms = [atoms] specie = [0] elif isinstance(atoms, Atoms): # Ensure we make a copy to not operate # on the same data. catoms = atoms.copy() uatoms = catoms.atom[:] specie = catoms.specie[:] elif isinstance(atoms, (str, Integral)): uatoms = [Atom(atoms)] specie = [0] elif isinstance(atoms, dict): uatoms = [Atom(**atoms)] specie = [0] elif isinstance(atoms, Iterable): # TODO this is very inefficient for large MD files uatoms = [] specie = [] for a in atoms: if isinstance(a, dict): a = Atom(**a) elif not isinstance(a, Atom): a = Atom(a) try: s = uatoms.index(a) except Exception: s = len(uatoms) uatoms.append(a) specie.append(s) else: raise TypeError(f"atoms keyword type is not acceptable {type(atoms)}") # Default for number of atoms if na is None: na = len(specie) # Create atom and species objects self._atom = list(uatoms) self._specie = array_fill_repeat(specie, na, cls=np.int16) self._update_orbitals()
def _update_orbitals(self): """Internal routine for updating the `firsto` attribute""" # Get number of orbitals per specie uorbs = _a.arrayi([a.no for a in self.atom]) self._firsto = np.insert(_a.cumsumi(uorbs[self.specie]), 0, 0)
[docs] def copy(self): """Return a copy of this atom""" atoms = Atoms() atoms._atom = [a.copy() for a in self._atom] atoms._specie = np.copy(self._specie) atoms._update_orbitals() return atoms
@property def atom(self): """List of unique atoms in this group of atoms""" return self._atom @property def nspecie(self): """Number of different species""" return len(self._atom) @property def specie(self): """List of atomic species""" return self._specie @property def no(self): """Total number of orbitals in this list of atoms""" uorbs = _a.arrayi([a.no for a in self.atom]) return uorbs[self.specie].sum() @property def orbitals(self): """Array of orbitals of the contained objects""" return np.diff(self.firsto) @property def firsto(self): """First orbital of the corresponding atom in the consecutive list of orbitals""" return self._firsto @property def lasto(self): """Last orbital of the corresponding atom in the consecutive list of orbitals""" return self._firsto[1:] - 1 @property def q0(self): """Initial charge per atom""" q0 = _a.arrayd([a.q0.sum() for a in self.atom]) return q0[self.specie]
[docs] def orbital(self, io): """Return an array of orbital of the contained objects""" io = _a.asarrayi(io) ndim = io.ndim io = io.ravel() % self.no a = list_index_le(io, self.lasto) io = io - self.firsto[a] a = self.specie[a] # Now extract the list of orbitals if ndim == 0: return self.atom[a[0]].orbitals[io[0]] return [self.atom[ia].orbitals[o] for ia, o in zip(a, io)]
[docs] def maxR(self, all=False): """The maximum radius of the atoms Parameters ---------- all : bool determine the returned maximum radii. If `True` is passed an array of all atoms maximum radii is returned (array). Else, if `False` the maximum of all atoms maximum radii is returned (scalar). """ if all: maxR = _a.arrayd([a.maxR() for a in self.atom]) return maxR[self.specie[:]] return np.amax([a.maxR() for a in self.atom])
@property def mass(self): """Array of masses of the contained objects""" umass = _a.arrayd([a.mass for a in self.atom]) return umass[self.specie[:]] @property def Z(self): """Array of atomic numbers""" uZ = _a.arrayi([a.Z for a in self.atom]) return uZ[self.specie[:]]
[docs] def scale(self, scale): """Scale the atomic radii and return an equivalent atom. Parameters ---------- scale : float the scale factor for the atomic radii """ atoms = Atoms() atoms._atom = [a.scale(scale) for a in self.atom] atoms._specie = np.copy(self._specie) return atoms
[docs] def index(self, atom): """Return the indices of the atom object""" return (self._specie == self.specie_index(atom)).nonzero()[0]
[docs] def specie_index(self, atom): """Return the species index of the atom object""" if not isinstance(atom, Atom): atom = self[atom] for s, a in enumerate(self.atom): if a == atom: return s raise KeyError("Could not find `atom` in the list of atoms.")
[docs] def group_atom_data(self, data, axis=0): r"""Group data for each atom based on number of orbitals This is useful for grouping data that is orbitally resolved. This will return a list of length ``len(self)`` and with each item having the sub-slice of the data corresponding to the orbitals on the given atom. Examples -------- >>> atoms = Atoms([Atom(4, [0.1, 0.2]), Atom(6, [0.2, 0.3, 0.5])]) >>> orb_data = np.arange(10).reshape(2, 5) >>> atoms.group_data(orb_data, axis=1) [ [[0, 1], [2, 3]], [[4, 5, 6], [7, 8, 9]] ] Parameters ---------- data : numpy.ndarray data to be grouped axis : int, optional along which axis one should split the data """ return np.split(data, self.lasto[:-1] + 1, axis=axis)
[docs] def reorder(self, in_place=False): """Reorders the atoms and species index so that they are ascending (starting with a specie that exists) Parameters ---------- in_place : bool, optional whether the re-order is done *in-place* """ # Contains the minimum atomic index for a given specie smin = _a.emptyi(len(self.atom)) smin.fill(len(self)) for a in range(len(self.atom)): lst = (self.specie == a).nonzero()[0] if len(lst) > 0: smin[a] = lst.min() # Now swap indices into correct place # This will give the indices of the species # in the ascending order isort = np.argsort(smin) if np.allclose(np.diff(isort), 0): # No swaps required return self.copy() # We need to swap something if in_place: atoms = self else: atoms = self.copy() atoms._atom[:] = [atoms._atom[i] for i in isort] atoms._specie[:] = isort[atoms._specie] atoms._update_orbitals() return atoms
[docs] def formula(self, system="Hill"): """Return the chemical formula for the species in this object Parameters ---------- system : {"Hill"}, optional which notation system to use Is not case-sensitive """ # loop different species c = Counter() for atom, indices in self.iter(species=True): if len(indices) > 0: c[atom.symbol] += len(indices) # now we have all elements, and the counts of them systeml = system.lower() if systeml == "hill": # sort lexographically symbols = sorted(c.keys()) def parse(symbol_c): symbol, c = symbol_c if c == 1: return symbol return f"{symbol}{c}" return "".join(map(parse, sorted(c.items()))) raise ValueError( f"{self.__class__.__name__}.formula got unrecognized argument 'system' {system}" )
[docs] def reduce(self, in_place=False): """Returns a new `Atoms` object by removing non-used atoms""" if in_place: atoms = self else: atoms = self.copy() atom = atoms._atom specie = atoms._specie rem = [] for i in range(len(self.atom)): if np.all(specie != i): rem.append(i) # Remove the atoms for i in rem[::-1]: atom.pop(i) specie = np.where(specie > i, specie - 1, specie) atoms._atom = atom atoms._specie = specie atoms._update_orbitals() return atoms
[docs] def sub(self, atoms): """Return a subset of the list""" atoms = _a.asarrayi(atoms).ravel() new_atoms = Atoms() new_atoms._atom = self._atom[:] new_atoms._specie = self._specie[atoms] new_atoms._update_orbitals() return new_atoms
[docs] def remove(self, atoms): """Remove a set of atoms""" atoms = _a.asarrayi(atoms).ravel() idx = np.setdiff1d(np.arange(len(self)), atoms, assume_unique=True) return self.sub(idx)
[docs] def tile(self, reps): """Tile this atom object""" atoms = self.copy() atoms._specie = np.tile(atoms._specie, reps) atoms._update_orbitals() return atoms
[docs] def repeat(self, reps): """Repeat this atom object""" atoms = self.copy() atoms._specie = np.repeat(atoms._specie, reps) atoms._update_orbitals() return atoms
[docs] def swap(self, a, b): """Swaps all atoms""" a = _a.asarrayi(a) b = _a.asarrayi(b) atoms = self.copy() spec = np.copy(atoms._specie) atoms._specie[a] = spec[b] atoms._specie[b] = spec[a] atoms._update_orbitals() return atoms
[docs] def swap_atom(self, a, b): """Swap specie index positions""" speciea = self.specie_index(a) specieb = self.specie_index(b) idx_a = (self._specie == speciea).nonzero()[0] idx_b = (self._specie == specieb).nonzero()[0] atoms = self.copy() atoms._atom[speciea], atoms._atom[specieb] = ( atoms._atom[specieb], atoms._atom[speciea], ) atoms._specie[idx_a] = specieb atoms._specie[idx_b] = speciea atoms._update_orbitals() return atoms
[docs] def append(self, other): """Append `other` to this list of atoms and return the appended version Parameters ---------- other : Atoms or Atom new atoms to be added Returns ------- Atoms merging of this objects atoms and the `other` objects atoms. """ if not isinstance(other, Atoms): other = Atoms(other) atoms = self.copy() spec = np.copy(other._specie) for i, atom in enumerate(other.atom): try: s = atoms.specie_index(atom) except KeyError: s = len(atoms.atom) atoms._atom.append(atom) spec = np.where(other._specie == i, s, spec) atoms._specie = np.concatenate((atoms._specie, spec)) atoms._update_orbitals() return atoms
add = append
[docs] def prepend(self, other): if not isinstance(other, Atoms): other = Atoms(other) return other.append(self)
[docs] def reverse(self, atoms=None): """Returns a reversed geometry Also enables reversing a subset of the atoms. """ copy = self.copy() if atoms is None: copy._specie = self._specie[::-1] else: copy._specie[atoms] = self._specie[atoms[::-1]] copy._update_orbitals() return copy
[docs] def insert(self, index, other): """Insert other atoms into the list of atoms at index""" if isinstance(other, Atom): other = Atoms(other) else: other = other.copy() # Create a copy for insertion atoms = self.copy() spec = other._specie[:] for i, atom in enumerate(other.atom): if atom not in atoms: s = len(atoms.atom) atoms._atom.append(atom) else: s = atoms.specie_index(atom) spec = np.where(spec == i, s, spec) atoms._specie = np.insert(atoms._specie, index, spec) atoms._update_orbitals() return atoms
def __str__(self): """Return the `Atoms` in str""" s = f"{self.__class__.__name__}{{species: {len(self._atom)},\n" for a, idx in self.iter(True): s += " {1}: {0},\n".format(len(idx), str(a).replace("\n", "\n ")) return f"{s}}}" def __repr__(self): return f"<{self.__module__}.{self.__class__.__name__} nspecies={len(self._atom)}, na={len(self)}, no={self.no}>" def __len__(self): """Return number of atoms in the object""" return len(self._specie)
[docs] def iter(self, species=False): """Loop on all atoms This iterator may be used in two contexts: 1. `species` is ``False``, this is the slowest method and will yield the `Atom` per contained atom. 2. `species` is ``True``, which yields a tuple of `(Atom, list)` where ``list`` contains all indices of atoms that has the `Atom` specie. This is much faster than the first option. Parameters ---------- species : bool, optional If ``True`` loops only on different species and yields a tuple of (Atom, list) Else yields the atom for the equivalent index. """ if species: for s, atom in enumerate(self._atom): yield atom, (self.specie == s).nonzero()[0] else: for s in self.specie: yield self._atom[s]
def __iter__(self): """Loop on all atoms with the same specie in order of atoms""" yield from self.iter() def __contains__(self, key): """Determine whether the `key` is in the unique atoms list""" return key in self.atom def __getitem__(self, key): """Return an `Atom` object corresponding to the key(s)""" if isinstance(key, slice): sl = key.indices(len(self)) return [self.atom[self._specie[s]] for s in range(sl[0], sl[1], sl[2])] elif isinstance(key, Integral): return self.atom[self._specie[key]] elif isinstance(key, str): for at in self.atom: if at.tag == key: return at return None key = np.asarray(key) if key.ndim == 0: return self.atom[self._specie[key]] return [self.atom[i] for i in self._specie[key]] def __setitem__(self, key, value): """Overwrite an `Atom` object corresponding to the key(s)""" # If key is a string, we replace the atom that matches 'key' if isinstance(key, str): self.replace_atom(self[key], value) return # Convert to array if isinstance(key, slice): sl = key.indices(len(self)) key = _a.arangei(sl[0], sl[1], sl[2]) else: key = _a.asarrayi(key).ravel() if len(key) == 0: if value not in self: self._atom.append(value) return # Create new atoms object to iterate other = Atoms(value, na=len(key)) # Append the new Atom objects for atom, s_i in other.iter(True): if atom not in self: self._atom.append(atom) self._specie[key[s_i]] = self.specie_index(atom) self._update_orbitals()
[docs] def replace(self, index, atom): """Replace all atomic indices `index` with the atom `atom` (in-place) This is the preferred way of replacing atoms in geometries. Parameters ---------- index : list of int or Atom the indices of the atoms that should be replaced by the new atom. If an `Atom` is passed, this routine defers its call to `replace_atom`. atom : Atom the replacement atom. """ if isinstance(index, Atom): self.replace_atom(index, atom) return if not isinstance(atom, Atom): raise TypeError( f"{self.__class__.__name__}.replace requires input arguments to " "be of the type Atom" ) index = _a.asarrayi(index).ravel() # Be sure to add the atom if atom not in self.atom: self._atom.append(atom) # Get specie index of the atom specie = self.specie_index(atom) # Loop unique species and check that we have the correct number of orbitals for ius in np.unique(self._specie[index]): a = self._atom[ius] if a.no != atom.no: a1 = " " + str(a).replace("\n", "\n ") a2 = " " + str(atom).replace("\n", "\n ") info( f"Substituting atom\n{a1}\n->\n{a2}\nwith a different number of orbitals!" ) self._specie[index] = specie # Update orbital counts... self._update_orbitals()
[docs] def replace_atom(self, atom_from, atom_to): """Replace all atoms equivalent to `atom_from` with `atom_to` (in-place) I.e. this is the preferred way of adapting all atoms of a specific type with another one. If the two atoms does not have the same number of orbitals a warning will be raised. Parameters ---------- atom_from : Atom the atom that should be replaced, if not found in the current list of atoms, nothing will happen. atom_to : Atom the replacement atom. Raises ------ KeyError if `atom_from` does not exist in the list of atoms UserWarning if the atoms does not have the same number of orbitals. """ if not isinstance(atom_from, Atom): raise TypeError( f"{self.__class__.__name__}.replace_atom requires input arguments to " "be of the class Atom" ) if not isinstance(atom_to, Atom): raise TypeError( f"{self.__class__.__name__}.replace_atom requires input arguments to " "be of the class Atom" ) # Get index of `atom_from` idx_from = self.specie_index(atom_from) try: idx_to = self.specie_index(atom_to) if idx_from == idx_to: raise KeyError("") # Decrement indices of the atoms that are # changed to one already there self._specie[self.specie == idx_from] = idx_to self._specie[self.specie > idx_from] -= 1 # Now delete the old index, we replace, so we *have* to remove it self._atom.pop(idx_from) except KeyError: # The atom_to is not in the list # Simply change self._atom[idx_from] = atom_to if atom_from.no != atom_to.no: a1 = " " + str(atom_from).replace("\n", "\n ") a2 = " " + str(atom_to).replace("\n", "\n ") info( f"Replacing atom\n{a1}\n->\n{a2}\nwith a different number of orbitals!" ) # Update orbital counts... self._update_orbitals()
[docs] def hassame(self, other, R=True): """True if the contained atoms are the same in the two lists Notes ----- This does not necessarily mean that the order, nor the number of atoms are the same. Parameters ---------- other : Atoms the list of atoms to check against R : bool, optional if True also checks that the orbital radius are the same See Also -------- equal : explicit check of the indices *and* the contained atoms """ if len(self.atom) != len(other.atom): return False for A in self.atom: is_in = False for B in other.atom: if A.equal(B, R): is_in = True break if not is_in: return False return True
[docs] def equal(self, other, R=True): """True if the contained atoms are the same in the two lists (also checks indices) Parameters ---------- other : Atoms the list of atoms to check against R : bool, optional if True also checks that the orbital radius are the same See Also -------- hassame : only check whether the two atoms are contained in both """ if len(self.atom) > len(other.atom): for iA, A in enumerate(self.atom): is_in = -1 for iB, B in enumerate(other.atom): if A.equal(B, R): is_in = iB break if is_in == -1: return False # We should check that they also have the same indices if not np.all( np.nonzero(self.specie == iA)[0] == np.nonzero(other.specie == is_in)[0] ): return False else: for iB, B in enumerate(other.atom): is_in = -1 for iA, A in enumerate(self.atom): if B.equal(A, R): is_in = iA break if is_in == -1: return False # We should check that they also have the same indices if not np.all( np.nonzero(other.specie == iB)[0] == np.nonzero(self.specie == is_in)[0] ): return False return True
def __eq__(self, b): """Returns true if the contained atoms are the same""" return self.equal(b) # Create pickling routines def __getstate__(self): """Return the state of this object""" return {"atom": self.atom, "specie": self.specie} def __setstate__(self, d): """Re-create the state of this object""" self.__init__() self._atom = d["atom"] self._specie = d["specie"]