from warnings import warn
import numpy as np
from ase.constraints.constraint import FixConstraint
from ase.spacegroup.symmetrize import (
prep_symmetry,
refine_symmetry,
symmetrize_rank1,
symmetrize_rank2,
)
from ase.stress import full_3x3_to_voigt_6_stress, voigt_6_to_full_3x3_stress
[docs]
class FixSymmetry(FixConstraint):
"""
Constraint to preserve spacegroup symmetry during optimisation.
Requires spglib package to be available.
"""
def __init__(
self,
atoms,
symprec=0.01,
adjust_positions=True,
adjust_cell=True,
verbose=False,
):
self.atoms = atoms.copy()
self.symprec = symprec
self.verbose = verbose
refine_symmetry(atoms, symprec, self.verbose) # refine initial symmetry
sym = prep_symmetry(atoms, symprec, self.verbose)
self.rotations, self.translations, self.symm_map = sym
self.do_adjust_positions = adjust_positions
self.do_adjust_cell = adjust_cell
def adjust_cell(self, atoms, cell):
if not self.do_adjust_cell:
return
# stress should definitely be symmetrized as a rank 2 tensor
# UnitCellFilter uses deformation gradient as cell DOF with steps
# dF = stress.F^-T quantity that should be symmetrized is therefore dF .
# F^T assume prev F = I, so just symmetrize dF
cur_cell = atoms.get_cell()
cur_cell_inv = atoms.cell.reciprocal().T
# F defined such that cell = cur_cell . F^T
# assume prev F = I, so dF = F - I
delta_deform_grad = np.dot(cur_cell_inv, cell).T - np.eye(3)
# symmetrization doesn't work properly with large steps, since
# it depends on current cell, and cell is being changed by deformation
# gradient
max_delta_deform_grad = np.max(np.abs(delta_deform_grad))
if max_delta_deform_grad > 0.25:
raise RuntimeError(
'FixSymmetry adjust_cell does not work properly'
' with large deformation gradient step {} > 0.25'.format(
max_delta_deform_grad
)
)
elif max_delta_deform_grad > 0.15:
warn(
'FixSymmetry adjust_cell may be ill behaved with large '
'deformation gradient step {}'.format(max_delta_deform_grad)
)
symmetrized_delta_deform_grad = symmetrize_rank2(
cur_cell, cur_cell_inv, delta_deform_grad, self.rotations
)
cell[:] = np.dot(
cur_cell, (symmetrized_delta_deform_grad + np.eye(3)).T
)
def adjust_positions(self, atoms, new):
if not self.do_adjust_positions:
return
# symmetrize changes in position as rank 1 tensors
step = new - atoms.positions
symmetrized_step = symmetrize_rank1(
atoms.get_cell(),
atoms.cell.reciprocal().T,
step,
self.rotations,
self.translations,
self.symm_map,
)
new[:] = atoms.positions + symmetrized_step
def adjust_forces(self, atoms, forces):
# symmetrize forces as rank 1 tensors
# print('adjusting forces')
forces[:] = symmetrize_rank1(
atoms.get_cell(),
atoms.cell.reciprocal().T,
forces,
self.rotations,
self.translations,
self.symm_map,
)
def adjust_stress(self, atoms, stress):
# symmetrize stress as rank 2 tensor
raw_stress = voigt_6_to_full_3x3_stress(stress)
symmetrized_stress = symmetrize_rank2(
atoms.get_cell(),
atoms.cell.reciprocal().T,
raw_stress,
self.rotations,
)
stress[:] = full_3x3_to_voigt_6_stress(symmetrized_stress)
def index_shuffle(self, atoms, ind):
if len(atoms) != len(ind) or len(set(ind)) != len(ind):
raise RuntimeError(
'FixSymmetry can only accomodate atom'
' permutions, and len(Atoms) == {} '
'!= len(ind) == {} or ind has duplicates'.format(
len(atoms), len(ind)
)
)
ind_reversed = np.zeros((len(ind)), dtype=int)
ind_reversed[ind] = range(len(ind))
new_symm_map = []
for sm in self.symm_map:
new_sm = np.array([-1] * len(atoms))
for at_i in range(len(ind)):
new_sm[ind_reversed[at_i]] = ind_reversed[sm[at_i]]
new_symm_map.append(new_sm)
self.symm_map = new_symm_map
def todict(self):
return {
'name': 'FixSymmetry',
'kwargs': {
'atoms': self.atoms,
'symprec': self.symprec,
'adjust_positions': self.do_adjust_positions,
'adjust_cell': self.do_adjust_cell,
'verbose': self.verbose,
},
}