Coverage for /builds/debichem-team/python-ase/ase/calculators/fd.py: 100.00%
58 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-03-06 04:00 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-03-06 04:00 +0000
1from collections.abc import Iterable
2from typing import Optional
4import numpy as np
6from ase import Atoms
7from ase.calculators.calculator import BaseCalculator, all_properties
10class FiniteDifferenceCalculator(BaseCalculator):
11 """Wrapper calculator using the finite-difference method.
13 The forces and the stress are computed using the finite-difference method.
15 .. versionadded:: 3.24.0
16 """
18 implemented_properties = all_properties
20 def __init__(
21 self,
22 calc: BaseCalculator,
23 eps_disp: float = 1e-6,
24 eps_strain: float = 1e-6,
25 ) -> None:
26 """
28 Parameters
29 ----------
30 calc : :class:`~ase.calculators.calculator.BaseCalculator`
31 ASE Calculator object to be wrapped.
32 eps_disp : float, default 1e-6
33 Displacement used for computing forces.
34 eps_strain : float, default 1e-6
35 Strain used for computing stress.
37 """
38 super().__init__()
39 self.calc = calc
40 self.eps_disp = eps_disp
41 self.eps_strain = eps_strain
43 def calculate(self, atoms: Atoms, properties, system_changes) -> None:
44 atoms = atoms.copy() # copy to not mess up original `atoms`
45 atoms.calc = self.calc
46 self.results = {
47 'energy': atoms.get_potential_energy(),
48 'forces': calculate_numerical_forces(atoms, eps=self.eps_disp),
49 'stress': calculate_numerical_stress(atoms, eps=self.eps_strain),
50 }
51 self.results['free_energy'] = self.results['energy']
54def _numeric_force(
55 atoms: Atoms,
56 iatom: int,
57 icart: int,
58 eps: float = 1e-6,
59) -> float:
60 """Calculate numerical force on a specific atom along a specific direction.
62 Parameters
63 ----------
64 atoms : :class:`~ase.Atoms`
65 ASE :class:`~ase.Atoms` object.
66 iatom : int
67 Index of atoms.
68 icart : {0, 1, 2}
69 Index of Cartesian component.
70 eps : float, default 1e-6
71 Displacement.
73 """
74 p0 = atoms.get_positions()
75 p = p0.copy()
76 p[iatom, icart] = p0[iatom, icart] + eps
77 atoms.set_positions(p, apply_constraint=False)
78 eplus = atoms.get_potential_energy()
79 p[iatom, icart] = p0[iatom, icart] - eps
80 atoms.set_positions(p, apply_constraint=False)
81 eminus = atoms.get_potential_energy()
82 atoms.set_positions(p0, apply_constraint=False)
83 return (eminus - eplus) / (2 * eps)
86def calculate_numerical_forces(
87 atoms: Atoms,
88 eps: float = 1e-6,
89 iatoms: Optional[Iterable[int]] = None,
90 icarts: Optional[Iterable[int]] = None,
91) -> np.ndarray:
92 """Calculate forces numerically based on the finite-difference method.
94 Parameters
95 ----------
96 atoms : :class:`~ase.Atoms`
97 ASE :class:`~ase.Atoms` object.
98 eps : float, default 1e-6
99 Displacement.
100 iatoms : Optional[Iterable[int]]
101 Indices of atoms for which forces are computed.
102 By default, all atoms are considered.
103 icarts : Optional[Iterable[int]]
104 Indices of Cartesian coordinates for which forces are computed.
105 By default, all three coordinates are considered.
107 Returns
108 -------
109 forces : np.ndarray
110 Forces computed numerically based on the finite-difference method.
112 """
113 if iatoms is None:
114 iatoms = range(len(atoms))
115 if icarts is None:
116 icarts = [0, 1, 2]
117 return np.array(
118 [
119 [_numeric_force(atoms, iatom, icart, eps) for icart in icarts]
120 for iatom in iatoms
121 ]
122 )
125def calculate_numerical_stress(
126 atoms: Atoms,
127 eps: float = 1e-6,
128 voigt: bool = True,
129) -> np.ndarray:
130 """Calculate stress numerically based on the finite-difference method.
132 Parameters
133 ----------
134 atoms : :class:`~ase.Atoms`
135 ASE :class:`~ase.Atoms` object.
136 eps : float, default 1e-6
137 Strain in the Voigt notation.
138 voigt : bool, default True
139 If True, the stress is returned in the Voigt notation.
141 Returns
142 -------
143 stress : np.ndarray
144 Stress computed numerically based on the finite-difference method.
146 """
147 stress = np.zeros((3, 3), dtype=float)
149 cell = atoms.cell.copy()
150 volume = atoms.get_volume()
151 for i in range(3):
152 x = np.eye(3)
153 x[i, i] = 1.0 + eps
154 atoms.set_cell(cell @ x, scale_atoms=True)
155 eplus = atoms.get_potential_energy(force_consistent=True)
157 x[i, i] = 1.0 - eps
158 atoms.set_cell(cell @ x, scale_atoms=True)
159 eminus = atoms.get_potential_energy(force_consistent=True)
161 stress[i, i] = (eplus - eminus) / (2 * eps * volume)
162 x[i, i] = 1.0
164 j = i - 2
165 x[i, j] = x[j, i] = +0.5 * eps
166 atoms.set_cell(cell @ x, scale_atoms=True)
167 eplus = atoms.get_potential_energy(force_consistent=True)
169 x[i, j] = x[j, i] = -0.5 * eps
170 atoms.set_cell(cell @ x, scale_atoms=True)
171 eminus = atoms.get_potential_energy(force_consistent=True)
173 stress[i, j] = stress[j, i] = (eplus - eminus) / (2 * eps * volume)
175 atoms.set_cell(cell, scale_atoms=True)
177 return stress.flat[[0, 4, 8, 5, 2, 1]] if voigt else stress