Coverage for /builds/debichem-team/python-ase/ase/db/row.py: 92.76%
221 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-03-06 04:00 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-03-06 04:00 +0000
1from random import randint
2from typing import Any, Dict
4import numpy as np
6from ase import Atoms
7from ase.calculators.calculator import (
8 PropertyNotImplementedError,
9 all_properties,
10 kptdensity2monkhorstpack,
11)
12from ase.calculators.singlepoint import SinglePointCalculator
13from ase.data import atomic_masses, chemical_symbols
14from ase.formula import Formula
15from ase.geometry import cell_to_cellpar
16from ase.io.jsonio import decode
19class FancyDict(dict):
20 """Dictionary with keys available as attributes also."""
22 def __getattr__(self, key):
23 if key not in self:
24 return dict.__getattribute__(self, key)
25 value = self[key]
26 if isinstance(value, dict):
27 return FancyDict(value)
28 return value
30 def __dir__(self):
31 return self.keys() # for tab-completion
34def atoms2dict(atoms):
35 dct = {
36 'numbers': atoms.numbers,
37 'positions': atoms.positions,
38 'unique_id': '%x' % randint(16**31, 16**32 - 1)}
39 if atoms.pbc.any():
40 dct['pbc'] = atoms.pbc
41 if atoms.cell.any():
42 dct['cell'] = atoms.cell
43 if atoms.has('initial_magmoms'):
44 dct['initial_magmoms'] = atoms.get_initial_magnetic_moments()
45 if atoms.has('initial_charges'):
46 dct['initial_charges'] = atoms.get_initial_charges()
47 if atoms.has('masses'):
48 dct['masses'] = atoms.get_masses()
49 if atoms.has('tags'):
50 dct['tags'] = atoms.get_tags()
51 if atoms.has('momenta'):
52 dct['momenta'] = atoms.get_momenta()
53 if atoms.constraints:
54 dct['constraints'] = [c.todict() for c in atoms.constraints]
55 if atoms.calc is not None:
56 dct['calculator'] = atoms.calc.name.lower()
57 dct['calculator_parameters'] = atoms.calc.todict()
58 if len(atoms.calc.check_state(atoms)) == 0:
59 for prop in all_properties:
60 try:
61 x = atoms.calc.get_property(prop, atoms, False)
62 except PropertyNotImplementedError:
63 pass
64 else:
65 if x is not None:
66 dct[prop] = x
67 return dct
70class AtomsRow:
71 mtime: float
72 positions: np.ndarray
73 id: int
75 def __init__(self, dct):
76 if isinstance(dct, dict):
77 dct = dct.copy()
78 if 'calculator_parameters' in dct:
79 # Earlier version of ASE would encode the calculator
80 # parameter dict again and again and again ...
81 while isinstance(dct['calculator_parameters'], str):
82 dct['calculator_parameters'] = decode(
83 dct['calculator_parameters'])
84 else:
85 dct = atoms2dict(dct)
86 assert 'numbers' in dct
87 self._constraints = dct.pop('constraints', [])
88 self._constrained_forces = None
89 self._data = dct.pop('data', {})
90 kvp = dct.pop('key_value_pairs', {})
91 self._keys = list(kvp.keys())
92 self.__dict__.update(kvp)
93 self.__dict__.update(dct)
94 if 'cell' not in dct:
95 self.cell = np.zeros((3, 3))
96 if 'pbc' not in dct:
97 self.pbc = np.zeros(3, bool)
99 def __contains__(self, key):
100 return key in self.__dict__
102 def __iter__(self):
103 return (key for key in self.__dict__ if key[0] != '_')
105 def get(self, key, default=None):
106 """Return value of key if present or default if not."""
107 return getattr(self, key, default)
109 @property
110 def key_value_pairs(self):
111 """Return dict of key-value pairs."""
112 return {key: self.get(key) for key in self._keys}
114 def count_atoms(self):
115 """Count atoms.
117 Return dict mapping chemical symbol strings to number of atoms.
118 """
119 count = {}
120 for symbol in self.symbols:
121 count[symbol] = count.get(symbol, 0) + 1
122 return count
124 def __getitem__(self, key):
125 return getattr(self, key)
127 def __setitem__(self, key, value):
128 setattr(self, key, value)
130 def __str__(self):
131 return '<AtomsRow: formula={}, keys={}>'.format(
132 self.formula, ','.join(self._keys))
134 @property
135 def constraints(self):
136 """List of constraints."""
137 from ase.constraints import dict2constraint
138 if not isinstance(self._constraints, list):
139 # Lazy decoding:
140 cs = decode(self._constraints)
141 self._constraints = []
142 for c in cs:
143 # Convert to new format:
144 name = c.pop('__name__', None)
145 if name:
146 c = {'name': name, 'kwargs': c}
147 if c['name'].startswith('ase'):
148 c['name'] = c['name'].rsplit('.', 1)[1]
149 self._constraints.append(c)
150 return [dict2constraint(d) for d in self._constraints]
152 @property
153 def data(self):
154 """Data dict."""
155 if isinstance(self._data, str):
156 self._data = decode(self._data) # lazy decoding
157 elif isinstance(self._data, bytes):
158 from ase.db.core import bytes_to_object
159 self._data = bytes_to_object(self._data) # lazy decoding
160 return FancyDict(self._data)
162 @property
163 def natoms(self):
164 """Number of atoms."""
165 return len(self.numbers)
167 @property
168 def formula(self):
169 """Chemical formula string."""
170 return Formula('', _tree=[(self.symbols, 1)]).format('metal')
172 @property
173 def symbols(self):
174 """List of chemical symbols."""
175 return [chemical_symbols[Z] for Z in self.numbers]
177 @property
178 def fmax(self):
179 """Maximum atomic force."""
180 forces = self.constrained_forces
181 return (forces**2).sum(1).max()**0.5
183 @property
184 def constrained_forces(self):
185 """Forces after applying constraints."""
186 if self._constrained_forces is not None:
187 return self._constrained_forces
188 forces = self.forces
189 constraints = self.constraints
190 if constraints:
191 forces = forces.copy()
192 atoms = self.toatoms()
193 for constraint in constraints:
194 constraint.adjust_forces(atoms, forces)
196 self._constrained_forces = forces
197 return forces
199 @property
200 def smax(self):
201 """Maximum stress tensor component."""
202 return (self.stress**2).max()**0.5
204 @property
205 def mass(self):
206 """Total mass."""
207 if 'masses' in self:
208 return self.masses.sum()
209 return atomic_masses[self.numbers].sum()
211 @property
212 def volume(self):
213 """Volume of unit cell."""
214 if self.cell is None:
215 return None
216 vol = abs(np.linalg.det(self.cell))
217 if vol == 0.0:
218 raise AttributeError
219 return vol
221 @property
222 def charge(self):
223 """Total charge."""
224 charges = self.get('initial_charges')
225 if charges is None:
226 return 0.0
227 return charges.sum()
229 def toatoms(self,
230 add_additional_information=False):
231 """Create Atoms object."""
232 atoms = Atoms(self.numbers,
233 self.positions,
234 cell=self.cell,
235 pbc=self.pbc,
236 magmoms=self.get('initial_magmoms'),
237 charges=self.get('initial_charges'),
238 tags=self.get('tags'),
239 masses=self.get('masses'),
240 momenta=self.get('momenta'),
241 constraint=self.constraints)
243 results = {prop: self[prop] for prop in all_properties if prop in self}
244 if results:
245 atoms.calc = SinglePointCalculator(atoms, **results)
246 atoms.calc.name = self.get('calculator', 'unknown')
248 if add_additional_information:
249 atoms.info = {}
250 atoms.info['unique_id'] = self.unique_id
251 if self._keys:
252 atoms.info['key_value_pairs'] = self.key_value_pairs
253 data = self.get('data')
254 if data:
255 atoms.info['data'] = data
257 return atoms
260def row2dct(row, key_descriptions) -> Dict[str, Any]:
261 """Convert row to dict of things for printing or a web-page."""
263 from ase.db.core import float_to_time_string, now
265 dct = {}
267 atoms = Atoms(cell=row.cell, pbc=row.pbc)
268 dct['size'] = kptdensity2monkhorstpack(atoms,
269 kptdensity=1.8,
270 even=False)
272 dct['cell'] = [[f'{a:.3f}' for a in axis] for axis in row.cell]
273 par = [f'{x:.3f}' for x in cell_to_cellpar(row.cell)]
274 dct['lengths'] = par[:3]
275 dct['angles'] = par[3:]
277 stress = row.get('stress')
278 if stress is not None:
279 dct['stress'] = ', '.join(f'{s:.3f}' for s in stress)
281 dct['formula'] = Formula(row.formula).format('abc')
283 dipole = row.get('dipole')
284 if dipole is not None:
285 dct['dipole'] = ', '.join(f'{d:.3f}' for d in dipole)
287 data = row.get('data')
288 if data:
289 dct['data'] = ', '.join(data.keys())
291 constraints = row.get('constraints')
292 if constraints:
293 dct['constraints'] = ', '.join(c.__class__.__name__
294 for c in constraints)
296 keys = ({'id', 'energy', 'fmax', 'smax', 'mass', 'age'} |
297 set(key_descriptions) |
298 set(row.key_value_pairs))
299 dct['table'] = []
301 from ase.db.project import KeyDescription
302 for key in keys:
303 if key == 'age':
304 age = float_to_time_string(now() - row.ctime, True)
305 dct['table'].append(('ctime', 'Age', age))
306 continue
307 value = row.get(key)
308 if value is not None:
309 if isinstance(value, float):
310 value = f'{value:.3f}'
311 elif not isinstance(value, str):
312 value = str(value)
314 nokeydesc = KeyDescription(key, '', '', '')
315 keydesc = key_descriptions.get(key, nokeydesc)
316 unit = keydesc.unit
317 if unit:
318 value += ' ' + unit
319 dct['table'].append((key, keydesc.longdesc, value))
321 return dct