Coverage for /builds/ase/ase/ase/atoms.py: 93.71%

985 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-08-02 00:12 +0000

1# fmt: off 

2 

3# Copyright 2008, 2009 CAMd 

4# (see accompanying license files for details). 

5 

6"""Definition of the Atoms class. 

7 

8This module defines the central object in the ASE package: the Atoms 

9object. 

10""" 

11from __future__ import annotations 

12 

13import copy 

14import numbers 

15from math import cos, pi, sin 

16from typing import Sequence, Union, overload 

17 

18import numpy as np 

19 

20from ase import units 

21from ase.atom import Atom 

22from ase.cell import Cell 

23from ase.data import atomic_masses, atomic_masses_common 

24from ase.stress import full_3x3_to_voigt_6_stress, voigt_6_to_full_3x3_stress 

25from ase.symbols import Symbols, symbols2numbers 

26from ase.utils import deprecated, string2index 

27 

28 

29class Atoms: 

30 """Atoms object. 

31 

32 The Atoms object can represent an isolated molecule, or a 

33 periodically repeated structure. It has a unit cell and 

34 there may be periodic boundary conditions along any of the three 

35 unit cell axes. 

36 Information about the atoms (atomic numbers and position) is 

37 stored in ndarrays. Optionally, there can be information about 

38 tags, momenta, masses, magnetic moments and charges. 

39 

40 In order to calculate energies, forces and stresses, a calculator 

41 object has to attached to the atoms object. 

42 

43 Parameters 

44 ---------- 

45 symbols : str | list[str] | list[Atom] 

46 Chemical formula, a list of chemical symbols, or list of 

47 :class:`~ase.Atom` objects (mutually exclusive with ``numbers``). 

48 

49 - ``'H2O'`` 

50 - ``'COPt12'`` 

51 - ``['H', 'H', 'O']`` 

52 - ``[Atom('Ne', (x, y, z)), ...]`` 

53 

54 positions : list[tuple[float, float, float]] 

55 Atomic positions in Cartesian coordinates 

56 (mutually exclusive with ``scaled_positions``). 

57 Anything that can be converted to an ndarray of shape (n, 3) works: 

58 [(x0, y0, z0), (x1, y1, z1), ...]. 

59 scaled_positions : list[tuple[float, float, float]] 

60 Atomic positions in units of the unit cell 

61 (mutually exclusive with ``positions``). 

62 numbers : list[int] 

63 Atomic numbers (mutually exclusive with ``symbols``). 

64 tags : list[int] 

65 Special purpose tags. 

66 momenta : list[tuple[float, float, float]] 

67 Momenta for all atoms in Cartesian coordinates 

68 (mutually exclusive with ``velocities``). 

69 velocities : list[tuple[float, float, float]] 

70 Velocities for all atoms in Cartesian coordinates 

71 (mutually exclusive with ``momenta``). 

72 masses : list[float] 

73 Atomic masses in atomic units. 

74 magmoms : list[float] | list[tuple[float, float, float]] 

75 Magnetic moments. Can be either a single value for each atom 

76 for collinear calculations or three numbers for each atom for 

77 non-collinear calculations. 

78 charges : list[float] 

79 Initial atomic charges. 

80 cell : 3x3 matrix or length 3 or 6 vector, default: (0, 0, 0) 

81 Unit cell vectors. Can also be given as just three 

82 numbers for orthorhombic cells, or 6 numbers, where 

83 first three are lengths of unit cell vectors, and the 

84 other three are angles between them (in degrees), in following order: 

85 [len(a), len(b), len(c), angle(b,c), angle(a,c), angle(a,b)]. 

86 First vector will lie in x-direction, second in xy-plane, 

87 and the third one in z-positive subspace. 

88 celldisp : tuple[float, float, float], default: (0, 0, 0) 

89 Unit cell displacement vector. To visualize a displaced cell 

90 around the center of mass of a Systems of atoms. 

91 pbc : bool | tuple[bool, bool, bool], default: False 

92 Periodic boundary conditions flags. 

93 

94 - ``True`` 

95 - ``False`` 

96 - ``0`` 

97 - ``1`` 

98 - ``(1, 1, 0)`` 

99 - ``(True, False, False)`` 

100 

101 constraint : constraint object(s) 

102 One or more ASE constraints applied during structure optimization. 

103 calculator : calculator object 

104 ASE calculator to obtain energies and atomic forces. 

105 info : dict | None, default: None 

106 Dictionary with additional information about the system. 

107 The following keys may be used by ASE: 

108 

109 - spacegroup: :class:`~ase.spacegroup.Spacegroup` instance 

110 - unit_cell: 'conventional' | 'primitive' | int | 3 ints 

111 - adsorbate_info: Information about special adsorption sites 

112 

113 Items in the info attribute survives copy and slicing and can 

114 be stored in and retrieved from trajectory files given that the 

115 key is a string, the value is JSON-compatible and, if the value is a 

116 user-defined object, its base class is importable. One should 

117 not make any assumptions about the existence of keys. 

118 

119 Examples 

120 -------- 

121 >>> from ase import Atom 

122 

123 N2 molecule (These three are equivalent): 

124 

125 >>> d = 1.104 # N2 bondlength 

126 >>> a = Atoms('N2', [(0, 0, 0), (0, 0, d)]) 

127 >>> a = Atoms(numbers=[7, 7], positions=[(0, 0, 0), (0, 0, d)]) 

128 >>> a = Atoms([Atom('N', (0, 0, 0)), Atom('N', (0, 0, d))]) 

129 

130 FCC gold: 

131 

132 >>> a = 4.05 # Gold lattice constant 

133 >>> b = a / 2 

134 >>> fcc = Atoms('Au', 

135 ... cell=[(0, b, b), (b, 0, b), (b, b, 0)], 

136 ... pbc=True) 

137 

138 Hydrogen wire: 

139 

140 >>> d = 0.9 # H-H distance 

141 >>> h = Atoms('H', positions=[(0, 0, 0)], 

142 ... cell=(d, 0, 0), 

143 ... pbc=(1, 0, 0)) 

144 """ 

145 

146 ase_objtype = 'atoms' # For JSONability 

147 

148 def __init__(self, symbols=None, 

149 positions=None, numbers=None, 

150 tags=None, momenta=None, masses=None, 

151 magmoms=None, charges=None, 

152 scaled_positions=None, 

153 cell=None, pbc=None, celldisp=None, 

154 constraint=None, 

155 calculator=None, 

156 info=None, 

157 velocities=None): 

158 

159 self._cellobj = Cell.new() 

160 self._pbc = np.zeros(3, bool) 

161 

162 atoms = None 

163 

164 if hasattr(symbols, 'get_positions'): 

165 atoms = symbols 

166 symbols = None 

167 elif (isinstance(symbols, (list, tuple)) and 

168 len(symbols) > 0 and isinstance(symbols[0], Atom)): 

169 # Get data from a list or tuple of Atom objects: 

170 data = [[atom.get_raw(name) for atom in symbols] 

171 for name in 

172 ['position', 'number', 'tag', 'momentum', 

173 'mass', 'magmom', 'charge']] 

174 atoms = self.__class__(None, *data) 

175 symbols = None 

176 

177 if atoms is not None: 

178 # Get data from another Atoms object: 

179 if scaled_positions is not None: 

180 raise NotImplementedError 

181 if symbols is None and numbers is None: 

182 numbers = atoms.get_atomic_numbers() 

183 if positions is None: 

184 positions = atoms.get_positions() 

185 if tags is None and atoms.has('tags'): 

186 tags = atoms.get_tags() 

187 if momenta is None and atoms.has('momenta'): 

188 momenta = atoms.get_momenta() 

189 if magmoms is None and atoms.has('initial_magmoms'): 

190 magmoms = atoms.get_initial_magnetic_moments() 

191 if masses is None and atoms.has('masses'): 

192 masses = atoms.get_masses() 

193 if charges is None and atoms.has('initial_charges'): 

194 charges = atoms.get_initial_charges() 

195 if cell is None: 

196 cell = atoms.get_cell() 

197 if celldisp is None: 

198 celldisp = atoms.get_celldisp() 

199 if pbc is None: 

200 pbc = atoms.get_pbc() 

201 if constraint is None: 

202 constraint = [c.copy() for c in atoms.constraints] 

203 if calculator is None: 

204 calculator = atoms.calc 

205 if info is None: 

206 info = copy.deepcopy(atoms.info) 

207 

208 self.arrays: dict[str, np.ndarray] = {} 

209 

210 if symbols is not None and numbers is not None: 

211 raise TypeError('Use only one of "symbols" and "numbers".') 

212 if symbols is not None: 

213 numbers = symbols2numbers(symbols) 

214 elif numbers is None: 

215 if positions is not None: 

216 natoms = len(positions) 

217 elif scaled_positions is not None: 

218 natoms = len(scaled_positions) 

219 else: 

220 natoms = 0 

221 numbers = np.zeros(natoms, int) 

222 self.new_array('numbers', numbers, int) 

223 

224 if self.numbers.ndim != 1: 

225 raise ValueError('"numbers" must be 1-dimensional.') 

226 

227 if cell is None: 

228 cell = np.zeros((3, 3)) 

229 self.set_cell(cell) 

230 

231 if celldisp is None: 

232 celldisp = np.zeros(shape=(3, 1)) 

233 self.set_celldisp(celldisp) 

234 

235 if positions is None: 

236 if scaled_positions is None: 

237 positions = np.zeros((len(self.arrays['numbers']), 3)) 

238 else: 

239 assert self.cell.rank == 3 

240 positions = np.dot(scaled_positions, self.cell) 

241 else: 

242 if scaled_positions is not None: 

243 raise TypeError( 

244 'Use only one of "positions" and "scaled_positions".') 

245 self.new_array('positions', positions, float, (3,)) 

246 

247 self.set_constraint(constraint) 

248 self.set_tags(default(tags, 0)) 

249 self.set_masses(default(masses, None)) 

250 self.set_initial_magnetic_moments(default(magmoms, 0.0)) 

251 self.set_initial_charges(default(charges, 0.0)) 

