Coverage for /builds/debichem-team/python-ase/ase/calculators/morse.py: 100.00%
43 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
1import numpy as np
3from ase.calculators.calculator import Calculator, all_changes
4from ase.neighborlist import neighbor_list
5from ase.stress import full_3x3_to_voigt_6_stress
8def fcut(r: np.ndarray, r0: float, r1: float) -> np.ndarray:
9 """Piecewise quintic C^{2,1} regular polynomial for use as a smooth cutoff.
11 Ported from JuLIP.jl.
13 https://github.com/JuliaMolSim/JuLIP.jl/blob/master/src/cutoffs.jl
15 https://en.wikipedia.org/wiki/Smoothstep
17 Parameters
18 ----------
19 r : np.ndarray
20 Distances between atoms.
21 r0 : float
22 Inner cutoff radius.
23 r1 : float
24 Outder cutoff radius.
26 Returns
27 -------
28 np.ndarray
29 Sigmoid-like function smoothly interpolating (r0, 1) and (r1, 0).
31 """""
32 s = 1.0 - (r - r0) / (r1 - r0)
33 return (s >= 1.0) + ((s > 0.0) & (s < 1.0)) * (
34 6.0 * s**5 - 15.0 * s**4 + 10.0 * s**3
35 )
38def fcut_d(r: np.ndarray, r0: float, r1: float) -> np.ndarray:
39 """Derivative of fcut() function defined above."""
40 s = 1.0 - (r - r0) / (r1 - r0)
41 return -(
42 ((s > 0.0) & (s < 1.0))
43 * (30.0 * s**4 - 60.0 * s**3 + 30.0 * s**2)
44 / (r1 - r0)
45 )
48class MorsePotential(Calculator):
49 """Morse potential."""
51 implemented_properties = [
52 'energy', 'energies', 'free_energy', 'forces', 'stress',
53 ]
54 default_parameters = {'epsilon': 1.0,
55 'rho0': 6.0,
56 'r0': 1.0,
57 'rcut1': 1.9,
58 'rcut2': 2.7}
59 nolabel = True
61 def __init__(self, **kwargs):
62 r"""
64 The pairwise energy between atoms *i* and *j* is given by
66 .. math::
68 V_{ij} = \epsilon \left(
69 \mathrm{e}^{-2 \rho_0 (r_{ij} / r_0 - 1)}
70 - 2 \mathrm{e}^{- \rho_0 (r_{ij} / r_0 - 1)}
71 \right)
73 Parameters
74 ----------
75 epsilon : float, default 1.0
76 Absolute minimum depth.
77 r0 : float, default 1.0
78 Minimum distance.
79 rho0 : float, default 6.0
80 Exponential prefactor.
81 The force constant in the potential minimum is given by
83 .. math::
85 k = 2 \epsilon \left(\frac{\rho_0}{r_0}\right)^2.
87 rcut1 : float, default 1.9
88 Distance starting a smooth cutoff normalized by ``r0``.
89 rcut2 : float, default 2.7
90 Distance ending a smooth cutoff normalized by ``r0``.
92 Notes
93 -----
94 The default values are chosen to be similar as Lennard-Jones.
96 """
97 Calculator.__init__(self, **kwargs)
99 def calculate(self, atoms=None, properties=['energy'],
100 system_changes=all_changes):
101 Calculator.calculate(self, atoms, properties, system_changes)
102 epsilon = self.parameters['epsilon']
103 rho0 = self.parameters['rho0']
104 r0 = self.parameters['r0']
105 rcut1 = self.parameters['rcut1'] * r0
106 rcut2 = self.parameters['rcut2'] * r0
108 number_of_atoms = len(self.atoms)
110 forces = np.zeros((number_of_atoms, 3))
112 i, _j, d, D = neighbor_list('ijdD', atoms, rcut2)
113 dhat = (D / d[:, None]).T
115 expf = np.exp(rho0 * (1.0 - d / r0))
117 cutoff_fn = fcut(d, rcut1, rcut2)
118 d_cutoff_fn = fcut_d(d, rcut1, rcut2)
120 pairwise_energies = epsilon * expf * (expf - 2.0)
121 self.results['energies'] = np.bincount(
122 i,
123 weights=0.5 * (pairwise_energies * cutoff_fn),
124 minlength=number_of_atoms,
125 )
126 self.results['energy'] = self.results['energies'].sum()
127 self.results['free_energy'] = self.results['energy']
129 # derivatives of `pair_energies` with respect to `d`
130 de = (-2.0 * epsilon * rho0 / r0) * expf * (expf - 1.0)
132 # smoothened `de`
133 de = de * cutoff_fn + pairwise_energies * d_cutoff_fn
135 de_vec = (de * dhat).T
136 for dim in range(3):
137 forces[:, dim] = np.bincount(
138 i,
139 weights=de_vec[:, dim],
140 minlength=number_of_atoms,
141 )
142 self.results['forces'] = forces
144 if self.atoms.cell.rank == 3:
145 stress = 0.5 * (D.T @ de_vec) / self.atoms.get_volume()
146 self.results['stress'] = full_3x3_to_voigt_6_stress(stress)