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