252 if pbc is None: 

253 pbc = False 

254 self.set_pbc(pbc) 

255 self.set_momenta(default(momenta, (0.0, 0.0, 0.0)), 

256 apply_constraint=False) 

257 

258 if velocities is not None: 

259 if momenta is None: 

260 self.set_velocities(velocities) 

261 else: 

262 raise TypeError( 

263 'Use only one of "momenta" and "velocities".') 

264 

265 if info is None: 

266 self.info = {} 

267 else: 

268 self.info = dict(info) 

269 

270 self.calc = calculator 

271 

272 @property 

273 def symbols(self): 

274 """Get chemical symbols as a :class:`ase.symbols.Symbols` object. 

275 

276 The object works like ``atoms.numbers`` except its values 

277 are strings. It supports in-place editing.""" 

278 return Symbols(self.numbers) 

279 

280 @symbols.setter 

281 def symbols(self, obj): 

282 new_symbols = Symbols.fromsymbols(obj) 

283 self.numbers[:] = new_symbols.numbers 

284 

285 def get_chemical_symbols(self): 

286 """Get list of chemical symbol strings. 

287 

288 Equivalent to ``list(atoms.symbols)``.""" 

289 return list(self.symbols) 

290 

291 def set_chemical_symbols(self, symbols): 

292 """Set chemical symbols.""" 

293 self.set_array('numbers', symbols2numbers(symbols), int, ()) 

294 

295 @property 

296 def numbers(self): 

297 """Attribute for direct manipulation of the atomic numbers.""" 

298 return self.arrays['numbers'] 

299 

300 @numbers.setter 

301 def numbers(self, numbers): 

302 self.set_atomic_numbers(numbers) 

303 

304 def set_atomic_numbers(self, numbers): 

305 """Set atomic numbers.""" 

306 self.set_array('numbers', numbers, int, ()) 

307 

308 def get_atomic_numbers(self): 

309 """Get integer array of atomic numbers.""" 

310 return self.arrays['numbers'].copy() 

311 

312 @property 

313 def positions(self): 

314 """Attribute for direct manipulation of the positions.""" 

315 return self.arrays['positions'] 

316 

317 @positions.setter 

318 def positions(self, pos): 

319 self.arrays['positions'][:] = pos 

320 

321 def set_positions(self, newpositions, apply_constraint=True): 

322 """Set positions, honoring any constraints. To ignore constraints, 

323 use *apply_constraint=False*.""" 

324 if self.constraints and apply_constraint: 

325 newpositions = np.array(newpositions, float) 

326 for constraint in self.constraints: 

327 constraint.adjust_positions(self, newpositions) 

328 

329 self.set_array('positions', newpositions, shape=(3,)) 

330 

331 def get_positions(self, wrap=False, **wrap_kw): 

332 """Get array of positions. 

333 

334 Parameters: 

335 

336 wrap: bool 

337 wrap atoms back to the cell before returning positions 

338 wrap_kw: (keyword=value) pairs 

339 optional keywords `pbc`, `center`, `pretty_translation`, `eps`, 

340 see :func:`ase.geometry.wrap_positions` 

341 """ 

342 from ase.geometry import wrap_positions 

343 if wrap: 

344 if 'pbc' not in wrap_kw: 

345 wrap_kw['pbc'] = self.pbc 

346 return wrap_positions(self.positions, self.cell, **wrap_kw) 

347 else: 

348 return self.arrays['positions'].copy() 

349 

350 @deprecated("Please use atoms.calc = calc", FutureWarning) 

351 def set_calculator(self, calc=None): 

352 """Attach calculator object. 

353 

354 .. deprecated:: 3.20.0 

355 Please use the equivalent ``atoms.calc = calc`` instead of this 

356 method. 

357 """ 

358 

359 self.calc = calc 

360 

361 @deprecated("Please use atoms.calc", FutureWarning) 

362 def get_calculator(self): 

363 """Get currently attached calculator object. 

364 

365 .. deprecated:: 3.20.0 

366 Please use the equivalent ``atoms.calc`` instead of 

367 ``atoms.get_calculator()``. 

368 """ 

369 

370 return self.calc 

371 

372 @property 

373 def calc(self): 

374 """Calculator object.""" 

375 return self._calc 

376 

377 @calc.setter 

378 def calc(self, calc): 

379 self._calc = calc 

380 if hasattr(calc, 'set_atoms'): 

381 calc.set_atoms(self) 

382 

383 @calc.deleter 

384 @deprecated('Please use atoms.calc = None', FutureWarning) 

385 def calc(self): 

386 """Delete calculator 

387 

388 .. deprecated:: 3.20.0 

389 Please use ``atoms.calc = None`` 

390 """ 

391 self._calc = None 

392 

393 @property 

394 @deprecated('Please use atoms.cell.rank instead', DeprecationWarning) 

395 def number_of_lattice_vectors(self): 

396 """Number of (non-zero) lattice vectors. 

397 

398 .. deprecated:: 3.21.0 

399 Please use ``atoms.cell.rank`` instead 

400 """ 

401 return self.cell.rank 

402 

403 @property 

404 def constraints(self): 

405 return self._constraints 

406 

407 @constraints.setter 

408 def constraints(self, constraint): 

409 self.set_constraint(constraint) 

410 

411 @constraints.deleter 

412 def constraints(self): 

413 self._constraints = [] 

414 

415 def set_constraint(self, constraint=None): 

416 """Apply one or more constrains. 

417 

418 The *constraint* argument must be one constraint object or a 

419 list of constraint objects.""" 

420 if constraint is None: 

421 self._constraints = [] 

422 else: 

423 if isinstance(constraint, list): 

424 self._constraints = constraint 

425 elif isinstance(constraint, tuple): 

426 self._constraints = list(constraint) 

427 else: 

428 self._constraints = [constraint] 

429 

430 def get_number_of_degrees_of_freedom(self): 

431 """Calculate the number of degrees of freedom in the system.""" 

432 return len(self) * 3 - sum( 

433 c.get_removed_dof(self) for c in self._constraints 

434 ) 

435 

436 def set_cell(self, cell, scale_atoms=False, apply_constraint=True): 

437 """Set unit cell vectors. 

438 

439 Parameters: 

440 

441 cell: 3x3 matrix or length 3 or 6 vector 

442 Unit cell. A 3x3 matrix (the three unit cell vectors) or 

443 just three numbers for an orthorhombic cell. Another option is 

444 6 numbers, which describes unit cell with lengths of unit cell 

445 vectors and with angles between them (in degrees), in following 

446 order: [len(a), len(b), len(c), angle(b,c), angle(a,c), 

447 angle(a,b)]. First vector will lie in x-direction, second in 

448 xy-plane, and the third one in z-positive subspace. 

449 scale_atoms: bool 

450 Fix atomic positions or move atoms with the unit cell? 

451 Default behavior is to *not* move the atoms (scale_atoms=False). 

452 apply_constraint: bool 

453 Whether to apply constraints to the given cell. 

454 

455 Examples: 

456 

457 Two equivalent ways to define an orthorhombic cell: 

458 

459 >>> atoms = Atoms('He') 

460 >>> a, b, c = 7, 7.5, 8 

461 >>> atoms.set_cell([a, b, c]) 

462 >>> atoms.set_cell([(a, 0, 0), (0, b, 0), (0, 0, c)]) 

463 

464 FCC unit cell: 

465 

466 >>> atoms.set_cell([(0, b, b), (b, 0, b), (b, b, 0)]) 

467 

468 Hexagonal unit cell: 

469 

470 >>> atoms.set_cell([a, a, c, 90, 90, 120]) 

471 

472 Rhombohedral unit cell: 

473 

474 >>> alpha = 77 

475 >>> atoms.set_cell([a, a, a, alpha, alpha, alpha]) 

476 """ 

477 

478 # Override pbcs if and only if given a Cell object: 

479 cell = Cell.new(cell) 

480 

481 # XXX not working well during initialize due to missing _constraints 

482 if apply_constraint and hasattr(self, '_constraints'): 

483 for constraint in self.constraints: 

484 if hasattr(constraint, 'adjust_cell'): 

485 constraint.adjust_cell(self, cell) 

486 

487 if scale_atoms: 

488 M = np.linalg.solve(self.cell.complete(), cell.complete()) 

489 self.positions[:] = np.dot(self.positions, M) 

490 

491 self.cell[:] = cell 

492 

493 def set_celldisp(self, celldisp): 

494 """Set the unit cell displacement vectors.""" 

495 celldisp = np.array(celldisp, float) 

496 self._celldisp = celldisp 

497 

498 def get_celldisp(self): 

499 """Get the unit cell displacement vectors.""" 

500 return self._celldisp.copy() 

501 

502 def get_cell(self, complete=False): 

503 """Get the three unit cell vectors as a `class`:ase.cell.Cell` object. 

504 

505 The Cell object resembles a 3x3 ndarray, and cell[i, j] 

506 is the jth Cartesian coordinate of the ith cell vector.""" 

507 if complete: 

508 cell = self.cell.complete() 

509 else: 

510 cell = self.cell.copy() 

511 

512 return cell 

513 

514 @deprecated('Please use atoms.cell.cellpar() instead', DeprecationWarning) 

515 def get_cell_lengths_and_angles(self): 

516 """Get unit cell parameters. Sequence of 6 numbers. 

517 

518 First three are unit cell vector lengths and second three 

519 are angles between them:: 

520 

521 [len(a), len(b), len(c), angle(b,c), angle(a,c), angle(a,b)] 

522 

523 in degrees. 

524 

525 .. deprecated:: 3.21.0 

526 Please use ``atoms.cell.cellpar()`` instead 

527 """ 

528 return self.cell.cellpar() 

529 

530 @deprecated('Please use atoms.cell.reciprocal()', DeprecationWarning) 

531 def get_reciprocal_cell(self): 

532 """Get the three reciprocal lattice vectors as a 3x3 ndarray. 

533 

534 Note that the commonly used factor of 2 pi for Fourier 

535 transforms is not included here. 

536 

537 .. deprecated:: 3.21.0 

538 Please use ``atoms.cell.reciprocal()`` 

539 """ 

