Coverage for ase / optimize / cellawarebfgs.py: 100.00%
67 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-30 08:22 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-30 08:22 +0000
1# fmt: off
3import time
4from typing import IO
6import numpy as np
8from ase import Atoms
9from ase.geometry import cell_to_cellpar
10from ase.optimize import BFGS
11from ase.optimize.optimize import Dynamics
12from ase.units import GPa
15def calculate_isotropic_elasticity_tensor(bulk_modulus, poisson_ratio,
16 suppress_rotation=0):
17 """
18 Parameters
19 ----------
20 bulk_modulus Bulk Modulus of the isotropic system used to set up the
21 Hessian (in ASE units (eV/Å^3)).
23 poisson_ratio Poisson ratio of the isotropic system used to set up the
24 initial Hessian (unitless, between -1 and 0.5).
26 suppress_rotation The rank-2 matrix C_ijkl.reshape((9,9)) has by
27 default 6 non-zero eigenvalues, because energy is
28 invariant to orthonormal rotations of the cell
29 vector. This serves as a bad initial Hessian due to 3
30 zero eigenvalues. Suppress rotation sets a value for
31 those zero eigenvalues.
33 Returns C_ijkl
34 """
36 # https://scienceworld.wolfram.com/physics/LameConstants.html
37 _lambda = 3 * bulk_modulus * poisson_ratio / (1 + 1 * poisson_ratio)
38 _mu = _lambda * (1 - 2 * poisson_ratio) / (2 * poisson_ratio)
40 # https://en.wikipedia.org/wiki/Elasticity_tensor
41 g_ij = np.eye(3)
43 # Construct 4th rank Elasticity tensor for isotropic systems
44 C_ijkl = _lambda * np.einsum('ij,kl->ijkl', g_ij, g_ij)
45 C_ijkl += _mu * (np.einsum('ik,jl->ijkl', g_ij, g_ij) +
46 np.einsum('il,kj->ijkl', g_ij, g_ij))
48 # Supplement the tensor with suppression of pure rotations that are right
49 # now 0 eigenvalues.
50 # Loop over all basis vectors of skew symmetric real matrix
51 for i, j in ((0, 1), (0, 2), (1, 2)):
52 Q = np.zeros((3, 3))
53 Q[i, j], Q[j, i] = 1, -1
54 C_ijkl += (np.einsum('ij,kl->ijkl', Q, Q)
55 * suppress_rotation / 2)
57 return C_ijkl
60class CellAwareBFGS(BFGS):
61 def __init__(
62 self,
63 atoms: Atoms,
64 restart: str | None = None,
65 logfile: IO | str = '-',
66 trajectory: str | None = None,
67 append_trajectory: bool = False,
68 maxstep: float | None = None,
69 bulk_modulus: float | None = 145 * GPa,
70 poisson_ratio: float | None = 0.3,
71 alpha: float | None = None,
72 long_output: bool | None = False,
73 **kwargs,
74 ):
75 self.bulk_modulus = bulk_modulus
76 self.poisson_ratio = poisson_ratio
77 self.long_output = long_output
78 super().__init__(
79 atoms=atoms, restart=restart, logfile=logfile,
80 trajectory=trajectory, maxstep=maxstep,
81 alpha=alpha, append_trajectory=append_trajectory,
82 **kwargs)
83 assert not isinstance(atoms, Atoms)
84 if hasattr(atoms, 'exp_cell_factor'):
85 assert atoms.exp_cell_factor == 1.0
87 def initialize(self):
88 super().initialize()
89 C_ijkl = calculate_isotropic_elasticity_tensor(
90 self.bulk_modulus,
91 self.poisson_ratio,
92 suppress_rotation=self.alpha)
93 cell_H = self.H0[-9:, -9:]
94 ind = np.where(self.atoms.mask.ravel() != 0)[0]
95 cell_H[np.ix_(ind, ind)] = C_ijkl.reshape((9, 9))[
96 np.ix_(ind, ind)] * self.atoms.atoms.cell.volume
98 def gradient_converged(self, gradient):
99 # XXX currently ignoring gradient
100 forces = self.atoms.atoms.get_forces()
101 stress = self.atoms.atoms.get_stress(voigt=False) * self.atoms.mask
102 return np.max(np.sum(forces**2, axis=1))**0.5 < self.fmax and \
103 np.max(np.abs(stress)) < self.smax
105 def run(self, fmax=0.05, smax=0.005, steps=None):
106 """ call Dynamics.run and keep track of fmax"""
107 self.fmax = fmax
108 self.smax = smax
109 if steps is not None:
110 return Dynamics.run(self, steps=steps)
111 return Dynamics.run(self)
113 def log(self, gradient):
114 # XXX ignoring gradient
115 forces = self.atoms.atoms.get_forces()
116 fmax = (forces ** 2).sum(axis=1).max() ** 0.5
117 e = self.optimizable.get_value()
118 T = time.localtime()
119 smax = abs(self.atoms.atoms.get_stress(voigt=False) *
120 self.atoms.mask).max()
121 volume = self.atoms.atoms.cell.volume
122 if self.logfile is not None:
123 name = self.__class__.__name__
124 if self.nsteps == 0:
125 args = (" " * len(name),
126 "Step", "Time", "Energy", "fmax", "smax", "volume")
127 msg = "\n%s %4s %8s %15s %15s %15s %15s" % args
128 if self.long_output:
129 msg += ("%8s %8s %8s %8s %8s %8s" %
130 ('A', 'B', 'C', 'α', 'β', 'γ'))
131 msg += '\n'
132 self.logfile.write(msg)
134 ast = ''
135 args = (name, self.nsteps, T[3], T[4], T[5], e, ast, fmax, smax,
136 volume)
137 msg = ("%s: %3d %02d:%02d:%02d %15.6f%1s %15.6f %15.6f %15.6f" %
138 args)
139 if self.long_output:
140 msg += ("%8.3f %8.3f %8.3f %8.3f %8.3f %8.3f" %
141 tuple(cell_to_cellpar(self.atoms.atoms.cell)))
142 msg += '\n'
143 self.logfile.write(msg)