You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2013 lines
69 KiB
2013 lines
69 KiB
# Copyright 2008, 2009 CAMd |
|
# (see accompanying license files for details). |
|
|
|
"""Definition of the Atoms class. |
|
|
|
This module defines the central object in the ASE package: the Atoms |
|
object. |
|
""" |
|
import copy |
|
import numbers |
|
from math import cos, sin, pi |
|
|
|
import numpy as np |
|
|
|
import cmmde_units as units |
|
from cmmde_unit import Atom |
|
from cmmde_cell import Cell |
|
from cmmde_stress import voigt_6_to_full_3x3_stress, full_3x3_to_voigt_6_stress |
|
from ase.data import atomic_masses, atomic_masses_common |
|
from ase.geometry import (wrap_positions, find_mic, get_angles, get_distances, |
|
get_dihedrals) |
|
from ase.symbols import Symbols, symbols2numbers |
|
from ase.utils import deprecated |
|
|
|
|
|
class Atoms: |
|
"""Atoms object. |
|
|
|
The Atoms object can represent an isolated molecule, or a |
|
periodically repeated structure. It has a unit cell and |
|
there may be periodic boundary conditions along any of the three |
|
unit cell axes. |
|
Information about the atoms (atomic numbers and position) is |
|
stored in ndarrays. Optionally, there can be information about |
|
tags, momenta, masses, magnetic moments and charges. |
|
|
|
In order to calculate energies, forces and stresses, a calculator |
|
object has to attached to the atoms object. |
|
|
|
Parameters: |
|
|
|
symbols: str (formula) or list of str |
|
Can be a string formula, a list of symbols or a list of |
|
Atom objects. Examples: 'H2O', 'COPt12', ['H', 'H', 'O'], |
|
[Atom('Ne', (x, y, z)), ...]. |
|
positions: list of xyz-positions |
|
Atomic positions. Anything that can be converted to an |
|
ndarray of shape (n, 3) will do: [(x1,y1,z1), (x2,y2,z2), |
|
...]. |
|
scaled_positions: list of scaled-positions |
|
Like positions, but given in units of the unit cell. |
|
Can not be set at the same time as positions. |
|
numbers: list of int |
|
Atomic numbers (use only one of symbols/numbers). |
|
tags: list of int |
|
Special purpose tags. |
|
momenta: list of xyz-momenta |
|
Momenta for all atoms. |
|
masses: list of float |
|
Atomic masses in atomic units. |
|
magmoms: list of float or list of xyz-values |
|
Magnetic moments. Can be either a single value for each atom |
|
for collinear calculations or three numbers for each atom for |
|
non-collinear calculations. |
|
charges: list of float |
|
Initial atomic charges. |
|
cell: 3x3 matrix or length 3 or 6 vector |
|
Unit cell vectors. Can also be given as just three |
|
numbers for orthorhombic cells, or 6 numbers, where |
|
first three are lengths of unit cell vectors, and the |
|
other three are angles between them (in degrees), in following order: |
|
[len(a), len(b), len(c), angle(b,c), angle(a,c), angle(a,b)]. |
|
First vector will lie in x-direction, second in xy-plane, |
|
and the third one in z-positive subspace. |
|
Default value: [0, 0, 0]. |
|
celldisp: Vector |
|
Unit cell displacement vector. To visualize a displaced cell |
|
around the center of mass of a Systems of atoms. Default value |
|
= (0,0,0) |
|
pbc: one or three bool |
|
Periodic boundary conditions flags. Examples: True, |
|
False, 0, 1, (1, 1, 0), (True, False, False). Default |
|
value: False. |
|
constraint: constraint object(s) |
|
Used for applying one or more constraints during structure |
|
optimization. |
|
calculator: calculator object |
|
Used to attach a calculator for calculating energies and atomic |
|
forces. |
|
info: dict of key-value pairs |
|
Dictionary of key-value pairs with additional information |
|
about the system. The following keys may be used by ase: |
|
|
|
- spacegroup: Spacegroup instance |
|
- unit_cell: 'conventional' | 'primitive' | int | 3 ints |
|
- adsorbate_info: Information about special adsorption sites |
|
|
|
Items in the info attribute survives copy and slicing and can |
|
be stored in and retrieved from trajectory files given that the |
|
key is a string, the value is JSON-compatible and, if the value is a |
|
user-defined object, its base class is importable. One should |
|
not make any assumptions about the existence of keys. |
|
|
|
Examples: |
|
|
|
These three are equivalent: |
|
|
|
>>> d = 1.104 # N2 bondlength |
|
>>> a = Atoms('N2', [(0, 0, 0), (0, 0, d)]) |
|
>>> a = Atoms(numbers=[7, 7], positions=[(0, 0, 0), (0, 0, d)]) |
|
>>> a = Atoms([Atom('N', (0, 0, 0)), Atom('N', (0, 0, d))]) |
|
|
|
FCC gold: |
|
|
|
>>> a = 4.05 # Gold lattice constant |
|
>>> b = a / 2 |
|
>>> fcc = Atoms('Au', |
|
... cell=[(0, b, b), (b, 0, b), (b, b, 0)], |
|
... pbc=True) |
|
|
|
Hydrogen wire: |
|
|
|
>>> d = 0.9 # H-H distance |
|
>>> h = Atoms('H', positions=[(0, 0, 0)], |
|
... cell=(d, 0, 0), |
|
... pbc=(1, 0, 0)) |
|
""" |
|
|
|
ase_objtype = 'atoms' # For JSONability |
|
|
|
def __init__(self, symbols=None, |
|
positions=None, numbers=None, |
|
tags=None, momenta=None, masses=None, |
|
magmoms=None, charges=None, |
|
scaled_positions=None, |
|
cell=None, pbc=None, celldisp=None, |
|
constraint=None, |
|
calculator=None, |
|
info=None, |
|
velocities=None): |
|
|
|
self._cellobj = Cell.new() |
|
self._pbc = np.zeros(3, bool) |
|
|
|
atoms = None |
|
|
|
if hasattr(symbols, 'get_positions'): |
|
atoms = symbols |
|
symbols = None |
|
elif (isinstance(symbols, (list, tuple)) and |
|
len(symbols) > 0 and isinstance(symbols[0], Atom)): |
|
# Get data from a list or tuple of Atom objects: |
|
data = [[atom.get_raw(name) for atom in symbols] |
|
for name in |
|
['position', 'number', 'tag', 'momentum', |
|
'mass', 'magmom', 'charge']] |
|
atoms = self.__class__(None, *data) |
|
symbols = None |
|
|
|
if atoms is not None: |
|
# Get data from another Atoms object: |
|
if scaled_positions is not None: |
|
raise NotImplementedError |
|
if symbols is None and numbers is None: |
|
numbers = atoms.get_atomic_numbers() |
|
if positions is None: |
|
positions = atoms.get_positions() |
|
if tags is None and atoms.has('tags'): |
|
tags = atoms.get_tags() |
|
if momenta is None and atoms.has('momenta'): |
|
momenta = atoms.get_momenta() |
|
if magmoms is None and atoms.has('initial_magmoms'): |
|
magmoms = atoms.get_initial_magnetic_moments() |
|
if masses is None and atoms.has('masses'): |
|
masses = atoms.get_masses() |
|
if charges is None and atoms.has('initial_charges'): |
|
charges = atoms.get_initial_charges() |
|
if cell is None: |
|
cell = atoms.get_cell() |
|
if celldisp is None: |
|
celldisp = atoms.get_celldisp() |
|
if pbc is None: |
|
pbc = atoms.get_pbc() |
|
if constraint is None: |
|
constraint = [c.copy() for c in atoms.constraints] |
|
if calculator is None: |
|
calculator = atoms.calc |
|
if info is None: |
|
info = copy.deepcopy(atoms.info) |
|
|
|
self.arrays = {} |
|
|
|
if symbols is None: |
|
if numbers is None: |
|
if positions is not None: |
|
natoms = len(positions) |
|
elif scaled_positions is not None: |
|
natoms = len(scaled_positions) |
|
else: |
|
natoms = 0 |
|
numbers = np.zeros(natoms, int) |
|
self.new_array('numbers', numbers, int) |
|
else: |
|
if numbers is not None: |
|
raise TypeError( |
|
'Use only one of "symbols" and "numbers".') |
|
else: |
|
self.new_array('numbers', symbols2numbers(symbols), int) |
|
|
|
if self.numbers.ndim != 1: |
|
raise ValueError('"numbers" must be 1-dimensional.') |
|
|
|
if cell is None: |
|
cell = np.zeros((3, 3)) |
|
self.set_cell(cell) |
|
|
|
if celldisp is None: |
|
celldisp = np.zeros(shape=(3, 1)) |
|
self.set_celldisp(celldisp) |
|
|
|
if positions is None: |
|
if scaled_positions is None: |
|
positions = np.zeros((len(self.arrays['numbers']), 3)) |
|
else: |
|
assert self.cell.rank == 3 |
|
positions = np.dot(scaled_positions, self.cell) |
|
else: |
|
if scaled_positions is not None: |
|
raise TypeError( |
|
'Use only one of "symbols" and "numbers".') |
|
self.new_array('positions', positions, float, (3,)) |
|
|
|
self.set_constraint(constraint) |
|
self.set_tags(default(tags, 0)) |
|
self.set_masses(default(masses, None)) |
|
self.set_initial_magnetic_moments(default(magmoms, 0.0)) |
|
self.set_initial_charges(default(charges, 0.0)) |
|
if pbc is None: |
|
pbc = False |
|
self.set_pbc(pbc) |
|
self.set_momenta(default(momenta, (0.0, 0.0, 0.0)), |
|
apply_constraint=False) |
|
|
|
if velocities is not None: |
|
if momenta is None: |
|
self.set_velocities(velocities) |
|
else: |
|
raise TypeError( |
|
'Use only one of "momenta" and "velocities".') |
|
|
|
if info is None: |
|
self.info = {} |
|
else: |
|
self.info = dict(info) |
|
|
|
self.calc = calculator |
|
|
|
@property |
|
def symbols(self): |
|
"""Get chemical symbols as a :class:`ase.symbols.Symbols` object. |
|
|
|
The object works like ``atoms.numbers`` except its values |
|
are strings. It supports in-place editing.""" |
|
return Symbols(self.numbers) |
|
|
|
@symbols.setter |
|
def symbols(self, obj): |
|
new_symbols = Symbols.fromsymbols(obj) |
|
self.numbers[:] = new_symbols.numbers |
|
|
|
@deprecated(DeprecationWarning('Please use atoms.calc = calc')) |
|
def set_calculator(self, calc=None): |
|
"""Attach calculator object. |
|
|
|
Please use the equivalent atoms.calc = calc instead of this |
|
method.""" |
|
self.calc = calc |
|
|
|
@deprecated(DeprecationWarning('Please use atoms.calc')) |
|
def get_calculator(self): |
|
"""Get currently attached calculator object. |
|
|
|
Please use the equivalent atoms.calc instead of |
|
atoms.get_calculator().""" |
|
return self.calc |
|
|
|
@property |
|
def calc(self): |
|
"""Calculator object.""" |
|
return self._calc |
|
|
|
@calc.setter |
|
def calc(self, calc): |
|
self._calc = calc |
|
if hasattr(calc, 'set_atoms'): |
|
calc.set_atoms(self) |
|
|
|
@calc.deleter # type: ignore |
|
@deprecated(DeprecationWarning('Please use atoms.calc = None')) |
|
def calc(self): |
|
self._calc = None |
|
|
|
@property # type: ignore |
|
@deprecated('Please use atoms.cell.rank instead') |
|
def number_of_lattice_vectors(self): |
|
"""Number of (non-zero) lattice vectors.""" |
|
return self.cell.rank |
|
|
|
def set_constraint(self, constraint=None): |
|
"""Apply one or more constrains. |
|
|
|
The *constraint* argument must be one constraint object or a |
|
list of constraint objects.""" |
|
if constraint is None: |
|
self._constraints = [] |
|
else: |
|
if isinstance(constraint, list): |
|
self._constraints = constraint |
|
elif isinstance(constraint, tuple): |
|
self._constraints = list(constraint) |
|
else: |
|
self._constraints = [constraint] |
|
|
|
def _get_constraints(self): |
|
return self._constraints |
|
|
|
def _del_constraints(self): |
|
self._constraints = [] |
|
|
|
constraints = property(_get_constraints, set_constraint, _del_constraints, |
|
'Constraints of the atoms.') |
|
|
|
def set_cell(self, cell, scale_atoms=False, apply_constraint=True): |
|
"""Set unit cell vectors. |
|
|
|
Parameters: |
|
|
|
cell: 3x3 matrix or length 3 or 6 vector |
|
Unit cell. A 3x3 matrix (the three unit cell vectors) or |
|
just three numbers for an orthorhombic cell. Another option is |
|
6 numbers, which describes unit cell with lengths of unit cell |
|
vectors and with angles between them (in degrees), in following |
|
order: [len(a), len(b), len(c), angle(b,c), angle(a,c), |
|
angle(a,b)]. First vector will lie in x-direction, second in |
|
xy-plane, and the third one in z-positive subspace. |
|
scale_atoms: bool |
|
Fix atomic positions or move atoms with the unit cell? |
|
Default behavior is to *not* move the atoms (scale_atoms=False). |
|
apply_constraint: bool |
|
Whether to apply constraints to the given cell. |
|
|
|
Examples: |
|
|
|
Two equivalent ways to define an orthorhombic cell: |
|
|
|
>>> atoms = Atoms('He') |
|
>>> a, b, c = 7, 7.5, 8 |
|
>>> atoms.set_cell([a, b, c]) |
|
>>> atoms.set_cell([(a, 0, 0), (0, b, 0), (0, 0, c)]) |
|
|
|
FCC unit cell: |
|
|
|
>>> atoms.set_cell([(0, b, b), (b, 0, b), (b, b, 0)]) |
|
|
|
Hexagonal unit cell: |
|
|
|
>>> atoms.set_cell([a, a, c, 90, 90, 120]) |
|
|
|
Rhombohedral unit cell: |
|
|
|
>>> alpha = 77 |
|
>>> atoms.set_cell([a, a, a, alpha, alpha, alpha]) |
|
""" |
|
|
|
# Override pbcs if and only if given a Cell object: |
|
cell = Cell.new(cell) |
|
|
|
# XXX not working well during initialize due to missing _constraints |
|
if apply_constraint and hasattr(self, '_constraints'): |
|
for constraint in self.constraints: |
|
if hasattr(constraint, 'adjust_cell'): |
|
constraint.adjust_cell(self, cell) |
|
|
|
if scale_atoms: |
|
M = np.linalg.solve(self.cell.complete(), cell.complete()) |
|
self.positions[:] = np.dot(self.positions, M) |
|
|
|
self.cell[:] = cell |
|
|
|
def set_celldisp(self, celldisp): |
|
"""Set the unit cell displacement vectors.""" |
|
celldisp = np.array(celldisp, float) |
|
self._celldisp = celldisp |
|
|
|
def get_celldisp(self): |
|
"""Get the unit cell displacement vectors.""" |
|
return self._celldisp.copy() |
|
|
|
def get_cell(self, complete=False): |
|
"""Get the three unit cell vectors as a `class`:ase.cell.Cell` object. |
|
|
|
The Cell object resembles a 3x3 ndarray, and cell[i, j] |
|
is the jth Cartesian coordinate of the ith cell vector.""" |
|
if complete: |
|
cell = self.cell.complete() |
|
else: |
|
cell = self.cell.copy() |
|
|
|
return cell |
|
|
|
@deprecated('Please use atoms.cell.cellpar() instead') |
|
def get_cell_lengths_and_angles(self): |
|
"""Get unit cell parameters. Sequence of 6 numbers. |
|
|
|
First three are unit cell vector lengths and second three |
|
are angles between them:: |
|
|
|
[len(a), len(b), len(c), angle(b,c), angle(a,c), angle(a,b)] |
|
|
|
in degrees. |
|
""" |
|
return self.cell.cellpar() |
|
|
|
@deprecated('Please use atoms.cell.reciprocal()') |
|
def get_reciprocal_cell(self): |
|
"""Get the three reciprocal lattice vectors as a 3x3 ndarray. |
|
|
|
Note that the commonly used factor of 2 pi for Fourier |
|
transforms is not included here.""" |
|
|
|
return self.cell.reciprocal() |
|
|
|
@property |
|
def pbc(self): |
|
"""Reference to pbc-flags for in-place manipulations.""" |
|
return self._pbc |
|
|
|
@pbc.setter |
|
def pbc(self, pbc): |
|
self._pbc[:] = pbc |
|
|
|
def set_pbc(self, pbc): |
|
"""Set periodic boundary condition flags.""" |
|
self.pbc = pbc |
|
|
|
def get_pbc(self): |
|
"""Get periodic boundary condition flags.""" |
|
return self.pbc.copy() |
|
|
|
def new_array(self, name, a, dtype=None, shape=None): |
|
"""Add new array. |
|
|
|
If *shape* is not *None*, the shape of *a* will be checked.""" |
|
|
|
if dtype is not None: |
|
a = np.array(a, dtype, order='C') |
|
if len(a) == 0 and shape is not None: |
|
a.shape = (-1,) + shape |
|
else: |
|
if not a.flags['C_CONTIGUOUS']: |
|
a = np.ascontiguousarray(a) |
|
else: |
|
a = a.copy() |
|
|
|
if name in self.arrays: |
|
raise RuntimeError('Array {} already present'.format(name)) |
|
|
|
for b in self.arrays.values(): |
|
if len(a) != len(b): |
|
raise ValueError('Array "%s" has wrong length: %d != %d.' % |
|
(name, len(a), len(b))) |
|
break |
|
|
|
if shape is not None and a.shape[1:] != shape: |
|
raise ValueError('Array "%s" has wrong shape %s != %s.' % |
|
(name, a.shape, (a.shape[0:1] + shape))) |
|
|
|
self.arrays[name] = a |
|
|
|
def get_array(self, name, copy=True): |
|
"""Get an array. |
|
|
|
Returns a copy unless the optional argument copy is false. |
|
""" |
|
if copy: |
|
return self.arrays[name].copy() |
|
else: |
|
return self.arrays[name] |
|
|
|
def set_array(self, name, a, dtype=None, shape=None): |
|
"""Update array. |
|
|
|
If *shape* is not *None*, the shape of *a* will be checked. |
|
If *a* is *None*, then the array is deleted.""" |
|
|
|
b = self.arrays.get(name) |
|
if b is None: |
|
if a is not None: |
|
self.new_array(name, a, dtype, shape) |
|
else: |
|
if a is None: |
|
del self.arrays[name] |
|
else: |
|
a = np.asarray(a) |
|
if a.shape != b.shape: |
|
raise ValueError('Array "%s" has wrong shape %s != %s.' % |
|
(name, a.shape, b.shape)) |
|
b[:] = a |
|
|
|
def has(self, name): |
|
"""Check for existence of array. |
|
|
|
name must be one of: 'tags', 'momenta', 'masses', 'initial_magmoms', |
|
'initial_charges'.""" |
|
# XXX extend has to calculator properties |
|
return name in self.arrays |
|
|
|
def set_atomic_numbers(self, numbers): |
|
"""Set atomic numbers.""" |
|
self.set_array('numbers', numbers, int, ()) |
|
|
|
def get_atomic_numbers(self): |
|
"""Get integer array of atomic numbers.""" |
|
return self.arrays['numbers'].copy() |
|
|
|
def get_chemical_symbols(self): |
|
"""Get list of chemical symbol strings. |
|
|
|
Equivalent to ``list(atoms.symbols)``.""" |
|
return list(self.symbols) |
|
|
|
def set_chemical_symbols(self, symbols): |
|
"""Set chemical symbols.""" |
|
self.set_array('numbers', symbols2numbers(symbols), int, ()) |
|
|
|
def get_chemical_formula(self, mode='hill', empirical=False): |
|
"""Get the chemical formula as a string based on the chemical symbols. |
|
|
|
Parameters: |
|
|
|
mode: str |
|
There are four different modes available: |
|
|
|
'all': The list of chemical symbols are contracted to a string, |
|
e.g. ['C', 'H', 'H', 'H', 'O', 'H'] becomes 'CHHHOH'. |
|
|
|
'reduce': The same as 'all' where repeated elements are contracted |
|
to a single symbol and a number, e.g. 'CHHHOCHHH' is reduced to |
|
'CH3OCH3'. |
|
|
|
'hill': The list of chemical symbols are contracted to a string |
|
following the Hill notation (alphabetical order with C and H |
|
first), e.g. 'CHHHOCHHH' is reduced to 'C2H6O' and 'SOOHOHO' to |
|
'H2O4S'. This is default. |
|
|
|
'metal': The list of chemical symbols (alphabetical metals, |
|
and alphabetical non-metals) |
|
|
|
empirical, bool (optional, default=False) |
|
Divide the symbol counts by their greatest common divisor to yield |
|
an empirical formula. Only for mode `metal` and `hill`. |
|
""" |
|
return self.symbols.get_chemical_formula(mode, empirical) |
|
|
|
def set_tags(self, tags): |
|
"""Set tags for all atoms. If only one tag is supplied, it is |
|
applied to all atoms.""" |
|
if isinstance(tags, int): |
|
tags = [tags] * len(self) |
|
self.set_array('tags', tags, int, ()) |
|
|
|
def get_tags(self): |
|
"""Get integer array of tags.""" |
|
if 'tags' in self.arrays: |
|
return self.arrays['tags'].copy() |
|
else: |
|
return np.zeros(len(self), int) |
|
|
|
def set_momenta(self, momenta, apply_constraint=True): |
|
"""Set momenta.""" |
|
if (apply_constraint and len(self.constraints) > 0 and |
|
momenta is not None): |
|
momenta = np.array(momenta) # modify a copy |
|
for constraint in self.constraints: |
|
if hasattr(constraint, 'adjust_momenta'): |
|
constraint.adjust_momenta(self, momenta) |
|
self.set_array('momenta', momenta, float, (3,)) |
|
|
|
def set_velocities(self, velocities): |
|
"""Set the momenta by specifying the velocities.""" |
|
self.set_momenta(self.get_masses()[:, np.newaxis] * velocities) |
|
|
|
def get_momenta(self): |
|
"""Get array of momenta.""" |
|
if 'momenta' in self.arrays: |
|
return self.arrays['momenta'].copy() |
|
else: |
|
return np.zeros((len(self), 3)) |
|
|
|
def set_masses(self, masses='defaults'): |
|
"""Set atomic masses in atomic mass units. |
|
|
|
The array masses should contain a list of masses. In case |
|
the masses argument is not given or for those elements of the |
|
masses list that are None, standard values are set.""" |
|
|
|
if isinstance(masses, str): |
|
if masses == 'defaults': |
|
masses = atomic_masses[self.arrays['numbers']] |
|
elif masses == 'most_common': |
|
masses = atomic_masses_common[self.arrays['numbers']] |
|
elif masses is None: |
|
pass |
|
elif not isinstance(masses, np.ndarray): |
|
masses = list(masses) |
|
for i, mass in enumerate(masses): |
|
if mass is None: |
|
masses[i] = atomic_masses[self.numbers[i]] |
|
self.set_array('masses', masses, float, ()) |
|
|
|
def get_masses(self): |
|
"""Get array of masses in atomic mass units.""" |
|
if 'masses' in self.arrays: |
|
return self.arrays['masses'].copy() |
|
else: |
|
return atomic_masses[self.arrays['numbers']] |
|
|
|
def set_initial_magnetic_moments(self, magmoms=None): |
|
"""Set the initial magnetic moments. |
|
|
|
Use either one or three numbers for every atom (collinear |
|
or non-collinear spins).""" |
|
|
|
if magmoms is None: |
|
self.set_array('initial_magmoms', None) |
|
else: |
|
magmoms = np.asarray(magmoms) |
|
self.set_array('initial_magmoms', magmoms, float, |
|
magmoms.shape[1:]) |
|
|
|
def get_initial_magnetic_moments(self): |
|
"""Get array of initial magnetic moments.""" |
|
if 'initial_magmoms' in self.arrays: |
|
return self.arrays['initial_magmoms'].copy() |
|
else: |
|
return np.zeros(len(self)) |
|
|
|
def get_magnetic_moments(self): |
|
"""Get calculated local magnetic moments.""" |
|
if self._calc is None: |
|
raise RuntimeError('Atoms object has no calculator.') |
|
return self._calc.get_magnetic_moments(self) |
|
|
|
def get_magnetic_moment(self): |
|
"""Get calculated total magnetic moment.""" |
|
if self._calc is None: |
|
raise RuntimeError('Atoms object has no calculator.') |
|
return self._calc.get_magnetic_moment(self) |
|
|
|
def set_initial_charges(self, charges=None): |
|
"""Set the initial charges.""" |
|
|
|
if charges is None: |
|
self.set_array('initial_charges', None) |
|
else: |
|
self.set_array('initial_charges', charges, float, ()) |
|
|
|
def get_initial_charges(self): |
|
"""Get array of initial charges.""" |
|
if 'initial_charges' in self.arrays: |
|
return self.arrays['initial_charges'].copy() |
|
else: |
|
return np.zeros(len(self)) |
|
|
|
def get_charges(self): |
|
"""Get calculated charges.""" |
|
if self._calc is None: |
|
raise RuntimeError('Atoms object has no calculator.') |
|
try: |
|
return self._calc.get_charges(self) |
|
except AttributeError: |
|
from ase.calculators.calculator import PropertyNotImplementedError |
|
raise PropertyNotImplementedError |
|
|
|
def set_positions(self, newpositions, apply_constraint=True): |
|
"""Set positions, honoring any constraints. To ignore constraints, |
|
use *apply_constraint=False*.""" |
|
if self.constraints and apply_constraint: |
|
newpositions = np.array(newpositions, float) |
|
for constraint in self.constraints: |
|
constraint.adjust_positions(self, newpositions) |
|
|
|
self.set_array('positions', newpositions, shape=(3,)) |
|
|
|
def get_positions(self, wrap=False, **wrap_kw): |
|
"""Get array of positions. |
|
|
|
Parameters: |
|
|
|
wrap: bool |
|
wrap atoms back to the cell before returning positions |
|
wrap_kw: (keyword=value) pairs |
|
optional keywords `pbc`, `center`, `pretty_translation`, `eps`, |
|
see :func:`ase.geometry.wrap_positions` |
|
""" |
|
if wrap: |
|
if 'pbc' not in wrap_kw: |
|
wrap_kw['pbc'] = self.pbc |
|
return wrap_positions(self.positions, self.cell, **wrap_kw) |
|
else: |
|
return self.arrays['positions'].copy() |
|
|
|
def get_potential_energy(self, force_consistent=False, |
|
apply_constraint=True): |
|
"""Calculate potential energy. |
|
|
|
Ask the attached calculator to calculate the potential energy and |
|
apply constraints. Use *apply_constraint=False* to get the raw |
|
forces. |
|
|
|
When supported by the calculator, either the energy extrapolated |
|
to zero Kelvin or the energy consistent with the forces (the free |
|
energy) can be returned. |
|
""" |
|
if self._calc is None: |
|
raise RuntimeError('Atoms object has no calculator.') |
|
if force_consistent: |
|
energy = self._calc.get_potential_energy( |
|
self, force_consistent=force_consistent) |
|
else: |
|
energy = self._calc.get_potential_energy(self) |
|
if apply_constraint: |
|
for constraint in self.constraints: |
|
if hasattr(constraint, 'adjust_potential_energy'): |
|
energy += constraint.adjust_potential_energy(self) |
|
return energy |
|
|
|
def get_properties(self, properties): |
|
"""This method is experimental; currently for internal use.""" |
|
# XXX Something about constraints. |
|
if self._calc is None: |
|
raise RuntimeError('Atoms object has no calculator.') |
|
return self._calc.calculate_properties(self, properties) |
|
|
|
def get_potential_energies(self): |
|
"""Calculate the potential energies of all the atoms. |
|
|
|
Only available with calculators supporting per-atom energies |
|
(e.g. classical potentials). |
|
""" |
|
if self._calc is None: |
|
raise RuntimeError('Atoms object has no calculator.') |
|
return self._calc.get_potential_energies(self) |
|
|
|
def get_kinetic_energy(self): |
|
"""Get the kinetic energy.""" |
|
momenta = self.arrays.get('momenta') |
|
if momenta is None: |
|
return 0.0 |
|
return 0.5 * np.vdot(momenta, self.get_velocities()) |
|
|
|
def get_velocities(self): |
|
"""Get array of velocities.""" |
|
momenta = self.get_momenta() |
|
masses = self.get_masses() |
|
return momenta / masses[:, np.newaxis] |
|
|
|
def get_total_energy(self): |
|
"""Get the total energy - potential plus kinetic energy.""" |
|
return self.get_potential_energy() + self.get_kinetic_energy() |
|
|
|
def get_forces(self, apply_constraint=True, md=False): |
|
"""Calculate atomic forces. |
|
|
|
Ask the attached calculator to calculate the forces and apply |
|
constraints. Use *apply_constraint=False* to get the raw |
|
forces. |
|
|
|
For molecular dynamics (md=True) we don't apply the constraint |
|
to the forces but to the momenta. When holonomic constraints for |
|
rigid linear triatomic molecules are present, ask the constraints |
|
to redistribute the forces within each triple defined in the |
|
constraints (required for molecular dynamics with this type of |
|
constraints).""" |
|
|
|
if self._calc is None: |
|
raise RuntimeError('Atoms object has no calculator.') |
|
forces = self._calc.get_forces(self) |
|
|
|
if apply_constraint: |
|
# We need a special md flag here because for MD we want |
|
# to skip real constraints but include special "constraints" |
|
# Like Hookean. |
|
for constraint in self.constraints: |
|
if md and hasattr(constraint, 'redistribute_forces_md'): |
|
constraint.redistribute_forces_md(self, forces) |
|
if not md or hasattr(constraint, 'adjust_potential_energy'): |
|
constraint.adjust_forces(self, forces) |
|
return forces |
|
|
|
# Informs calculators (e.g. Asap) that ideal gas contribution is added here. |
|
_ase_handles_dynamic_stress = True |
|
|
|
def get_stress(self, voigt=True, apply_constraint=True, |
|
include_ideal_gas=False): |
|
"""Calculate stress tensor. |
|
|
|
Returns an array of the six independent components of the |
|
symmetric stress tensor, in the traditional Voigt order |
|
(xx, yy, zz, yz, xz, xy) or as a 3x3 matrix. Default is Voigt |
|
order. |
|
|
|
The ideal gas contribution to the stresses is added if the |
|
atoms have momenta and ``include_ideal_gas`` is set to True. |
|
""" |
|
|
|
if self._calc is None: |
|
raise RuntimeError('Atoms object has no calculator.') |
|
|
|
stress = self._calc.get_stress(self) |
|
shape = stress.shape |
|
|
|
if shape == (3, 3): |
|
# Convert to the Voigt form before possibly applying |
|
# constraints and adding the dynamic part of the stress |
|
# (the "ideal gas contribution"). |
|
stress = full_3x3_to_voigt_6_stress(stress) |
|
else: |
|
assert shape == (6,) |
|
|
|
if apply_constraint: |
|
for constraint in self.constraints: |
|
if hasattr(constraint, 'adjust_stress'): |
|
constraint.adjust_stress(self, stress) |
|
|
|
# Add ideal gas contribution, if applicable |
|
if include_ideal_gas and self.has('momenta'): |
|
stresscomp = np.array([[0, 5, 4], [5, 1, 3], [4, 3, 2]]) |
|
p = self.get_momenta() |
|
masses = self.get_masses() |
|
invmass = 1.0 / masses |
|
invvol = 1.0 / self.get_volume() |
|
for alpha in range(3): |
|
for beta in range(alpha, 3): |
|
stress[stresscomp[alpha, beta]] -= ( |
|
p[:, alpha] * p[:, beta] * invmass).sum() * invvol |
|
|
|
if voigt: |
|
return stress |
|
else: |
|
return voigt_6_to_full_3x3_stress(stress) |
|
|
|
def get_stresses(self, include_ideal_gas=False, voigt=True): |
|
"""Calculate the stress-tensor of all the atoms. |
|
|
|
Only available with calculators supporting per-atom energies and |
|
stresses (e.g. classical potentials). Even for such calculators |
|
there is a certain arbitrariness in defining per-atom stresses. |
|
|
|
The ideal gas contribution to the stresses is added if the |
|
atoms have momenta and ``include_ideal_gas`` is set to True. |
|
""" |
|
if self._calc is None: |
|
raise RuntimeError('Atoms object has no calculator.') |
|
stresses = self._calc.get_stresses(self) |
|
|
|
# make sure `stresses` are in voigt form |
|
if np.shape(stresses)[1:] == (3, 3): |
|
stresses_voigt = [full_3x3_to_voigt_6_stress(s) for s in stresses] |
|
stresses = np.array(stresses_voigt) |
|
|
|
# REMARK: The ideal gas contribution is intensive, i.e., the volume |
|
# is divided out. We currently don't check if `stresses` are intensive |
|
# as well, i.e., if `a.get_stresses.sum(axis=0) == a.get_stress()`. |
|
# It might be good to check this here, but adds computational overhead. |
|
|
|
if include_ideal_gas and self.has('momenta'): |
|
stresscomp = np.array([[0, 5, 4], [5, 1, 3], [4, 3, 2]]) |
|
if hasattr(self._calc, 'get_atomic_volumes'): |
|
invvol = 1.0 / self._calc.get_atomic_volumes() |
|
else: |
|
invvol = self.get_global_number_of_atoms() / self.get_volume() |
|
p = self.get_momenta() |
|
invmass = 1.0 / self.get_masses() |
|
for alpha in range(3): |
|
for beta in range(alpha, 3): |
|
stresses[:, stresscomp[alpha, beta]] -= ( |
|
p[:, alpha] * p[:, beta] * invmass * invvol) |
|
if voigt: |
|
return stresses |
|
else: |
|
stresses_3x3 = [voigt_6_to_full_3x3_stress(s) for s in stresses] |
|
return np.array(stresses_3x3) |
|
|
|
def get_dipole_moment(self): |
|
"""Calculate the electric dipole moment for the atoms object. |
|
|
|
Only available for calculators which has a get_dipole_moment() |
|
method.""" |
|
|
|
if self._calc is None: |
|
raise RuntimeError('Atoms object has no calculator.') |
|
return self._calc.get_dipole_moment(self) |
|
|
|
def copy(self): |
|
"""Return a copy.""" |
|
atoms = self.__class__(cell=self.cell, pbc=self.pbc, info=self.info, |
|
celldisp=self._celldisp.copy()) |
|
|
|
atoms.arrays = {} |
|
for name, a in self.arrays.items(): |
|
atoms.arrays[name] = a.copy() |
|
atoms.constraints = copy.deepcopy(self.constraints) |
|
return atoms |
|
|
|
def todict(self): |
|
"""For basic JSON (non-database) support.""" |
|
d = dict(self.arrays) |
|
d['cell'] = np.asarray(self.cell) |
|
d['pbc'] = self.pbc |
|
if self._celldisp.any(): |
|
d['celldisp'] = self._celldisp |
|
if self.constraints: |
|
d['constraints'] = self.constraints |
|
if self.info: |
|
d['info'] = self.info |
|
# Calculator... trouble. |
|
return d |
|
|
|
@classmethod |
|
def fromdict(cls, dct): |
|
"""Rebuild atoms object from dictionary representation (todict).""" |
|
dct = dct.copy() |
|
kw = {} |
|
for name in ['numbers', 'positions', 'cell', 'pbc']: |
|
kw[name] = dct.pop(name) |
|
|
|
constraints = dct.pop('constraints', None) |
|
if constraints: |
|
from ase.constraints import dict2constraint |
|
constraints = [dict2constraint(d) for d in constraints] |
|
|
|
info = dct.pop('info', None) |
|
|
|
atoms = cls(constraint=constraints, |
|
celldisp=dct.pop('celldisp', None), |
|
info=info, **kw) |
|
natoms = len(atoms) |
|
|
|
# Some arrays are named differently from the atoms __init__ keywords. |
|
# Also, there may be custom arrays. Hence we set them directly: |
|
for name, arr in dct.items(): |
|
assert len(arr) == natoms, name |
|
assert isinstance(arr, np.ndarray) |
|
atoms.arrays[name] = arr |
|
return atoms |
|
|
|
def __len__(self): |
|
return len(self.arrays['positions']) |
|
|
|
def get_number_of_atoms(self): |
|
"""Deprecated, please do not use. |
|
|
|
You probably want len(atoms). Or if your atoms are distributed, |
|
use (and see) get_global_number_of_atoms().""" |
|
import warnings |
|
warnings.warn('Use get_global_number_of_atoms() instead', |
|
np.VisibleDeprecationWarning) |
|
return len(self) |
|
|
|
def get_global_number_of_atoms(self): |
|
"""Returns the global number of atoms in a distributed-atoms parallel |
|
simulation. |
|
|
|
DO NOT USE UNLESS YOU KNOW WHAT YOU ARE DOING! |
|
|
|
Equivalent to len(atoms) in the standard ASE Atoms class. You should |
|
normally use len(atoms) instead. This function's only purpose is to |
|
make compatibility between ASE and Asap easier to maintain by having a |
|
few places in ASE use this function instead. It is typically only |
|
when counting the global number of degrees of freedom or in similar |
|
situations. |
|
""" |
|
return len(self) |
|
|
|
def __repr__(self): |
|
tokens = [] |
|
|
|
N = len(self) |
|
if N <= 60: |
|
symbols = self.get_chemical_formula('reduce') |
|
else: |
|
symbols = self.get_chemical_formula('hill') |
|
tokens.append("symbols='{0}'".format(symbols)) |
|
|
|
if self.pbc.any() and not self.pbc.all(): |
|
tokens.append('pbc={0}'.format(self.pbc.tolist())) |
|
else: |
|
tokens.append('pbc={0}'.format(self.pbc[0])) |
|
|
|
cell = self.cell |
|
if cell: |
|
if cell.orthorhombic: |
|
cell = cell.lengths().tolist() |
|
else: |
|
cell = cell.tolist() |
|
tokens.append('cell={0}'.format(cell)) |
|
|
|
for name in sorted(self.arrays): |
|
if name in ['numbers', 'positions']: |
|
continue |
|
tokens.append('{0}=...'.format(name)) |
|
|
|
if self.constraints: |
|
if len(self.constraints) == 1: |
|
constraint = self.constraints[0] |
|
else: |
|
constraint = self.constraints |
|
tokens.append('constraint={0}'.format(repr(constraint))) |
|
|
|
if self._calc is not None: |
|
tokens.append('calculator={0}(...)' |
|
.format(self._calc.__class__.__name__)) |
|
|
|
return '{0}({1})'.format(self.__class__.__name__, ', '.join(tokens)) |
|
|
|
def __add__(self, other): |
|
atoms = self.copy() |
|
atoms += other |
|
return atoms |
|
|
|
def extend(self, other): |
|
"""Extend atoms object by appending atoms from *other*.""" |
|
if isinstance(other, Atom): |
|
other = self.__class__([other]) |
|
|
|
n1 = len(self) |
|
n2 = len(other) |
|
|
|
for name, a1 in self.arrays.items(): |
|
a = np.zeros((n1 + n2,) + a1.shape[1:], a1.dtype) |
|
a[:n1] = a1 |
|
if name == 'masses': |
|
a2 = other.get_masses() |
|
else: |
|
a2 = other.arrays.get(name) |
|
if a2 is not None: |
|
a[n1:] = a2 |
|
self.arrays[name] = a |
|
|
|
for name, a2 in other.arrays.items(): |
|
if name in self.arrays: |
|
continue |
|
a = np.empty((n1 + n2,) + a2.shape[1:], a2.dtype) |
|
a[n1:] = a2 |
|
if name == 'masses': |
|
a[:n1] = self.get_masses()[:n1] |
|
else: |
|
a[:n1] = 0 |
|
|
|
self.set_array(name, a) |
|
|
|
def __iadd__(self, other): |
|
self.extend(other) |
|
return self |
|
|
|
def append(self, atom): |
|
"""Append atom to end.""" |
|
self.extend(self.__class__([atom])) |
|
|
|
def __iter__(self): |
|
for i in range(len(self)): |
|
yield self[i] |
|
|
|
def __getitem__(self, i): |
|
"""Return a subset of the atoms. |
|
|
|
i -- scalar integer, list of integers, or slice object |
|
describing which atoms to return. |
|
|
|
If i is a scalar, return an Atom object. If i is a list or a |
|
slice, return an Atoms object with the same cell, pbc, and |
|
other associated info as the original Atoms object. The |
|
indices of the constraints will be shuffled so that they match |
|
the indexing in the subset returned. |
|
|
|
""" |
|
|
|
if isinstance(i, numbers.Integral): |
|
natoms = len(self) |
|
if i < -natoms or i >= natoms: |
|
raise IndexError('Index out of range.') |
|
|
|
return Atom(atoms=self, index=i) |
|
elif not isinstance(i, slice): |
|
i = np.array(i) |
|
# if i is a mask |
|
if i.dtype == bool: |
|
if len(i) != len(self): |
|
raise IndexError('Length of mask {} must equal ' |
|
'number of atoms {}' |
|
.format(len(i), len(self))) |
|
i = np.arange(len(self))[i] |
|
|
|
import copy |
|
|
|
conadd = [] |
|
# Constraints need to be deepcopied, but only the relevant ones. |
|
for con in copy.deepcopy(self.constraints): |
|
try: |
|
con.index_shuffle(self, i) |
|
except (IndexError, NotImplementedError): |
|
pass |
|
else: |
|
conadd.append(con) |
|
|
|
atoms = self.__class__(cell=self.cell, pbc=self.pbc, info=self.info, |
|
# should be communicated to the slice as well |
|
celldisp=self._celldisp) |
|
# TODO: Do we need to shuffle indices in adsorbate_info too? |
|
|
|
atoms.arrays = {} |
|
for name, a in self.arrays.items(): |
|
atoms.arrays[name] = a[i].copy() |
|
|
|
atoms.constraints = conadd |
|
return atoms |
|
|
|
def __delitem__(self, i): |
|
from ase.constraints import FixAtoms |
|
for c in self._constraints: |
|
if not isinstance(c, FixAtoms): |
|
raise RuntimeError('Remove constraint using set_constraint() ' |
|
'before deleting atoms.') |
|
|
|
if isinstance(i, list) and len(i) > 0: |
|
# Make sure a list of booleans will work correctly and not be |
|
# interpreted at 0 and 1 indices. |
|
i = np.array(i) |
|
|
|
if len(self._constraints) > 0: |
|
n = len(self) |
|
i = np.arange(n)[i] |
|
if isinstance(i, int): |
|
i = [i] |
|
constraints = [] |
|
for c in self._constraints: |
|
c = c.delete_atoms(i, n) |
|
if c is not None: |
|
constraints.append(c) |
|
self.constraints = constraints |
|
|
|
mask = np.ones(len(self), bool) |
|
mask[i] = False |
|
for name, a in self.arrays.items(): |
|
self.arrays[name] = a[mask] |
|
|
|
def pop(self, i=-1): |
|
"""Remove and return atom at index *i* (default last).""" |
|
atom = self[i] |
|
atom.cut_reference_to_atoms() |
|
del self[i] |
|
return atom |
|
|
|
def __imul__(self, m): |
|
"""In-place repeat of atoms.""" |
|
if isinstance(m, int): |
|
m = (m, m, m) |
|
|
|
for x, vec in zip(m, self.cell): |
|
if x != 1 and not vec.any(): |
|
raise ValueError('Cannot repeat along undefined lattice ' |
|
'vector') |
|
|
|
M = np.product(m) |
|
n = len(self) |
|
|
|
for name, a in self.arrays.items(): |
|
self.arrays[name] = np.tile(a, (M,) + (1,) * (len(a.shape) - 1)) |
|
|
|
positions = self.arrays['positions'] |
|
i0 = 0 |
|
for m0 in range(m[0]): |
|
for m1 in range(m[1]): |
|
for m2 in range(m[2]): |
|
i1 = i0 + n |
|
positions[i0:i1] += np.dot((m0, m1, m2), self.cell) |
|
i0 = i1 |
|
|
|
if self.constraints is not None: |
|
self.constraints = [c.repeat(m, n) for c in self.constraints] |
|
|
|
self.cell = np.array([m[c] * self.cell[c] for c in range(3)]) |
|
|
|
return self |
|
|
|
def repeat(self, rep): |
|
"""Create new repeated atoms object. |
|
|
|
The *rep* argument should be a sequence of three positive |
|
integers like *(2,3,1)* or a single integer (*r*) equivalent |
|
to *(r,r,r)*.""" |
|
|
|
atoms = self.copy() |
|
atoms *= rep |
|
return atoms |
|
|
|
def __mul__(self, rep): |
|
return self.repeat(rep) |
|
|
|
def translate(self, displacement): |
|
"""Translate atomic positions. |
|
|
|
The displacement argument can be a float an xyz vector or an |
|
nx3 array (where n is the number of atoms).""" |
|
|
|
self.arrays['positions'] += np.array(displacement) |
|
|
|
def center(self, vacuum=None, axis=(0, 1, 2), about=None): |
|
"""Center atoms in unit cell. |
|
|
|
Centers the atoms in the unit cell, so there is the same |
|
amount of vacuum on all sides. |
|
|
|
vacuum: float (default: None) |
|
If specified adjust the amount of vacuum when centering. |
|
If vacuum=10.0 there will thus be 10 Angstrom of vacuum |
|
on each side. |
|
axis: int or sequence of ints |
|
Axis or axes to act on. Default: Act on all axes. |
|
about: float or array (default: None) |
|
If specified, center the atoms about <about>. |
|
I.e., about=(0., 0., 0.) (or just "about=0.", interpreted |
|
identically), to center about the origin. |
|
""" |
|
|
|
# Find the orientations of the faces of the unit cell |
|
cell = self.cell.complete() |
|
dirs = np.zeros_like(cell) |
|
|
|
lengths = cell.lengths() |
|
for i in range(3): |
|
dirs[i] = np.cross(cell[i - 1], cell[i - 2]) |
|
dirs[i] /= np.linalg.norm(dirs[i]) |
|
if dirs[i] @ cell[i] < 0.0: |
|
dirs[i] *= -1 |
|
|
|
if isinstance(axis, int): |
|
axes = (axis,) |
|
else: |
|
axes = axis |
|
|
|
# Now, decide how much each basis vector should be made longer |
|
pos = self.positions |
|
longer = np.zeros(3) |
|
shift = np.zeros(3) |
|
for i in axes: |
|
if len(pos): |
|
scalarprod = pos @ dirs[i] |
|
p0 = scalarprod.min() |
|
p1 = scalarprod.max() |
|
else: |
|
p0 = 0 |
|
p1 = 0 |
|
height = cell[i] @ dirs[i] |
|
if vacuum is not None: |
|
lng = (p1 - p0 + 2 * vacuum) - height |
|
else: |
|
lng = 0.0 # Do not change unit cell size! |
|
top = lng + height - p1 |
|
shf = 0.5 * (top - p0) |
|
cosphi = cell[i] @ dirs[i] / lengths[i] |
|
longer[i] = lng / cosphi |
|
shift[i] = shf / cosphi |
|
|
|
# Now, do it! |
|
translation = np.zeros(3) |
|
for i in axes: |
|
nowlen = lengths[i] |
|
if vacuum is not None: |
|
self.cell[i] = cell[i] * (1 + longer[i] / nowlen) |
|
translation += shift[i] * cell[i] / nowlen |
|
|
|
# We calculated translations using the completed cell, |
|
# so directions without cell vectors will have been centered |
|
# along a "fake" vector of length 1. |
|
# Therefore, we adjust by -0.5: |
|
if not any(self.cell[i]): |
|
translation[i] -= 0.5 |
|
|
|
# Optionally, translate to center about a point in space. |
|
if about is not None: |
|
for vector in self.cell: |
|
translation -= vector / 2.0 |
|
translation += about |
|
|
|
self.positions += translation |
|
|
|
def get_center_of_mass(self, scaled=False): |
|
"""Get the center of mass. |
|
|
|
If scaled=True the center of mass in scaled coordinates |
|
is returned.""" |
|
masses = self.get_masses() |
|
com = masses @ self.positions / masses.sum() |
|
if scaled: |
|
return self.cell.scaled_positions(com) |
|
else: |
|
return com |
|
|
|
def set_center_of_mass(self, com, scaled=False): |
|
"""Set the center of mass. |
|
|
|
If scaled=True the center of mass is expected in scaled coordinates. |
|
Constraints are considered for scaled=False. |
|
""" |
|
old_com = self.get_center_of_mass(scaled=scaled) |
|
difference = old_com - com |
|
if scaled: |
|
self.set_scaled_positions(self.get_scaled_positions() + difference) |
|
else: |
|
self.set_positions(self.get_positions() + difference) |
|
|
|
def get_moments_of_inertia(self, vectors=False): |
|
"""Get the moments of inertia along the principal axes. |
|
|
|
The three principal moments of inertia are computed from the |
|
eigenvalues of the symmetric inertial tensor. Periodic boundary |
|
conditions are ignored. Units of the moments of inertia are |
|
amu*angstrom**2. |
|
""" |
|
com = self.get_center_of_mass() |
|
positions = self.get_positions() |
|
positions -= com # translate center of mass to origin |
|
masses = self.get_masses() |
|
|
|
# Initialize elements of the inertial tensor |
|
I11 = I22 = I33 = I12 = I13 = I23 = 0.0 |
|
for i in range(len(self)): |
|
x, y, z = positions[i] |
|
m = masses[i] |
|
|
|
I11 += m * (y ** 2 + z ** 2) |
|
I22 += m * (x ** 2 + z ** 2) |
|
I33 += m * (x ** 2 + y ** 2) |
|
I12 += -m * x * y |
|
I13 += -m * x * z |
|
I23 += -m * y * z |
|
|
|
I = np.array([[I11, I12, I13], |
|
[I12, I22, I23], |
|
[I13, I23, I33]]) |
|
|
|
evals, evecs = np.linalg.eigh(I) |
|
if vectors: |
|
return evals, evecs.transpose() |
|
else: |
|
return evals |
|
|
|
def get_angular_momentum(self): |
|
"""Get total angular momentum with respect to the center of mass.""" |
|
com = self.get_center_of_mass() |
|
positions = self.get_positions() |
|
positions -= com # translate center of mass to origin |
|
return np.cross(positions, self.get_momenta()).sum(0) |
|
|
|
def rotate(self, a, v, center=(0, 0, 0), rotate_cell=False): |
|
"""Rotate atoms based on a vector and an angle, or two vectors. |
|
|
|
Parameters: |
|
|
|
a = None: |
|
Angle that the atoms is rotated around the vector 'v'. 'a' |
|
can also be a vector and then 'a' is rotated |
|
into 'v'. |
|
|
|
v: |
|
Vector to rotate the atoms around. Vectors can be given as |
|
strings: 'x', '-x', 'y', ... . |
|
|
|
center = (0, 0, 0): |
|
The center is kept fixed under the rotation. Use 'COM' to fix |
|
the center of mass, 'COP' to fix the center of positions or |
|
'COU' to fix the center of cell. |
|
|
|
rotate_cell = False: |
|
If true the cell is also rotated. |
|
|
|
Examples: |
|
|
|
Rotate 90 degrees around the z-axis, so that the x-axis is |
|
rotated into the y-axis: |
|
|
|
>>> atoms = Atoms() |
|
>>> atoms.rotate(90, 'z') |
|
>>> atoms.rotate(90, (0, 0, 1)) |
|
>>> atoms.rotate(-90, '-z') |
|
>>> atoms.rotate('x', 'y') |
|
>>> atoms.rotate((1, 0, 0), (0, 1, 0)) |
|
""" |
|
|
|
if not isinstance(a, numbers.Real): |
|
a, v = v, a |
|
|
|
norm = np.linalg.norm |
|
v = string2vector(v) |
|
|
|
normv = norm(v) |
|
|
|
if normv == 0.0: |
|
raise ZeroDivisionError('Cannot rotate: norm(v) == 0') |
|
|
|
if isinstance(a, numbers.Real): |
|
a *= pi / 180 |
|
v /= normv |
|
c = cos(a) |
|
s = sin(a) |
|
else: |
|
v2 = string2vector(a) |
|
v /= normv |
|
normv2 = np.linalg.norm(v2) |
|
if normv2 == 0: |
|
raise ZeroDivisionError('Cannot rotate: norm(a) == 0') |
|
v2 /= norm(v2) |
|
c = np.dot(v, v2) |
|
v = np.cross(v, v2) |
|
s = norm(v) |
|
# In case *v* and *a* are parallel, np.cross(v, v2) vanish |
|
# and can't be used as a rotation axis. However, in this |
|
# case any rotation axis perpendicular to v2 will do. |
|
eps = 1e-7 |
|
if s < eps: |
|
v = np.cross((0, 0, 1), v2) |
|
if norm(v) < eps: |
|
v = np.cross((1, 0, 0), v2) |
|
assert norm(v) >= eps |
|
elif s > 0: |
|
v /= s |
|
|
|
center = self._centering_as_array(center) |
|
|
|
p = self.arrays['positions'] - center |
|
self.arrays['positions'][:] = (c * p - |
|
np.cross(p, s * v) + |
|
np.outer(np.dot(p, v), (1.0 - c) * v) + |
|
center) |
|
if rotate_cell: |
|
rotcell = self.get_cell() |
|
rotcell[:] = (c * rotcell - |
|
np.cross(rotcell, s * v) + |
|
np.outer(np.dot(rotcell, v), (1.0 - c) * v)) |
|
self.set_cell(rotcell) |
|
|
|
def _centering_as_array(self, center): |
|
if isinstance(center, str): |
|
if center.lower() == 'com': |
|
center = self.get_center_of_mass() |
|
elif center.lower() == 'cop': |
|
center = self.get_positions().mean(axis=0) |
|
elif center.lower() == 'cou': |
|
center = self.get_cell().sum(axis=0) / 2 |
|
else: |
|
raise ValueError('Cannot interpret center') |
|
else: |
|
center = np.array(center, float) |
|
return center |
|
|
|
def euler_rotate(self, phi=0.0, theta=0.0, psi=0.0, center=(0, 0, 0)): |
|
"""Rotate atoms via Euler angles (in degrees). |
|
|
|
See e.g http://mathworld.wolfram.com/EulerAngles.html for explanation. |
|
|
|
Parameters: |
|
|
|
center : |
|
The point to rotate about. A sequence of length 3 with the |
|
coordinates, or 'COM' to select the center of mass, 'COP' to |
|
select center of positions or 'COU' to select center of cell. |
|
phi : |
|
The 1st rotation angle around the z axis. |
|
theta : |
|
Rotation around the x axis. |
|
psi : |
|
2nd rotation around the z axis. |
|
|
|
""" |
|
center = self._centering_as_array(center) |
|
|
|
phi *= pi / 180 |
|
theta *= pi / 180 |
|
psi *= pi / 180 |
|
|
|
# First move the molecule to the origin In contrast to MATLAB, |
|
# numpy broadcasts the smaller array to the larger row-wise, |
|
# so there is no need to play with the Kronecker product. |
|
rcoords = self.positions - center |
|
# First Euler rotation about z in matrix form |
|
D = np.array(((cos(phi), sin(phi), 0.), |
|
(-sin(phi), cos(phi), 0.), |
|
(0., 0., 1.))) |
|
# Second Euler rotation about x: |
|
C = np.array(((1., 0., 0.), |
|
(0., cos(theta), sin(theta)), |
|
(0., -sin(theta), cos(theta)))) |
|
# Third Euler rotation, 2nd rotation about z: |
|
B = np.array(((cos(psi), sin(psi), 0.), |
|
(-sin(psi), cos(psi), 0.), |
|
(0., 0., 1.))) |
|
# Total Euler rotation |
|
A = np.dot(B, np.dot(C, D)) |
|
# Do the rotation |
|
rcoords = np.dot(A, np.transpose(rcoords)) |
|
# Move back to the rotation point |
|
self.positions = np.transpose(rcoords) + center |
|
|
|
def get_dihedral(self, a0, a1, a2, a3, mic=False): |
|
"""Calculate dihedral angle. |
|
|
|
Calculate dihedral angle (in degrees) between the vectors a0->a1 |
|
and a2->a3. |
|
|
|
Use mic=True to use the Minimum Image Convention and calculate the |
|
angle across periodic boundaries. |
|
""" |
|
return self.get_dihedrals([[a0, a1, a2, a3]], mic=mic)[0] |
|
|
|
def get_dihedrals(self, indices, mic=False): |
|
"""Calculate dihedral angles. |
|
|
|
Calculate dihedral angles (in degrees) between the list of vectors |
|
a0->a1 and a2->a3, where a0, a1, a2 and a3 are in each row of indices. |
|
|
|
Use mic=True to use the Minimum Image Convention and calculate the |
|
angles across periodic boundaries. |
|
""" |
|
indices = np.array(indices) |
|
assert indices.shape[1] == 4 |
|
|
|
a0s = self.positions[indices[:, 0]] |
|
a1s = self.positions[indices[:, 1]] |
|
a2s = self.positions[indices[:, 2]] |
|
a3s = self.positions[indices[:, 3]] |
|
|
|
# vectors 0->1, 1->2, 2->3 |
|
v0 = a1s - a0s |
|
v1 = a2s - a1s |
|
v2 = a3s - a2s |
|
|
|
cell = None |
|
pbc = None |
|
|
|
if mic: |
|
cell = self.cell |
|
pbc = self.pbc |
|
|
|
return get_dihedrals(v0, v1, v2, cell=cell, pbc=pbc) |
|
|
|
def _masked_rotate(self, center, axis, diff, mask): |
|
# do rotation of subgroup by copying it to temporary atoms object |
|
# and then rotating that |
|
# |
|
# recursive object definition might not be the most elegant thing, |
|
# more generally useful might be a rotation function with a mask? |
|
group = self.__class__() |
|
for i in range(len(self)): |
|
if mask[i]: |
|
group += self[i] |
|
group.translate(-center) |
|
group.rotate(diff * 180 / pi, axis) |
|
group.translate(center) |
|
# set positions in original atoms object |
|
j = 0 |
|
for i in range(len(self)): |
|
if mask[i]: |
|
self.positions[i] = group[j].position |
|
j += 1 |
|
|
|
def set_dihedral(self, a1, a2, a3, a4, angle, |
|
mask=None, indices=None): |
|
"""Set the dihedral angle (degrees) between vectors a1->a2 and |
|
a3->a4 by changing the atom indexed by a4. |
|
|
|
If mask is not None, all the atoms described in mask |
|
(read: the entire subgroup) are moved. Alternatively to the mask, |
|
the indices of the atoms to be rotated can be supplied. If both |
|
*mask* and *indices* are given, *indices* overwrites *mask*. |
|
|
|
**Important**: If *mask* or *indices* is given and does not contain |
|
*a4*, *a4* will NOT be moved. In most cases you therefore want |
|
to include *a4* in *mask*/*indices*. |
|
|
|
Example: the following defines a very crude |
|
ethane-like molecule and twists one half of it by 30 degrees. |
|
|
|
>>> atoms = Atoms('HHCCHH', [[-1, 1, 0], [-1, -1, 0], [0, 0, 0], |
|
... [1, 0, 0], [2, 1, 0], [2, -1, 0]]) |
|
>>> atoms.set_dihedral(1, 2, 3, 4, 210, mask=[0, 0, 0, 1, 1, 1]) |
|
""" |
|
|
|
angle *= pi / 180 |
|
|
|
# if not provided, set mask to the last atom in the |
|
# dihedral description |
|
if mask is None and indices is None: |
|
mask = np.zeros(len(self)) |
|
mask[a4] = 1 |
|
elif indices is not None: |
|
mask = [index in indices for index in range(len(self))] |
|
|
|
# compute necessary in dihedral change, from current value |
|
current = self.get_dihedral(a1, a2, a3, a4) * pi / 180 |
|
diff = angle - current |
|
axis = self.positions[a3] - self.positions[a2] |
|
center = self.positions[a3] |
|
self._masked_rotate(center, axis, diff, mask) |
|
|
|
def rotate_dihedral(self, a1, a2, a3, a4, |
|
angle=None, mask=None, indices=None): |
|
"""Rotate dihedral angle. |
|
|
|
Same usage as in :meth:`ase.Atoms.set_dihedral`: Rotate a group by a |
|
predefined dihedral angle, starting from its current configuration. |
|
""" |
|
start = self.get_dihedral(a1, a2, a3, a4) |
|
self.set_dihedral(a1, a2, a3, a4, angle + start, mask, indices) |
|
|
|
def get_angle(self, a1, a2, a3, mic=False): |
|
"""Get angle formed by three atoms. |
|
|
|
Calculate angle in degrees between the vectors a2->a1 and |
|
a2->a3. |
|
|
|
Use mic=True to use the Minimum Image Convention and calculate the |
|
angle across periodic boundaries. |
|
""" |
|
return self.get_angles([[a1, a2, a3]], mic=mic)[0] |
|
|
|
def get_angles(self, indices, mic=False): |
|
"""Get angle formed by three atoms for multiple groupings. |
|
|
|
Calculate angle in degrees between vectors between atoms a2->a1 |
|
and a2->a3, where a1, a2, and a3 are in each row of indices. |
|
|
|
Use mic=True to use the Minimum Image Convention and calculate |
|
the angle across periodic boundaries. |
|
""" |
|
indices = np.array(indices) |
|
assert indices.shape[1] == 3 |
|
|
|
a1s = self.positions[indices[:, 0]] |
|
a2s = self.positions[indices[:, 1]] |
|
a3s = self.positions[indices[:, 2]] |
|
|
|
v12 = a1s - a2s |
|
v32 = a3s - a2s |
|
|
|
cell = None |
|
pbc = None |
|
|
|
if mic: |
|
cell = self.cell |
|
pbc = self.pbc |
|
|
|
return get_angles(v12, v32, cell=cell, pbc=pbc) |
|
|
|
def set_angle(self, a1, a2=None, a3=None, angle=None, mask=None, |
|
indices=None, add=False): |
|
"""Set angle (in degrees) formed by three atoms. |
|
|
|
Sets the angle between vectors *a2*->*a1* and *a2*->*a3*. |
|
|
|
If *add* is `True`, the angle will be changed by the value given. |
|
|
|
Same usage as in :meth:`ase.Atoms.set_dihedral`. |
|
If *mask* and *indices* |
|
are given, *indices* overwrites *mask*. If *mask* and *indices* |
|
are not set, only *a3* is moved.""" |
|
|
|
if any(a is None for a in [a2, a3, angle]): |
|
raise ValueError('a2, a3, and angle must not be None') |
|
|
|
# If not provided, set mask to the last atom in the angle description |
|
if mask is None and indices is None: |
|
mask = np.zeros(len(self)) |
|
mask[a3] = 1 |
|
elif indices is not None: |
|
mask = [index in indices for index in range(len(self))] |
|
|
|
if add: |
|
diff = angle |
|
else: |
|
# Compute necessary in angle change, from current value |
|
diff = angle - self.get_angle(a1, a2, a3) |
|
|
|
diff *= pi / 180 |
|
# Do rotation of subgroup by copying it to temporary atoms object and |
|
# then rotating that |
|
v10 = self.positions[a1] - self.positions[a2] |
|
v12 = self.positions[a3] - self.positions[a2] |
|
v10 /= np.linalg.norm(v10) |
|
v12 /= np.linalg.norm(v12) |
|
axis = np.cross(v10, v12) |
|
center = self.positions[a2] |
|
self._masked_rotate(center, axis, diff, mask) |
|
|
|
def rattle(self, stdev=0.001, seed=None, rng=None): |
|
"""Randomly displace atoms. |
|
|
|
This method adds random displacements to the atomic positions, |
|
taking a possible constraint into account. The random numbers are |
|
drawn from a normal distribution of standard deviation stdev. |
|
|
|
For a parallel calculation, it is important to use the same |
|
seed on all processors! """ |
|
|
|
if seed is not None and rng is not None: |
|
raise ValueError('Please do not provide both seed and rng.') |
|
|
|
if rng is None: |
|
if seed is None: |
|
seed = 42 |
|
rng = np.random.RandomState(seed) |
|
positions = self.arrays['positions'] |
|
self.set_positions(positions + |
|
rng.normal(scale=stdev, size=positions.shape)) |
|
|
|
def get_distance(self, a0, a1, mic=False, vector=False): |
|
"""Return distance between two atoms. |
|
|
|
Use mic=True to use the Minimum Image Convention. |
|
vector=True gives the distance vector (from a0 to a1). |
|
""" |
|
return self.get_distances(a0, [a1], mic=mic, vector=vector)[0] |
|
|
|
def get_distances(self, a, indices, mic=False, vector=False): |
|
"""Return distances of atom No.i with a list of atoms. |
|
|
|
Use mic=True to use the Minimum Image Convention. |
|
vector=True gives the distance vector (from a to self[indices]). |
|
""" |
|
R = self.arrays['positions'] |
|
p1 = [R[a]] |
|
p2 = R[indices] |
|
|
|
cell = None |
|
pbc = None |
|
|
|
if mic: |
|
cell = self.cell |
|
pbc = self.pbc |
|
|
|
D, D_len = get_distances(p1, p2, cell=cell, pbc=pbc) |
|
|
|
if vector: |
|
D.shape = (-1, 3) |
|
return D |
|
else: |
|
D_len.shape = (-1,) |
|
return D_len |
|
|
|
def get_all_distances(self, mic=False, vector=False): |
|
"""Return distances of all of the atoms with all of the atoms. |
|
|
|
Use mic=True to use the Minimum Image Convention. |
|
""" |
|
R = self.arrays['positions'] |
|
|
|
cell = None |
|
pbc = None |
|
|
|
if mic: |
|
cell = self.cell |
|
pbc = self.pbc |
|
|
|
D, D_len = get_distances(R, cell=cell, pbc=pbc) |
|
|
|
if vector: |
|
return D |
|
else: |
|
return D_len |
|
|
|
def set_distance(self, a0, a1, distance, fix=0.5, mic=False, |
|
mask=None, indices=None, add=False, factor=False): |
|
"""Set the distance between two atoms. |
|
|
|
Set the distance between atoms *a0* and *a1* to *distance*. |
|
By default, the center of the two atoms will be fixed. Use |
|
*fix=0* to fix the first atom, *fix=1* to fix the second |
|
atom and *fix=0.5* (default) to fix the center of the bond. |
|
|
|
If *mask* or *indices* are set (*mask* overwrites *indices*), |
|
only the atoms defined there are moved |
|
(see :meth:`ase.Atoms.set_dihedral`). |
|
|
|
When *add* is true, the distance is changed by the value given. |
|
In combination |
|
with *factor* True, the value given is a factor scaling the distance. |
|
|
|
It is assumed that the atoms in *mask*/*indices* move together |
|
with *a1*. If *fix=1*, only *a0* will therefore be moved.""" |
|
|
|
if a0 % len(self) == a1 % len(self): |
|
raise ValueError('a0 and a1 must not be the same') |
|
|
|
if add: |
|
oldDist = self.get_distance(a0, a1, mic=mic) |
|
if factor: |
|
newDist = oldDist * distance |
|
else: |
|
newDist = oldDist + distance |
|
self.set_distance(a0, a1, newDist, fix=fix, mic=mic, |
|
mask=mask, indices=indices, add=False, |
|
factor=False) |
|
return |
|
|
|
R = self.arrays['positions'] |
|
D = np.array([R[a1] - R[a0]]) |
|
|
|
if mic: |
|
D, D_len = find_mic(D, self.cell, self.pbc) |
|
else: |
|
D_len = np.array([np.sqrt((D**2).sum())]) |
|
x = 1.0 - distance / D_len[0] |
|
|
|
if mask is None and indices is None: |
|
indices = [a0, a1] |
|
elif mask: |
|
indices = [i for i in range(len(self)) if mask[i]] |
|
|
|
for i in indices: |
|
if i == a0: |
|
R[a0] += (x * fix) * D[0] |
|
else: |
|
R[i] -= (x * (1.0 - fix)) * D[0] |
|
|
|
def get_scaled_positions(self, wrap=True): |
|
"""Get positions relative to unit cell. |
|
|
|
If wrap is True, atoms outside the unit cell will be wrapped into |
|
the cell in those directions with periodic boundary conditions |
|
so that the scaled coordinates are between zero and one. |
|
|
|
If any cell vectors are zero, the corresponding coordinates |
|
are evaluated as if the cell were completed using |
|
``cell.complete()``. This means coordinates will be Cartesian |
|
as long as the non-zero cell vectors span a Cartesian axis or |
|
plane.""" |
|
|
|
fractional = self.cell.scaled_positions(self.positions) |
|
|
|
if wrap: |
|
for i, periodic in enumerate(self.pbc): |
|
if periodic: |
|
# Yes, we need to do it twice. |
|
# See the scaled_positions.py test. |
|
fractional[:, i] %= 1.0 |
|
fractional[:, i] %= 1.0 |
|
|
|
return fractional |
|
|
|
def set_scaled_positions(self, scaled): |
|
"""Set positions relative to unit cell.""" |
|
self.positions[:] = self.cell.cartesian_positions(scaled) |
|
|
|
def wrap(self, **wrap_kw): |
|
"""Wrap positions to unit cell. |
|
|
|
Parameters: |
|
|
|
wrap_kw: (keyword=value) pairs |
|
optional keywords `pbc`, `center`, `pretty_translation`, `eps`, |
|
see :func:`ase.geometry.wrap_positions` |
|
""" |
|
|
|
if 'pbc' not in wrap_kw: |
|
wrap_kw['pbc'] = self.pbc |
|
|
|
self.positions[:] = self.get_positions(wrap=True, **wrap_kw) |
|
|
|
def get_temperature(self): |
|
"""Get the temperature in Kelvin.""" |
|
dof = len(self) * 3 |
|
for constraint in self._constraints: |
|
dof -= constraint.get_removed_dof(self) |
|
ekin = self.get_kinetic_energy() |
|
return 2 * ekin / (dof * units.kB) |
|
|
|
def __eq__(self, other): |
|
"""Check for identity of two atoms objects. |
|
|
|
Identity means: same positions, atomic numbers, unit cell and |
|
periodic boundary conditions.""" |
|
if not isinstance(other, Atoms): |
|
return False |
|
a = self.arrays |
|
b = other.arrays |
|
return (len(self) == len(other) and |
|
(a['positions'] == b['positions']).all() and |
|
(a['numbers'] == b['numbers']).all() and |
|
(self.cell == other.cell).all() and |
|
(self.pbc == other.pbc).all()) |
|
|
|
def __ne__(self, other): |
|
"""Check if two atoms objects are not equal. |
|
|
|
Any differences in positions, atomic numbers, unit cell or |
|
periodic boundary condtions make atoms objects not equal. |
|
""" |
|
eq = self.__eq__(other) |
|
if eq is NotImplemented: |
|
return eq |
|
else: |
|
return not eq |
|
|
|
# @deprecated('Please use atoms.cell.volume') |
|
# We kind of want to deprecate this, but the ValueError behaviour |
|
# might be desirable. Should we do this? |
|
def get_volume(self): |
|
"""Get volume of unit cell.""" |
|
if self.cell.rank != 3: |
|
raise ValueError( |
|
'You have {0} lattice vectors: volume not defined' |
|
.format(self.cell.rank)) |
|
return self.cell.volume |
|
|
|
def _get_positions(self): |
|
"""Return reference to positions-array for in-place manipulations.""" |
|
return self.arrays['positions'] |
|
|
|
def _set_positions(self, pos): |
|
"""Set positions directly, bypassing constraints.""" |
|
self.arrays['positions'][:] = pos |
|
|
|
positions = property(_get_positions, _set_positions, |
|
doc='Attribute for direct ' + |
|
'manipulation of the positions.') |
|
|
|
def _get_atomic_numbers(self): |
|
"""Return reference to atomic numbers for in-place |
|
manipulations.""" |
|
return self.arrays['numbers'] |
|
|
|
numbers = property(_get_atomic_numbers, set_atomic_numbers, |
|
doc='Attribute for direct ' + |
|
'manipulation of the atomic numbers.') |
|
|
|
@property |
|
def cell(self): |
|
"""The :class:`ase.cell.Cell` for direct manipulation.""" |
|
return self._cellobj |
|
|
|
@cell.setter |
|
def cell(self, cell): |
|
cell = Cell.ascell(cell) |
|
self._cellobj[:] = cell |
|
|
|
def write(self, filename, format=None, **kwargs): |
|
"""Write atoms object to a file. |
|
|
|
see ase.io.write for formats. |
|
kwargs are passed to ase.io.write. |
|
""" |
|
from ase.io import write |
|
write(filename, self, format, **kwargs) |
|
|
|
def iterimages(self): |
|
yield self |
|
|
|
def edit(self): |
|
"""Modify atoms interactively through ASE's GUI viewer. |
|
|
|
Conflicts leading to undesirable behaviour might arise |
|
when matplotlib has been pre-imported with certain |
|
incompatible backends and while trying to use the |
|
plot feature inside the interactive GUI. To circumvent, |
|
please set matplotlib.use('gtk') before calling this |
|
method. |
|
""" |
|
from ase.gui.images import Images |
|
from ase.gui.gui import GUI |
|
images = Images([self]) |
|
gui = GUI(images) |
|
gui.run() |
|
|
|
|
|
def string2vector(v): |
|
if isinstance(v, str): |
|
if v[0] == '-': |
|
return -string2vector(v[1:]) |
|
w = np.zeros(3) |
|
w['xyz'.index(v)] = 1.0 |
|
return w |
|
return np.array(v, float) |
|
|
|
|
|
def default(data, dflt): |
|
"""Helper function for setting default values.""" |
|
if data is None: |
|
return None |
|
elif isinstance(data, (list, tuple)): |
|
newdata = [] |
|
allnone = True |
|
for x in data: |
|
if x is None: |
|
newdata.append(dflt) |
|
else: |
|
newdata.append(x) |
|
allnone = False |
|
if allnone: |
|
return None |
|
return newdata |
|
else: |
|
return data
|
|
|