540 return self.cell.reciprocal() 

541 

542 @property 

543 def pbc(self): 

544 """Reference to pbc-flags for in-place manipulations.""" 

545 return self._pbc 

546 

547 @pbc.setter 

548 def pbc(self, pbc): 

549 self._pbc[:] = pbc 

550 

551 def set_pbc(self, pbc): 

552 """Set periodic boundary condition flags.""" 

553 self.pbc = pbc 

554 

555 def get_pbc(self): 

556 """Get periodic boundary condition flags.""" 

557 return self.pbc.copy() 

558 

559 def new_array(self, name, a, dtype=None, shape=None): 

560 """Add new array. 

561 

562 If *shape* is not *None*, the shape of *a* will be checked.""" 

563 

564 if dtype is not None: 

565 a = np.array(a, dtype, order='C') 

566 if len(a) == 0 and shape is not None: 

567 a.shape = (-1,) + shape 

568 else: 

569 if not a.flags['C_CONTIGUOUS']: 

570 a = np.ascontiguousarray(a) 

571 else: 

572 a = a.copy() 

573 

574 if name in self.arrays: 

575 raise RuntimeError(f'Array {name} already present') 

576 

577 for b in self.arrays.values(): 

578 if len(a) != len(b): 

579 raise ValueError('Array "%s" has wrong length: %d != %d.' % 

580 (name, len(a), len(b))) 

581 break 

582 

583 if shape is not None and a.shape[1:] != shape: 

584 raise ValueError( 

585 f'Array "{name}" has wrong shape {a.shape} != ' 

586 f'{(a.shape[0:1] + shape)}.') 

587 

588 self.arrays[name] = a 

589 

590 def get_array(self, name, copy=True): 

591 """Get an array. 

592 

593 Returns a copy unless the optional argument copy is false. 

594 """ 

595 if copy: 

596 return self.arrays[name].copy() 

597 else: 

598 return self.arrays[name] 

599 

600 def set_array(self, name, a, dtype=None, shape=None): 

601 """Update array. 

602 

603 If *shape* is not *None*, the shape of *a* will be checked. 

604 If *a* is *None*, then the array is deleted.""" 

605 

606 b = self.arrays.get(name) 

607 if b is None: 

608 if a is not None: 

609 self.new_array(name, a, dtype, shape) 

610 else: 

611 if a is None: 

612 del self.arrays[name] 

613 else: 

614 a = np.asarray(a) 

615 if a.shape != b.shape: 

616 raise ValueError( 

617 f'Array "{name}" has wrong shape ' 

618 f'{a.shape} != {b.shape}.') 

619 b[:] = a 

620 

621 def has(self, name): 

622 """Check for existence of array. 

623 

624 name must be one of: 'tags', 'momenta', 'masses', 'initial_magmoms', 

625 'initial_charges'.""" 

626 # XXX extend has to calculator properties 

627 return name in self.arrays 

628 

629 def get_chemical_formula(self, mode='hill', empirical=False): 

630 """Get the chemical formula as a string based on the chemical symbols. 

631 

632 Parameters: 

633 

634 mode: str 

635 There are four different modes available: 

636 

637 'all': The list of chemical symbols are contracted to a string, 

638 e.g. ['C', 'H', 'H', 'H', 'O', 'H'] becomes 'CHHHOH'. 

639 

640 'reduce': The same as 'all' where repeated elements are contracted 

641 to a single symbol and a number, e.g. 'CHHHOCHHH' is reduced to 

642 'CH3OCH3'. 

643 

644 'hill': The list of chemical symbols are contracted to a string 

645 following the Hill notation (alphabetical order with C and H 

646 first), e.g. 'CHHHOCHHH' is reduced to 'C2H6O' and 'SOOHOHO' to 

647 'H2O4S'. This is default. 

648 

649 'metal': The list of chemical symbols (alphabetical metals, 

650 and alphabetical non-metals) 

651 

652 empirical, bool (optional, default=False) 

653 Divide the symbol counts by their greatest common divisor to yield 

654 an empirical formula. Only for mode `metal` and `hill`. 

655 """ 

656 return self.symbols.get_chemical_formula(mode, empirical) 

657 

658 def set_tags(self, tags): 

659 """Set tags for all atoms. If only one tag is supplied, it is 

660 applied to all atoms.""" 

661 if isinstance(tags, int): 

662 tags = [tags] * len(self) 

663 self.set_array('tags', tags, int, ()) 

664 

665 def get_tags(self): 

666 """Get integer array of tags.""" 

667 if 'tags' in self.arrays: 

668 return self.arrays['tags'].copy() 

669 else: 

670 return np.zeros(len(self), int) 

671 

672 def set_momenta(self, momenta, apply_constraint=True): 

673 """Set momenta.""" 

674 if (apply_constraint and len(self.constraints) > 0 and 

675 momenta is not None): 

676 momenta = np.array(momenta) # modify a copy 

677 for constraint in self.constraints: 

678 if hasattr(constraint, 'adjust_momenta'): 

679 constraint.adjust_momenta(self, momenta) 

680 self.set_array('momenta', momenta, float, (3,)) 

681 

682 def get_momenta(self): 

683 """Get array of momenta.""" 

684 if 'momenta' in self.arrays: 

685 return self.arrays['momenta'].copy() 

686 else: 

687 return np.zeros((len(self), 3)) 

688 

689 def get_velocities(self): 

690 """Get array of velocities.""" 

691 momenta = self.get_momenta() 

692 masses = self.get_masses() 

693 return momenta / masses[:, np.newaxis] 

694 

695 def set_velocities(self, velocities): 

696 """Set the momenta by specifying the velocities.""" 

697 self.set_momenta(self.get_masses()[:, np.newaxis] * velocities) 

698 

699 def set_masses(self, masses='defaults'): 

700 """Set atomic masses in atomic mass units. 

701 

702 The array masses should contain a list of masses. In case 

703 the masses argument is not given or for those elements of the 

704 masses list that are None, standard values are set.""" 

705 

706 if isinstance(masses, str): 

707 if masses == 'defaults': 

708 masses = atomic_masses[self.arrays['numbers']] 

709 elif masses == 'most_common': 

710 masses = atomic_masses_common[self.arrays['numbers']] 

711 elif masses is None: 

712 pass 

713 elif not isinstance(masses, np.ndarray): 

714 masses = list(masses) 

715 for i, mass in enumerate(masses): 

716 if mass is None: 

717 masses[i] = atomic_masses[self.numbers[i]] 

718 self.set_array('masses', masses, float, ()) 

719 

720 def get_masses(self) -> np.ndarray: 

721 """Get masses of atoms. 

722 

723 Returns 

724 ------- 

725 masses : np.ndarray 

726 Atomic masses in dalton (unified atomic mass units). 

727 

728 Examples 

729 -------- 

730 >>> from ase.build import molecule 

731 >>> atoms = molecule('CH4') 

732 >>> atoms.get_masses() 

733 array([ 12.011, 1.008, 1.008, 1.008, 1.008]) 

734 >>> total_mass = atoms.get_masses().sum() 

735 >>> print(f'{total_mass:f}') 

736 16.043000 

737 """ 

738 if 'masses' in self.arrays: 

739 return self.arrays['masses'].copy() 

740 return atomic_masses[self.arrays['numbers']] 

741 

742 def set_initial_magnetic_moments(self, magmoms=None): 

743 """Set the initial magnetic moments. 

744 

745 Use either one or three numbers for every atom (collinear 

746 or non-collinear spins).""" 

747 

748 if magmoms is None: 

749 self.set_array('initial_magmoms', None) 

750 else: 

751 magmoms = np.asarray(magmoms) 

752 self.set_array('initial_magmoms', magmoms, float, 

753 magmoms.shape[1:]) 

754 

755 def get_initial_magnetic_moments(self): 

756 """Get array of initial magnetic moments.""" 

757 if 'initial_magmoms' in self.arrays: 

758 return self.arrays['initial_magmoms'].copy() 

759 else: 

760 return np.zeros(len(self)) 

761 

762 def get_magnetic_moments(self): 

763 """Get calculated local magnetic moments.""" 

764 if self._calc is None: 

765 raise RuntimeError('Atoms object has no calculator.') 

766 return self._calc.get_magnetic_moments(self) 

767 

768 def get_magnetic_moment(self): 

769 """Get calculated total magnetic moment.""" 

770 if self._calc is None: 

771 raise RuntimeError('Atoms object has no calculator.') 

772 return self._calc.get_magnetic_moment(self) 

773 

774 def set_initial_charges(self, charges=None): 

775 """Set the initial charges.""" 

776 

777 if charges is None: 

778 self.set_array('initial_charges', None) 

779 else: 

780 self.set_array('initial_charges', charges, float, ()) 

781 

782 def get_initial_charges(self): 

783 """Get array of initial charges.""" 

784 if 'initial_charges' in self.arrays: 

785 return self.arrays['initial_charges'].copy() 

786 else: 

787 return np.zeros(len(self)) 

788 

789 def get_charges(self): 

790 """Get calculated charges.""" 

791 if self._calc is None: 

792 raise RuntimeError('Atoms object has no calculator.') 

793 try: 

794 return self._calc.get_charges(self) 

795 except AttributeError: 

796 from ase.calculators.calculator import PropertyNotImplementedError 

797 raise PropertyNotImplementedError 

798 

799 def get_potential_energy(self, force_consistent=False, 

800 apply_constraint=True): 

801 """Calculate potential energy. 

802 

803 Ask the attached calculator to calculate the potential energy and 

804 apply constraints. Use *apply_constraint=False* to get the raw 

805 forces. 

806 

807 When supported by the calculator, either the energy extrapolated 

808 to zero Kelvin or the energy consistent with the forces (the free 

809 energy) can be returned. 

810 """ 

811 if self._calc is None: 

812 raise RuntimeError('Atoms object has no calculator.') 

813 if force_consistent: 

814 energy = self._calc.get_potential_energy( 

815 self, force_consistent=force_consistent) 

816 else: 

