Coverage for /builds/kinetik161/ase/ase/symbols.py: 96.08%
102 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-12-10 11:04 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-12-10 11:04 +0000
1import collections.abc
2import numbers
3import warnings
4from typing import Dict, Iterator, List, Sequence, Set, Union
6import numpy as np
8from ase.data import atomic_numbers, chemical_symbols
9from ase.formula import Formula
11Integers = Union[Sequence[int], np.ndarray]
14def string2symbols(s: str) -> List[str]:
15 """Convert string to list of chemical symbols."""
16 return list(Formula(s))
19def symbols2numbers(symbols) -> List[int]:
20 if isinstance(symbols, str):
21 symbols = string2symbols(symbols)
22 numbers = []
23 for s in symbols:
24 if isinstance(s, str):
25 numbers.append(atomic_numbers[s])
26 else:
27 numbers.append(int(s))
28 return numbers
31class Symbols(collections.abc.Sequence):
32 """A sequence of chemical symbols.
34 ``atoms.symbols`` is a :class:`ase.symbols.Symbols` object. This
35 object works like an editable view of ``atoms.numbers``, except
36 its elements are manipulated as strings.
38 Examples:
40 >>> from ase.build import molecule
41 >>> atoms = molecule('CH3CH2OH')
42 >>> atoms.symbols
43 Symbols('C2OH6')
44 >>> atoms.symbols[:3]
45 Symbols('C2O')
46 >>> atoms.symbols == 'H' # doctest: +ELLIPSIS
47 array([False, False, False, True, True, True, True, True, True]...)
48 >>> atoms.symbols[-3:] = 'Pu'
49 >>> atoms.symbols
50 Symbols('C2OH3Pu3')
51 >>> atoms.symbols[3:6] = 'Mo2U'
52 >>> atoms.symbols
53 Symbols('C2OMo2UPu3')
54 >>> atoms.symbols.formula
55 Formula('C2OMo2UPu3')
57 The :class:`ase.formula.Formula` object is useful for extended
58 formatting options and analysis.
60 """
62 def __init__(self, numbers) -> None:
63 self.numbers = np.asarray(numbers, int)
65 @classmethod
66 def fromsymbols(cls, symbols) -> 'Symbols':
67 numbers = symbols2numbers(symbols)
68 return cls(np.array(numbers))
70 @property
71 def formula(self) -> Formula:
72 """Formula object."""
73 string = Formula.from_list(self).format('reduce')
74 return Formula(string)
76 def __getitem__(self, key) -> Union['Symbols', str]:
77 num = self.numbers[key]
78 if isinstance(num, numbers.Integral):
79 return chemical_symbols[num]
80 return Symbols(num)
82 def __iter__(self) -> Iterator[str]:
83 for num in self.numbers:
84 yield chemical_symbols[num]
86 def __setitem__(self, key, value) -> None:
87 numbers = symbols2numbers(value)
88 if len(numbers) == 1:
89 self.numbers[key] = numbers[0]
90 else:
91 self.numbers[key] = numbers
93 def __len__(self) -> int:
94 return len(self.numbers)
96 def __str__(self) -> str:
97 return self.get_chemical_formula('reduce')
99 def __repr__(self) -> str:
100 return f'Symbols(\'{self}\')'
102 def __eq__(self, obj) -> bool:
103 if not hasattr(obj, '__len__'):
104 return False
106 try:
107 symbols = Symbols.fromsymbols(obj)
108 except Exception:
109 # Typically this would happen if obj cannot be converged to
110 # atomic numbers.
111 return False
112 return self.numbers == symbols.numbers
114 def get_chemical_formula(
115 self,
116 mode: str = 'hill',
117 empirical: bool = False,
118 ) -> str:
119 """Get chemical formula.
121 See documentation of ase.atoms.Atoms.get_chemical_formula()."""
122 # XXX Delegate the work to the Formula object!
123 if mode in ('reduce', 'all') and empirical:
124 warnings.warn("Empirical chemical formula not available "
125 "for mode '{}'".format(mode))
127 if len(self) == 0:
128 return ''
130 numbers = self.numbers
132 if mode == 'reduce':
133 n = len(numbers)
134 changes = np.concatenate(([0], np.arange(1, n)[numbers[1:] !=
135 numbers[:-1]]))
136 symbols = [chemical_symbols[e] for e in numbers[changes]]
137 counts = np.append(changes[1:], n) - changes
139 tokens = []
140 for s, c in zip(symbols, counts):
141 tokens.append(s)
142 if c > 1:
143 tokens.append(str(c))
144 formula = ''.join(tokens)
145 elif mode == 'all':
146 formula = ''.join([chemical_symbols[n] for n in numbers])
147 else:
148 symbols = [chemical_symbols[Z] for Z in numbers]
149 f = Formula('', _tree=[(symbols, 1)])
150 if empirical:
151 f, _ = f.reduce()
152 if mode in {'hill', 'metal'}:
153 formula = f.format(mode)
154 else:
155 raise ValueError(
156 "Use mode = 'all', 'reduce', 'hill' or 'metal'.")
158 return formula
160 def search(self, symbols) -> Integers:
161 """Return the indices of elements with given symbol or symbols."""
162 numbers = set(symbols2numbers(symbols))
163 indices = [i for i, number in enumerate(self.numbers)
164 if number in numbers]
165 return np.array(indices, int)
167 def species(self) -> Set[str]:
168 """Return unique symbols as a set."""
169 return set(self)
171 def indices(self) -> Dict[str, Integers]:
172 """Return dictionary mapping each unique symbol to indices.
174 >>> from ase.build import molecule
175 >>> atoms = molecule('CH3CH2OH')
176 >>> atoms.symbols.indices()
177 {'C': array([0, 1]), 'O': array([2]), 'H': array([3, 4, 5, 6, 7, 8])}
179 """
180 dct: Dict[str, List[int]] = {}
181 for i, symbol in enumerate(self):
182 dct.setdefault(symbol, []).append(i)
183 return {key: np.array(value, int) for key, value in dct.items()}
185 def species_indices(self) -> Sequence[int]:
186 """Return the indices of each atom within their individual species.
188 >>> from ase import Atoms
189 >>> atoms = Atoms('CH3CH2OH')
190 >>> atoms.symbols.species_indices()
191 [0, 0, 1, 2, 1, 3, 4, 0, 5]
193 ^ ^ ^ ^ ^ ^ ^ ^ ^
194 C H H H C H H O H
196 """
198 counts: Dict[str, int] = {}
199 result = []
200 for i, n in enumerate(self.numbers):
201 counts[n] = counts.get(n, -1) + 1
202 result.append(counts[n])
204 return result