817 energy = self._calc.get_potential_energy(self) 

818 if apply_constraint: 

819 for constraint in self.constraints: 

820 if hasattr(constraint, 'adjust_potential_energy'): 

821 energy += constraint.adjust_potential_energy(self) 

822 return energy 

823 

824 def get_properties(self, properties): 

825 """This method is experimental; currently for internal use.""" 

826 # XXX Something about constraints. 

827 if self._calc is None: 

828 raise RuntimeError('Atoms object has no calculator.') 

829 return self._calc.calculate_properties(self, properties) 

830 

831 def get_potential_energies(self): 

832 """Calculate the potential energies of all the atoms. 

833 

834 Only available with calculators supporting per-atom energies 

835 (e.g. classical potentials). 

836 """ 

837 if self._calc is None: 

838 raise RuntimeError('Atoms object has no calculator.') 

839 return self._calc.get_potential_energies(self) 

840 

841 def get_kinetic_energy(self): 

842 """Get the kinetic energy.""" 

843 momenta = self.arrays.get('momenta') 

844 if momenta is None: 

845 return 0.0 

846 return 0.5 * np.vdot(momenta, self.get_velocities()) 

847 

848 def get_total_energy(self): 

849 """Get the total energy - potential plus kinetic energy.""" 

850 return self.get_potential_energy() + self.get_kinetic_energy() 

851 

852 def get_forces(self, apply_constraint=True, md=False): 

853 """Calculate atomic forces. 

854 

855 Ask the attached calculator to calculate the forces and apply 

856 constraints. Use *apply_constraint=False* to get the raw 

857 forces. 

858 

859 For molecular dynamics (md=True) we don't apply the constraint 

860 to the forces but to the momenta. When holonomic constraints for 

861 rigid linear triatomic molecules are present, ask the constraints 

862 to redistribute the forces within each triple defined in the 

863 constraints (required for molecular dynamics with this type of 

864 constraints).""" 

865 

866 if self._calc is None: 

867 raise RuntimeError('Atoms object has no calculator.') 

868 forces = self._calc.get_forces(self) 

869 

870 if apply_constraint: 

871 # We need a special md flag here because for MD we want 

872 # to skip real constraints but include special "constraints" 

873 # Like Hookean. 

874 for constraint in self.constraints: 

875 if md and hasattr(constraint, 'redistribute_forces_md'): 

876 constraint.redistribute_forces_md(self, forces) 

877 if not md or hasattr(constraint, 'adjust_potential_energy'): 

878 constraint.adjust_forces(self, forces) 

879 return forces 

880 

881 # Informs calculators (e.g. Asap) that ideal gas contribution is added here. 

882 _ase_handles_dynamic_stress = True 

883 

884 def get_stress(self, voigt=True, apply_constraint=True, 

885 include_ideal_gas=False): 

886 """Calculate stress tensor. 

887 

888 Returns an array of the six independent components of the 

889 symmetric stress tensor, in the traditional Voigt order 

890 (xx, yy, zz, yz, xz, xy) or as a 3x3 matrix. Default is Voigt 

891 order. 

892 

893 The ideal gas contribution to the stresses is added if the 

894 atoms have momenta and ``include_ideal_gas`` is set to True. 

895 """ 

896 

897 if self._calc is None: 

898 raise RuntimeError('Atoms object has no calculator.') 

899 

900 stress = self._calc.get_stress(self) 

901 shape = stress.shape 

902 

903 if shape == (3, 3): 

904 # Convert to the Voigt form before possibly applying 

905 # constraints and adding the dynamic part of the stress 

906 # (the "ideal gas contribution"). 

907 stress = full_3x3_to_voigt_6_stress(stress) 

908 else: 

909 assert shape == (6,) 

910 

911 if apply_constraint: 

912 for constraint in self.constraints: 

913 if hasattr(constraint, 'adjust_stress'): 

914 constraint.adjust_stress(self, stress) 

915 

916 # Add ideal gas contribution, if applicable 

917 if include_ideal_gas and self.has('momenta'): 

918 stress += self.get_kinetic_stress() 

919 

920 if voigt: 

921 return stress 

922 else: 

923 return voigt_6_to_full_3x3_stress(stress) 

924 

925 def get_stresses(self, include_ideal_gas=False, voigt=True): 

926 """Calculate the stress-tensor of all the atoms. 

927 

928 Only available with calculators supporting per-atom energies and 

929 stresses (e.g. classical potentials). Even for such calculators 

930 there is a certain arbitrariness in defining per-atom stresses. 

931 

932 The ideal gas contribution to the stresses is added if the 

933 atoms have momenta and ``include_ideal_gas`` is set to True. 

934 """ 

935 if self._calc is None: 

936 raise RuntimeError('Atoms object has no calculator.') 

937 stresses = self._calc.get_stresses(self) 

938 

939 # make sure `stresses` are in voigt form 

940 if np.shape(stresses)[1:] == (3, 3): 

941 stresses_voigt = [full_3x3_to_voigt_6_stress(s) for s in stresses] 

942 stresses = np.array(stresses_voigt) 

943 

944 # REMARK: The ideal gas contribution is intensive, i.e., the volume 

945 # is divided out. We currently don't check if `stresses` are intensive 

946 # as well, i.e., if `a.get_stresses.sum(axis=0) == a.get_stress()`. 

947 # It might be good to check this here, but adds computational overhead. 

948 

949 if include_ideal_gas and self.has('momenta'): 

950 stresses += self.get_kinetic_stresses() 

951 

952 if voigt: 

953 return stresses 

954 else: 

955 stresses_3x3 = [voigt_6_to_full_3x3_stress(s) for s in stresses] 

956 return np.array(stresses_3x3) 

957 

958 def get_kinetic_stress(self, voigt=True): 

959 """Calculate the kinetic part of the Virial stress tensor.""" 

960 stress = np.zeros(6) # Voigt notation 

961 stresscomp = np.array([[0, 5, 4], [5, 1, 3], [4, 3, 2]]) 

962 p = self.get_momenta() 

963 masses = self.get_masses() 

964 invmass = 1.0 / masses 

965 invvol = 1.0 / self.get_volume() 

966 for alpha in range(3): 

967 for beta in range(alpha, 3): 

968 stress[stresscomp[alpha, beta]] -= ( 

969 p[:, alpha] * p[:, beta] * invmass).sum() * invvol 

970 

971 if voigt: 

972 return stress 

973 else: 

974 return voigt_6_to_full_3x3_stress(stress) 

975 

976 def get_kinetic_stresses(self, voigt=True): 

977 """Calculate the kinetic part of the Virial stress of all the atoms.""" 

978 stresses = np.zeros((len(self), 6)) # Voigt notation 

979 stresscomp = np.array([[0, 5, 4], [5, 1, 3], [4, 3, 2]]) 

980 if hasattr(self._calc, 'get_atomic_volumes'): 

981 invvol = 1.0 / self._calc.get_atomic_volumes() 

982 else: 

983 invvol = self.get_global_number_of_atoms() / self.get_volume() 

984 p = self.get_momenta() 

985 invmass = 1.0 / self.get_masses() 

986 for alpha in range(3): 

987 for beta in range(alpha, 3): 

988 stresses[:, stresscomp[alpha, beta]] -= ( 

989 p[:, alpha] * p[:, beta] * invmass * invvol) 

990 

991 if voigt: 

992 return stresses 

993 else: 

994 stresses_3x3 = [voigt_6_to_full_3x3_stress(s) for s in stresses] 

995 return np.array(stresses_3x3) 

996 

997 def get_dipole_moment(self): 

998 """Calculate the electric dipole moment for the atoms object. 

999 

1000 Only available for calculators which has a get_dipole_moment() 

1001 method.""" 

1002 

1003 if self._calc is None: 

1004 raise RuntimeError('Atoms object has no calculator.') 

1005 return self._calc.get_dipole_moment(self) 

1006 

1007 def copy(self): 

1008 """Return a copy.""" 

1009 atoms = self.__class__(cell=self.cell, pbc=self.pbc, info=self.info, 

1010 celldisp=self._celldisp.copy()) 

1011 

1012 atoms.arrays = {} 

1013 for name, a in self.arrays.items(): 

1014 atoms.arrays[name] = a.copy() 

1015 atoms.constraints = copy.deepcopy(self.constraints) 

1016 return atoms 

1017 

1018 def todict(self): 

1019 """For basic JSON (non-database) support.""" 

1020 d = dict(self.arrays) 

1021 d['cell'] = np.asarray(self.cell) 

1022 d['pbc'] = self.pbc 

1023 if self._celldisp.any(): 

1024 d['celldisp'] = self._celldisp 

1025 if self.constraints: 

1026 d['constraints'] = self.constraints 

1027 if self.info: 

1028 d['info'] = self.info 

1029 # Calculator... trouble. 

1030 return d 

1031 

1032 @classmethod 

1033 def fromdict(cls, dct): 

1034 """Rebuild atoms object from dictionary representation (todict).""" 

1035 dct = dct.copy() 

1036 kw = {name: dct.pop(name) 

1037 for name in ['numbers', 'positions', 'cell', 'pbc']} 

1038 constraints = dct.pop('constraints', None) 

1039 if constraints: 

1040 from ase.constraints import dict2constraint 

1041 constraints = [dict2constraint(d) for d in constraints] 

1042 

1043 info = dct.pop('info', None) 

1044 

1045 atoms = cls(constraint=constraints, 

1046 celldisp=dct.pop('celldisp', None), 

1047 info=info, **kw) 

1048 natoms = len(atoms) 

1049 

1050 # Some arrays are named differently from the atoms __init__ keywords. 

1051 # Also, there may be custom arrays. Hence we set them directly: 

1052 for name, arr in dct.items(): 

1053 assert len(arr) == natoms, name 

1054 assert isinstance(arr, np.ndarray) 

1055 atoms.arrays[name] = arr 

1056 return atoms 

1057 

1058 def __len__(self): 

1059 return len(self.arrays['positions']) 

1060 

1061 @deprecated( 

1062 "Please use len(self) or, if your atoms are distributed, " 

1063 "self.get_global_number_of_atoms.", 

1064 category=FutureWarning, 

1065 ) 

1066 def get_number_of_atoms(self): 

1067 """ 

1068 .. deprecated:: 3.18.1 

1069 You probably want ``len(atoms)``. Or if your atoms are distributed, 

1070 use (and see) :func:`get_global_number_of_atoms()`. 

1071 """ 

1072 return len(self) 

1073 

1074 def get_global_number_of_atoms(self): 

1075 """Returns the global number of atoms in a distributed-atoms parallel 

1076 simulation. 

1077 

1078 DO NOT USE UNLESS YOU KNOW WHAT YOU ARE DOING! 

1079 

1080 Equivalent to len(atoms) in the standard ASE Atoms class. You should 

1081 normally use len(atoms) instead. This function's only purpose is to 

1082 make compatibility between ASE and Asap easier to maintain by having a 

1083 few places in ASE use this function instead. It is typically only 

1084 when counting the global number of degrees of freedom or in similar 

1085 situations. 

1086 """ 

1087 return len(self) 

1088 

1089 def __repr__(self): 

1090 tokens = [] 

1091 

1092 N = len(self) 

1093 if N <= 60: 

1094 symbols = self.get_chemical_formula('reduce') 

1095 else: 

1096 symbols = self.get_chemical_formula('hill') 

1097 tokens.append(f"symbols='{symbols}'") 

1098 

1099 if self.pbc.any() and not self.pbc.all(): 

1100 tokens.append(f'pbc={self.pbc.tolist()}') 

1101 else: 

1102 tokens.append(f'pbc={self.pbc[0]}') 

1103 

1104 cell = self.cell 

1105 if cell: 

1106 if cell.orthorhombic: 

1107 cell = cell.lengths().tolist() 

1108 else: 

1109 cell = cell.tolist() 

1110 tokens.append(f'cell={cell}') 

1111 

1112 for name in sorted(self.arrays): 

1113 if name in ['numbers', 'positions']: 

1114 continue 

1115 tokens.append(f'{name}=...') 

1116 

1117 if self.constraints: 

1118 if len(self.constraints) == 1: 

1119 constraint = self.constraints[0] 

1120 else: 

1121 constraint = self.constraints 

1122 tokens.append(f'constraint={constraint!r}') 

1123 

1124 if self._calc is not None: 

1125 tokens.append('calculator={}(...)' 

1126 .format(self._calc.__class__.__name__)) 

1127 

1128 return '{}({})'.format(self.__class__.__name__, ', '.join(tokens)) 

1129 

1130 def __add__(self, other): 

1131 atoms = self.copy() 

1132 atoms += other 

1133 return atoms 

1134 

1135 def extend(self, other): 

1136 """Extend atoms object by appending atoms from *other*.""" 

1137 if isinstance(other, Atom): 

1138 other = self.__class__([other]) 

1139 

1140 n1 = len(self) 

1141 n2 = len(other) 

1142 

1143 for name, a1 in self.arrays.items(): 

1144 a = np.zeros((n1 + n2,) + a1.shape[1:], a1.dtype) 

1145 a[:n1] = a1 

1146 if name == 'masses': 

1147 a2 = other.get_masses() 

1148 else: 

1149 a2 = other.arrays.get(name) 

1150 if a2 is not None: 

1151 a[n1:] = a2 

1152 self.arrays[name] = a 

1153 

1154 for name, a2 in other.arrays.items(): 

1155 if name in self.arrays: 

1156 continue 

1157 a = np.empty((n1 + n2,) + a2.shape[1:], a2.dtype) 

1158 a[n1:] = a2 

1159 if name == 'masses': 

1160 a[:n1] = self.get_masses()[:n1] 

1161 else: 

1162 a[:n1] = 0 

1163 

1164 self.set_array(name, a) 

1165 

1166 def __iadd__(self, other): 

1167 self.extend(other) 

1168 return self 

1169 

1170 def append(self, atom): 

1171 """Append atom to end.""" 

1172 self.extend(self.__class__([atom])) 

1173 

1174 def __iter__(self): 

1175 for i in range(len(self)): 

1176 yield self[i] 

1177 

1178 @overload 

1179 def __getitem__(self, i: Union[int, np.integer]) -> Atom: ... 

1180 

1181 @overload 

1182 def __getitem__(self, i: Union[Sequence, slice, np.ndarray]) -> Atoms: ... 

1183 

1184 def __getitem__(self, i): 

1185 """Return a subset of the atoms. 

1186 

1187 i -- scalar integer, list of integers, or slice object 

1188 describing which atoms to return. 

1189 

1190 If i is a scalar, return an Atom object. If i is a list or a 

1191 slice, return an Atoms object with the same cell, pbc, and 

1192 other associated info as the original Atoms object. The 

1193 indices of the constraints will be shuffled so that they match 

1194 the indexing in the subset returned. 

1195 

1196 """ 

1197 

1198 if isinstance(i, numbers.Integral): 

1199 natoms = len(self) 

1200 if i < -natoms or i >= natoms: 

1201 raise IndexError('Index out of range.') 

1202 

1203 return Atom(atoms=self, index=i) 

1204 elif not isinstance(i, slice): 

1205 i = np.array(i) 

1206 if len(i) == 0: 

1207 i = np.array([], dtype=int) 

1208 # if i is a mask 

1209 if i.dtype == bool: 

1210 if len(i) != len(self): 

1211 raise IndexError('Length of mask {} must equal ' 

1212 'number of atoms {}' 

1213 .format(len(i), len(self))) 

1214 i = np.arange(len(self))[i] 

1215 

1216 import copy 

1217 

1218 conadd = [] 

1219 # Constraints need to be deepcopied, but only the relevant ones. 

1220 for con in copy.deepcopy(self.constraints): 

1221 try: 

1222 con.index_shuffle(self, i) 

1223 except (IndexError, NotImplementedError): 

1224 pass 

1225 else: 

1226 conadd.append(con) 

1227 

1228 atoms = self.__class__(cell=self.cell, pbc=self.pbc, info=self.info, 

1229 # should be communicated to the slice as well 

1230 celldisp=self._celldisp) 

1231 # TODO: Do we need to shuffle indices in adsorbate_info too? 

1232 

1233 atoms.arrays = {} 

1234 for name, a in self.arrays.items(): 

1235 atoms.arrays[name] = a[i].copy() 

1236 

1237 atoms.constraints = conadd 

1238 return atoms 

1239 

1240 def __delitem__(self, i): 

1241 from ase.constraints import FixAtoms 

1242 for c in self._constraints: 

1243 if not isinstance(c, FixAtoms): 

1244 raise RuntimeError('Remove constraint using set_constraint() ' 

1245 'before deleting atoms.') 

1246 

1247 if isinstance(i, list) and len(i) > 0: 

1248 # Make sure a list of booleans will work correctly and not be 

1249 # interpreted at 0 and 1 indices. 

1250 i = np.array(i) 

1251 

1252 if len(self._constraints) > 0: 

1253 n = len(self) 

1254 i = np.arange(n)[i] 

1255 if isinstance(i, int): 

1256 i = [i] 

1257 constraints = [] 

1258 for c in self._constraints: 

1259 c = c.delete_atoms(i, n) 

1260 if c is not None: 

1261 constraints.append(c) 

1262 self.constraints = constraints 

1263 

1264 mask = np.ones(len(self), bool) 

1265 mask[i] = False 

1266 for name, a in self.arrays.items(): 

1267 self.arrays[name] = a[mask] 

1268 

1269 def pop(self, i=-1): 

1270 """Remove and return atom at index *i* (default last).""" 

1271 atom = self[i] 

1272 atom.cut_reference_to_atoms() 

1273 del self[i] 

1274 return atom 

1275 

1276 def __imul__(self, m): 

1277 """In-place repeat of atoms.""" 

1278 if isinstance(m, int): 

1279 m = (m, m, m) 

1280 

1281 for x, vec in zip(m, self.cell): 

1282 if x != 1 and not vec.any(): 

1283 raise ValueError('Cannot repeat along undefined lattice ' 

1284 'vector') 

1285 

1286 M = np.prod(m) 

1287 n = len(self) 

1288 

1289 for name, a in self.arrays.items(): 

1290 self.arrays[name] = np.tile(a, (M,) + (1,) * (len(a.shape) - 1)) 

1291 

1292 positions = self.arrays['positions'] 

1293 i0 = 0 

1294 for m0 in range(m[0]): 

1295 for m1 in range(m[1]): 

1296 for m2 in range(m[2]): 

1297 i1 = i0 + n 

1298 positions[i0:i1] += np.dot((m0, m1, m2), self.cell) 

1299 i0 = i1 

1300 

1301 if self.constraints is not None: 

1302 self.constraints = [c.repeat(m, n) for c in self.constraints] 

1303 

1304 self.cell = np.array([m[c] * self.cell[c] for c in range(3)]) 

1305 

1306 return self 

1307 

1308 def repeat(self, rep): 

1309 """Create new repeated atoms object. 

1310 

1311 The *rep* argument should be a sequence of three positive 

1312 integers like *(2,3,1)* or a single integer (*r*) equivalent 

1313 to *(r,r,r)*.""" 

1314 

1315 atoms = self.copy() 

1316 atoms *= rep 

1317 return atoms 

1318 

1319 def __mul__(self, rep): 

1320 return self.repeat(rep) 

1321 

1322 def translate(self, displacement): 

1323 """Translate atomic positions. 

1324 

1325 The displacement argument can be a float an xyz vector or an 

1326 nx3 array (where n is the number of atoms).""" 

1327 

1328 self.arrays['positions'] += np.array(displacement) 

1329 

1330 def center(self, vacuum=None, axis=(0, 1, 2), about=None): 

1331 """Center atoms in unit cell. 

1332 

1333 Centers the atoms in the unit cell, so there is the same 

1334 amount of vacuum on all sides. 

1335 

1336 vacuum: float (default: None) 

1337 If specified adjust the amount of vacuum when centering. 

1338 If vacuum=10.0 there will thus be 10 Angstrom of vacuum 

1339 on each side. 

1340 axis: int or sequence of ints 

1341 Axis or axes to act on. Default: Act on all axes. 

1342 about: float or array (default: None) 

1343 If specified, center the atoms about <about>. 

1344 I.e., about=(0., 0., 0.) (or just "about=0.", interpreted 

1345 identically), to center about the origin. 

1346 """ 

1347 

1348 # Find the orientations of the faces of the unit cell 

1349 cell = self.cell.complete() 

1350 dirs = np.zeros_like(cell) 

1351 

1352 lengths = cell.lengths() 

1353 for i in range(3): 

1354 dirs[i] = np.cross(cell[i - 1], cell[i - 2]) 

1355 dirs[i] /= np.linalg.norm(dirs[i]) 

1356 if dirs[i] @ cell[i] < 0.0: 

1357 dirs[i] *= -1 

1358 

1359 if isinstance(axis, int): 

1360 axes = (axis,) 

1361 else: 

1362 axes = axis 

1363 

1364 # Now, decide how much each basis vector should be made longer 

1365 pos = self.positions 

1366 longer = np.zeros(3) 

1367 shift = np.zeros(3) 

1368 for i in axes: 

1369 if len(pos): 

1370 scalarprod = pos @ dirs[i] 

1371 p0 = scalarprod.min() 

1372 p1 = scalarprod.max() 

1373 else: 

1374 p0 = 0 

1375 p1 = 0 

1376 height = cell[i] @ dirs[i] 

1377 if vacuum is not None: 

1378 lng = (p1 - p0 + 2 * vacuum) - height 

1379 else: 

1380 lng = 0.0 # Do not change unit cell size! 

1381 top = lng + height - p1 

1382 shf = 0.5 * (top - p0) 

1383 cosphi = cell[i] @ dirs[i] / lengths[i] 

1384 longer[i] = lng / cosphi 

1385 shift[i] = shf / cosphi 

1386 

1387 # Now, do it! 

1388 translation = np.zeros(3) 

1389 for i in axes: 

1390 nowlen = lengths[i] 

1391 if vacuum is not None: 

1392 self.cell[i] = cell[i] * (1 + longer[i] / nowlen) 

1393 translation += shift[i] * cell[i] / nowlen 

1394 

1395 # We calculated translations using the completed cell, 

1396 # so directions without cell vectors will have been centered 

1397 # along a "fake" vector of length 1. 

1398 # Therefore, we adjust by -0.5: 

1399 if not any(self.cell[i]): 

1400 translation[i] -= 0.5 

1401 

1402 # Optionally, translate to center about a point in space. 

1403 if about is not None: 

1404 for n, vector in enumerate(self.cell): 

1405 if n in axes: 

1406 translation -= vector / 2.0 

1407 translation[n] += about[n] 

1408 

1409 self.positions += translation 

1410 

1411 def get_center_of_mass(self, scaled=False, indices=None): 

1412 """Get the center of mass. 

1413 

1414 Parameters 

1415 ---------- 

1416 scaled : bool 

1417 If True, the center of mass in scaled coordinates is returned. 

1418 indices : list | slice | str, default: None 

1419 If specified, the center of mass of a subset of atoms is returned. 

1420 """ 

1421 if indices is None: 

1422 indices = slice(None) 

1423 elif isinstance(indices, str): 

1424 indices = string2index(indices) 

1425 

1426 masses = self.get_masses()[indices] 

1427 com = masses @ self.positions[indices] / masses.sum() 

1428 if scaled: 

1429 return self.cell.scaled_positions(com) 

1430 return com # Cartesian coordinates 

1431 

1432 def set_center_of_mass(self, com, scaled=False): 

1433 """Set the center of mass. 

1434 

1435 If scaled=True the center of mass is expected in scaled coordinates. 

1436 Constraints are considered for scaled=False. 

1437 """ 

1438 old_com = self.get_center_of_mass(scaled=scaled) 

1439 difference = com - old_com 

1440 if scaled: 

1441 self.set_scaled_positions(self.get_scaled_positions() + difference) 

1442 else: 

1443 self.set_positions(self.get_positions() + difference) 

1444 

1445 def get_moments_of_inertia(self, vectors=False): 

1446 """Get the moments of inertia along the principal axes. 

1447 

1448 The three principal moments of inertia are computed from the 

1449 eigenvalues of the symmetric inertial tensor. Periodic boundary 

1450 conditions are ignored. Units of the moments of inertia are 

1451 amu*angstrom**2. 

1452 """ 

1453 com = self.get_center_of_mass() 

1454 positions = self.get_positions() 

1455 positions -= com # translate center of mass to origin 

1456 masses = self.get_masses() 

1457 

1458 # Initialize elements of the inertial tensor 

1459 I11 = I22 = I33 = I12 = I13 = I23 = 0.0 

1460 for i in range(len(self)): 

1461 x, y, z = positions[i] 

1462 m = masses[i] 

1463 

1464 I11 += m * (y ** 2 + z ** 2) 

1465 I22 += m * (x ** 2 + z ** 2) 

1466 I33 += m * (x ** 2 + y ** 2) 

1467 I12 += -m * x * y 

1468 I13 += -m * x * z 

1469 I23 += -m * y * z 

1470 

1471 Itensor = np.array([[I11, I12, I13], 

1472 [I12, I22, I23], 

1473 [I13, I23, I33]]) 

1474 

1475 evals, evecs = np.linalg.eigh(Itensor) 

1476 if vectors: 

1477 return evals, evecs.transpose() 

1478 else: 

1479 return evals 

1480 

1481 def get_angular_momentum(self): 

1482 """Get total angular momentum with respect to the center of mass.""" 

1483 com = self.get_center_of_mass() 

1484 positions = self.get_positions() 

1485 positions -= com # translate center of mass to origin 

1486 return np.cross(positions, self.get_momenta()).sum(0) 

1487 

1488 def rotate(self, a, v, center=(0, 0, 0), rotate_cell=False): 

1489 """Rotate atoms based on a vector and an angle, or two vectors. 

1490 

1491 Parameters: 

1492 

1493 a = None: 

1494 Angle that the atoms is rotated around the vector 'v'. 'a' 

1495 can also be a vector and then 'a' is rotated 

1496 into 'v'. 

1497 

1498 v: 

1499 Vector to rotate the atoms around. Vectors can be given as 

1500 strings: 'x', '-x', 'y', ... . 

1501 

1502 center = (0, 0, 0): 

1503 The center is kept fixed under the rotation. Use 'COM' to fix 

1504 the center of mass, 'COP' to fix the center of positions or 

1505 'COU' to fix the center of cell. 

1506 

1507 rotate_cell = False: 

1508 If true the cell is also rotated. 

1509 

1510 Examples: 

1511 

1512 Rotate 90 degrees around the z-axis, so that the x-axis is 

1513 rotated into the y-axis: 

1514 

1515 >>> atoms = Atoms() 

1516 >>> atoms.rotate(90, 'z') 

1517 >>> atoms.rotate(90, (0, 0, 1)) 

1518 >>> atoms.rotate(-90, '-z') 

1519 >>> atoms.rotate('x', 'y') 

1520 >>> atoms.rotate((1, 0, 0), (0, 1, 0)) 

1521 """ 

1522 

1523 if not isinstance(a, numbers.Real): 

1524 a, v = v, a 

1525 

1526 norm = np.linalg.norm 

1527 v = string2vector(v) 

1528 

1529 normv = norm(v) 

1530 

1531 if normv == 0.0: 

1532 raise ZeroDivisionError('Cannot rotate: norm(v) == 0') 

1533 

1534 if isinstance(a, numbers.Real): 

1535 a *= pi / 180 

1536 v /= normv 

1537 c = cos(a) 

1538 s = sin(a) 

1539 else: 

1540 v2 = string2vector(a) 

1541 v /= normv 

1542 normv2 = np.linalg.norm(v2) 

1543 if normv2 == 0: 

1544 raise ZeroDivisionError('Cannot rotate: norm(a) == 0') 

1545 v2 /= norm(v2) 

1546 c = np.dot(v, v2) 

1547 v = np.cross(v, v2) 

1548 s = norm(v) 

1549 # In case *v* and *a* are parallel, np.cross(v, v2) vanish 

1550 # and can't be used as a rotation axis. However, in this 

1551 # case any rotation axis perpendicular to v2 will do. 

1552 eps = 1e-7 

1553 if s < eps: 

1554 v = np.cross((0, 0, 1), v2) 

1555 if norm(v) < eps: 

1556 v = np.cross((1, 0, 0), v2) 

1557 assert norm(v) >= eps 

1558 elif s > 0: 

1559 v /= s 

1560 

1561 center = self._centering_as_array(center) 

1562 

1563 p = self.arrays['positions'] - center 

1564 self.arrays['positions'][:] = (c * p - 

1565 np.cross(p, s * v) + 

1566 np.outer(np.dot(p, v), (1.0 - c) * v) + 

1567 center) 

1568 if rotate_cell: 

1569 rotcell = self.get_cell() 

1570 rotcell[:] = (c * rotcell - 

1571 np.cross(rotcell, s * v) + 

1572 np.outer(np.dot(rotcell, v), (1.0 - c) * v)) 

1573 self.set_cell(rotcell) 

1574 

1575 def _centering_as_array(self, center): 

1576 if isinstance(center, str): 

1577 if center.lower() == 'com': 

1578 center = self.get_center_of_mass() 

1579 elif center.lower() == 'cop': 

1580 center = self.get_positions().mean(axis=0) 

1581 elif center.lower() == 'cou': 

1582 center = self.get_cell().sum(axis=0) / 2 

1583 else: 

1584 raise ValueError('Cannot interpret center') 

1585 else: 

1586 center = np.array(center, float) 

1587 return center 

1588 

1589 def euler_rotate( 

1590 self, 

1591 phi: float = 0.0, 

1592 theta: float = 0.0, 

1593 psi: float = 0.0, 

1594 center: Sequence[float] = (0.0, 0.0, 0.0), 

1595 ) -> None: 

1596 """Rotate atoms via Euler angles (in degrees). 

1597 

1598 See e.g http://mathworld.wolfram.com/EulerAngles.html for explanation. 

1599 

1600 Note that the rotations in this method are passive and applied **not** 

1601 to the atomic coordinates in the present frame **but** the frame itself. 

1602 

1603 Parameters 

1604 ---------- 

1605 phi : float 

1606 The 1st rotation angle around the z axis. 

1607 theta : float 

1608 Rotation around the x axis. 

1609 psi : float 

1610 2nd rotation around the z axis. 

1611 center : Sequence[float], default = (0.0, 0.0, 0.0) 

1612 The point to rotate about. A sequence of length 3 with the 

1613 coordinates, or 'COM' to select the center of mass, 'COP' to 

1614 select center of positions or 'COU' to select center of cell. 

1615 

1616 """ 

1617 from scipy.spatial.transform import Rotation as R 

1618 

1619 center = self._centering_as_array(center) 

1620 

1621 # passive rotations (negative angles) for backward compatibility 

1622 rotation = R.from_euler('zxz', (-phi, -theta, -psi), degrees=True) 

1623 

1624 self.positions = rotation.apply(self.positions - center) + center 

1625 

1626 def get_dihedral(self, a0, a1, a2, a3, mic=False): 

1627 """Calculate dihedral angle. 

1628 

1629 Calculate dihedral angle (in degrees) between the vectors a0->a1 

1630 and a2->a3. 

1631 

1632 Use mic=True to use the Minimum Image Convention and calculate the 

1633 angle across periodic boundaries. 

1634 """ 

1635 return self.get_dihedrals([[a0, a1, a2, a3]], mic=mic)[0] 

1636 

1637 def get_dihedrals(self, indices, mic=False): 

1638 """Calculate dihedral angles. 

1639 

1640 Calculate dihedral angles (in degrees) between the list of vectors 

1641 a0->a1 and a2->a3, where a0, a1, a2 and a3 are in each row of indices. 

1642 

1643 Use mic=True to use the Minimum Image Convention and calculate the 

1644 angles across periodic boundaries. 

1645 """ 

1646 from ase.geometry import get_dihedrals 

1647 

1648 indices = np.array(indices) 

1649 assert indices.shape[1] == 4 

1650 

1651 a0s = self.positions[indices[:, 0]] 

1652 a1s = self.positions[indices[:, 1]] 

1653 a2s = self.positions[indices[:, 2]] 

1654 a3s = self.positions[indices[:, 3]] 

1655 

1656 # vectors 0->1, 1->2, 2->3 

1657 v0 = a1s - a0s 

1658 v1 = a2s - a1s 

1659 v2 = a3s - a2s 

1660 

1661 cell = None 

1662 pbc = None 

1663 

1664 if mic: 

1665 cell = self.cell 

1666 pbc = self.pbc 

1667 

1668 return get_dihedrals(v0, v1, v2, cell=cell, pbc=pbc) 

1669 

1670 def _masked_rotate(self, center, axis, diff, mask): 

1671 # do rotation of subgroup by copying it to temporary atoms object 

1672 # and then rotating that 

1673 # 

1674 # recursive object definition might not be the most elegant thing, 

1675 # more generally useful might be a rotation function with a mask? 

1676 group = self.__class__() 

1677 for i in range(len(self)): 

1678 if mask[i]: 

1679 group += self[i] 

1680 group.translate(-center) 

1681 group.rotate(diff * 180 / pi, axis) 

1682 group.translate(center) 

1683 # set positions in original atoms object 

1684 j = 0 

1685 for i in range(len(self)): 

1686 if mask[i]: 

1687 self.positions[i] = group[j].position 

1688 j += 1 

1689 

1690 def set_dihedral(self, a1, a2, a3, a4, angle, 

1691 mask=None, indices=None): 

1692 """Set the dihedral angle (degrees) between vectors a1->a2 and 

1693 a3->a4 by changing the atom indexed by a4. 

1694 

1695 If mask is not None, all the atoms described in mask 

1696 (read: the entire subgroup) are moved. Alternatively to the mask, 

1697 the indices of the atoms to be rotated can be supplied. If both 

1698 *mask* and *indices* are given, *indices* overwrites *mask*. 

1699 

1700 **Important**: If *mask* or *indices* is given and does not contain 

1701 *a4*, *a4* will NOT be moved. In most cases you therefore want 

1702 to include *a4* in *mask*/*indices*. 

1703 

1704 Example: the following defines a very crude 

1705 ethane-like molecule and twists one half of it by 30 degrees. 

1706 

1707 >>> atoms = Atoms('HHCCHH', [[-1, 1, 0], [-1, -1, 0], [0, 0, 0], 

1708 ... [1, 0, 0], [2, 1, 0], [2, -1, 0]]) 

1709 >>> atoms.set_dihedral(1, 2, 3, 4, 210, mask=[0, 0, 0, 1, 1, 1]) 

1710 """ 

1711 

1712 angle *= pi / 180 

1713 

1714 # if not provided, set mask to the last atom in the 

1715 # dihedral description 

1716 if mask is None and indices is None: 

1717 mask = np.zeros(len(self)) 

1718 mask[a4] = 1 

1719 elif indices is not None: 

1720 mask = [index in indices for index in range(len(self))] 

1721 

1722 # compute necessary in dihedral change, from current value 

1723 current = self.get_dihedral(a1, a2, a3, a4) * pi / 180 

1724 diff = angle - current 

1725 axis = self.positions[a3] - self.positions[a2] 

1726 center = self.positions[a3] 

1727 self._masked_rotate(center, axis, diff, mask) 

1728 

1729 def rotate_dihedral(self, a1, a2, a3, a4, angle, mask=None, indices=None): 

1730 """Rotate dihedral angle. 

1731 

1732 Same usage as in :meth:`ase.Atoms.set_dihedral`: Rotate a group by a 

1733 predefined dihedral angle, starting from its current configuration. 

1734 """ 

1735 start = self.get_dihedral(a1, a2, a3, a4) 

1736 self.set_dihedral(a1, a2, a3, a4, angle + start, mask, indices) 

1737 

1738 def get_angle(self, a1, a2, a3, mic=False): 

1739 """Get angle formed by three atoms. 

1740 

1741 Calculate angle in degrees between the vectors a2->a1 and 

1742 a2->a3. 

1743 

1744 Use mic=True to use the Minimum Image Convention and calculate the 

1745 angle across periodic boundaries. 

1746 """ 

1747 return self.get_angles([[a1, a2, a3]], mic=mic)[0] 

1748 

1749 def get_angles(self, indices, mic=False): 

1750 """Get angle formed by three atoms for multiple groupings. 

1751 

1752 Calculate angle in degrees between vectors between atoms a2->a1 

1753 and a2->a3, where a1, a2, and a3 are in each row of indices. 

1754 

1755 Use mic=True to use the Minimum Image Convention and calculate 

1756 the angle across periodic boundaries. 

1757 """ 

1758 from ase.geometry import get_angles 

1759 

1760 indices = np.array(indices) 

1761 assert indices.shape[1] == 3 

1762 

1763 a1s = self.positions[indices[:, 0]] 

1764 a2s = self.positions[indices[:, 1]] 

1765 a3s = self.positions[indices[:, 2]] 

1766 

1767 v12 = a1s - a2s 

1768 v32 = a3s - a2s 

1769 

1770 cell = None 

1771 pbc = None 

1772 

1773 if mic: 

1774 cell = self.cell 

1775 pbc = self.pbc 

1776 

1777 return get_angles(v12, v32, cell=cell, pbc=pbc) 

1778 

1779 def set_angle(self, a1, a2=None, a3=None, angle=None, mask=None, 

1780 indices=None, add=False): 

1781 """Set angle (in degrees) formed by three atoms. 

1782 

1783 Sets the angle between vectors *a2*->*a1* and *a2*->*a3*. 

1784 

1785 If *add* is `True`, the angle will be changed by the value given. 

1786 

1787 Same usage as in :meth:`ase.Atoms.set_dihedral`. 

1788 If *mask* and *indices* 

1789 are given, *indices* overwrites *mask*. If *mask* and *indices* 

1790 are not set, only *a3* is moved.""" 

1791 

1792 if any(a is None for a in [a2, a3, angle]): 

1793 raise ValueError('a2, a3, and angle must not be None') 

1794 

1795 # If not provided, set mask to the last atom in the angle description 

1796 if mask is None and indices is None: 

1797 mask = np.zeros(len(self)) 

1798 mask[a3] = 1 

1799 elif indices is not None: 

1800 mask = [index in indices for index in range(len(self))] 

1801 

1802 if add: 

1803 diff = angle 

1804 else: 

1805 # Compute necessary in angle change, from current value 

1806 diff = angle - self.get_angle(a1, a2, a3) 

1807 

1808 diff *= pi / 180 

1809 # Do rotation of subgroup by copying it to temporary atoms object and 

1810 # then rotating that 

1811 v10 = self.positions[a1] - self.positions[a2] 

1812 v12 = self.positions[a3] - self.positions[a2] 

1813 v10 /= np.linalg.norm(v10) 

1814 v12 /= np.linalg.norm(v12) 

1815 axis = np.cross(v10, v12) 

1816 center = self.positions[a2] 

1817 self._masked_rotate(center, axis, diff, mask) 

1818 

1819 def rattle(self, stdev=0.001, seed=None, rng=None): 

1820 """Randomly displace atoms. 

1821 

1822 This method adds random displacements to the atomic positions, 

1823 taking a possible constraint into account. The random numbers are 

1824 drawn from a normal distribution of standard deviation stdev. 

1825 

1826 By default, the random number generator always uses the same seed (42) 

1827 for repeatability. You can provide your own seed (an integer), or if you 

1828 want the randomness to be different each time you run a script, then 

1829 provide `rng=numpy.random`. For a parallel calculation, it is important 

1830 to use the same seed on all processors! """ 

1831 

1832 if seed is not None and rng is not None: 

1833 raise ValueError('Please do not provide both seed and rng.') 

1834 

1835 if rng is None: 

1836 if seed is None: 

1837 seed = 42 

1838 rng = np.random.RandomState(seed) 

1839 positions = self.arrays['positions'] 

1840 self.set_positions(positions + 

1841 rng.normal(scale=stdev, size=positions.shape)) 

1842 

1843 def get_distance(self, a0, a1, mic=False, vector=False): 

1844 """Return distance between two atoms. 

1845 

1846 Use mic=True to use the Minimum Image Convention. 

1847 vector=True gives the distance vector (from a0 to a1). 

1848 """ 

1849 return self.get_distances(a0, [a1], mic=mic, vector=vector)[0] 

1850 

1851 def get_distances(self, a, indices, mic=False, vector=False): 

1852 """Return distances of atom No.i with a list of atoms. 

1853 

1854 Use mic=True to use the Minimum Image Convention. 

1855 vector=True gives the distance vector (from a to self[indices]). 

1856 """ 

1857 from ase.geometry import get_distances 

1858 

1859 R = self.arrays['positions'] 

1860 p1 = [R[a]] 

1861 p2 = R[indices] 

1862 

1863 cell = None 

1864 pbc = None 

1865 

1866 if mic: 

1867 cell = self.cell 

1868 pbc = self.pbc 

1869 

1870 D, D_len = get_distances(p1, p2, cell=cell, pbc=pbc) 

1871 

1872 if vector: 

1873 D.shape = (-1, 3) 

1874 return D 

1875 else: 

1876 D_len.shape = (-1,) 

1877 return D_len 

1878 

1879 def get_all_distances(self, mic=False, vector=False): 

1880 """Return distances of all of the atoms with all of the atoms. 

1881 

1882 Use mic=True to use the Minimum Image Convention. 

1883 """ 

1884 from ase.geometry import get_distances 

1885 

1886 R = self.arrays['positions'] 

1887 

1888 cell = None 

1889 pbc = None 

1890 

1891 if mic: 

1892 cell = self.cell 

1893 pbc = self.pbc 

1894 

1895 D, D_len = get_distances(R, cell=cell, pbc=pbc) 

1896 

1897 if vector: 

1898 return D 

1899 else: 

1900 return D_len 

1901 

1902 def set_distance(self, a0, a1, distance, fix=0.5, mic=False, 

1903 mask=None, indices=None, add=False, factor=False): 

1904 """Set the distance between two atoms. 

1905 

1906 Set the distance between atoms *a0* and *a1* to *distance*. 

1907 By default, the center of the two atoms will be fixed. Use 

1908 *fix=0* to fix the first atom, *fix=1* to fix the second 

1909 atom and *fix=0.5* (default) to fix the center of the bond. 

1910 

1911 If *mask* or *indices* are set (*mask* overwrites *indices*), 

1912 only the atoms defined there are moved 

1913 (see :meth:`ase.Atoms.set_dihedral`). 

1914 

1915 When *add* is true, the distance is changed by the value given. 

1916 In combination 

1917 with *factor* True, the value given is a factor scaling the distance. 

1918 

1919 It is assumed that the atoms in *mask*/*indices* move together 

1920 with *a1*. If *fix=1*, only *a0* will therefore be moved.""" 

1921 from ase.geometry import find_mic 

1922 

1923 if a0 % len(self) == a1 % len(self): 

1924 raise ValueError('a0 and a1 must not be the same') 

1925 

1926 if add: 

1927 oldDist = self.get_distance(a0, a1, mic=mic) 

1928 if factor: 

1929 newDist = oldDist * distance 

1930 else: 

1931 newDist = oldDist + distance 

1932 self.set_distance(a0, a1, newDist, fix=fix, mic=mic, 

1933 mask=mask, indices=indices, add=False, 

1934 factor=False) 

1935 return 

1936 

1937 R = self.arrays['positions'] 

1938 D = np.array([R[a1] - R[a0]]) 

1939 

1940 if mic: 

1941 D, D_len = find_mic(D, self.cell, self.pbc) 

1942 else: 

1943 D_len = np.array([np.sqrt((D**2).sum())]) 

1944 x = 1.0 - distance / D_len[0] 

1945 

1946 if mask is None and indices is None: 

1947 indices = [a0, a1] 

1948 elif mask: 

1949 indices = [i for i in range(len(self)) if mask[i]] 

1950 

1951 for i in indices: 

1952 if i == a0: 

1953 R[a0] += (x * fix) * D[0] 

1954 else: 

1955 R[i] -= (x * (1.0 - fix)) * D[0] 

1956 

1957 def get_scaled_positions(self, wrap=True): 

1958 """Get positions relative to unit cell. 

1959 

1960 If wrap is True, atoms outside the unit cell will be wrapped into 

1961 the cell in those directions with periodic boundary conditions 

1962 so that the scaled coordinates are between zero and one. 

1963 

1964 If any cell vectors are zero, the corresponding coordinates 

1965 are evaluated as if the cell were completed using 

1966 ``cell.complete()``. This means coordinates will be Cartesian 

1967 as long as the non-zero cell vectors span a Cartesian axis or 

1968 plane.""" 

1969 

1970 fractional = self.cell.scaled_positions(self.positions) 

1971 

1972 if wrap: 

1973 for i, periodic in enumerate(self.pbc): 

1974 if periodic: 

1975 # Yes, we need to do it twice. 

1976 # See the scaled_positions.py test. 

1977 fractional[:, i] %= 1.0 

1978 fractional[:, i] %= 1.0 

1979 

1980 return fractional 

1981 

1982 def set_scaled_positions(self, scaled): 

1983 """Set positions relative to unit cell.""" 

1984 self.positions[:] = self.cell.cartesian_positions(scaled) 

1985 

1986 def wrap(self, **wrap_kw): 

1987 """Wrap positions to unit cell. 

1988 

1989 Parameters: 

1990 

1991 wrap_kw: (keyword=value) pairs 

1992 optional keywords `pbc`, `center`, `pretty_translation`, `eps`, 

1993 see :func:`ase.geometry.wrap_positions` 

1994 """ 

1995 

1996 if 'pbc' not in wrap_kw: 

1997 wrap_kw['pbc'] = self.pbc 

1998 

1999 self.positions[:] = self.get_positions(wrap=True, **wrap_kw) 

2000 

2001 def get_temperature(self): 

2002 """Get the temperature in Kelvin.""" 

2003 ekin = self.get_kinetic_energy() 

2004 return 2 * ekin / (self.get_number_of_degrees_of_freedom() * units.kB) 

2005 

2006 def __eq__(self, other): 

2007 """Check for identity of two atoms objects. 

2008 

2009 Identity means: same positions, atomic numbers, unit cell and 

2010 periodic boundary conditions.""" 

2011 if not isinstance(other, Atoms): 

2012 return False 

2013 a = self.arrays 

2014 b = other.arrays 

2015 return (len(self) == len(other) and 

2016 (a['positions'] == b['positions']).all() and 

2017 (a['numbers'] == b['numbers']).all() and 

2018 (self.cell == other.cell).all() and 

2019 (self.pbc == other.pbc).all()) 

2020 

2021 def __ne__(self, other): 

2022 """Check if two atoms objects are not equal. 

2023 

2024 Any differences in positions, atomic numbers, unit cell or 

2025 periodic boundary condtions make atoms objects not equal. 

2026 """ 

2027 eq = self.__eq__(other) 

2028 if eq is NotImplemented: 

2029 return eq 

2030 else: 

2031 return not eq 

2032 

2033 # @deprecated('Please use atoms.cell.volume') 

2034 # We kind of want to deprecate this, but the ValueError behaviour 

2035 # might be desirable. Should we do this? 

2036 def get_volume(self): 

2037 """Get volume of unit cell.""" 

2038 if self.cell.rank != 3: 

2039 raise ValueError( 

2040 'You have {} lattice vectors: volume not defined' 

2041 .format(self.cell.rank)) 

2042 return self.cell.volume 

2043 

2044 @property 

2045 def cell(self): 

2046 """The :class:`ase.cell.Cell` for direct manipulation.""" 

2047 return self._cellobj 

2048 

2049 @cell.setter 

2050 def cell(self, cell): 

2051 cell = Cell.ascell(cell) 

2052 self._cellobj[:] = cell 

2053 

2054 def write(self, filename, format=None, **kwargs): 

2055 """Write atoms object to a file. 

2056 

2057 see ase.io.write for formats. 

2058 kwargs are passed to ase.io.write. 

2059 """ 

2060 from ase.io import write 

2061 write(filename, self, format, **kwargs) 

2062 

2063 def iterimages(self): 

2064 yield self 

2065 

2066 def __ase_optimizable__(self): 

2067 from ase.optimize.optimize import OptimizableAtoms 

2068 return OptimizableAtoms(self) 

2069 

2070 def edit(self): 

2071 """Modify atoms interactively through ASE's GUI viewer. 

2072 

2073 Conflicts leading to undesirable behaviour might arise 

2074 when matplotlib has been pre-imported with certain 

2075 incompatible backends and while trying to use the 

2076 plot feature inside the interactive GUI. To circumvent, 

2077 please set matplotlib.use('gtk') before calling this 

2078 method. 

2079 """ 

2080 from ase.gui.gui import GUI 

2081 from ase.gui.images import Images 

2082 images = Images([self]) 

2083 gui = GUI(images) 

2084 gui.run() 

2085 

2086 

2087def string2vector(v): 

2088 if isinstance(v, str): 

2089 if v[0] == '-': 

2090 return -string2vector(v[1:]) 

2091 w = np.zeros(3) 

2092 w['xyz'.index(v)] = 1.0 

2093 return w 

2094 return np.array(v, float) 

2095 

2096 

2097def default(data, dflt): 

2098 """Helper function for setting default values.""" 

2099 if data is None: 

2100 return None 

2101 elif isinstance(data, (list, tuple)): 

2102 newdata = [] 

2103 allnone = True 

2104 for x in data: 

2105 if x is None: 

2106 newdata.append(dflt) 

2107 else: 

2108 newdata.append(x) 

2109 allnone = False 

2110 if allnone: 

2111 return None 

2112 return newdata 

2113 else: 

2114 return data