Coverage for /builds/ase/ase/ase/calculators/morse.py: 100.00%
44 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
1# fmt: off
3import numpy as np
5from ase.calculators.calculator import Calculator, all_changes
6from ase.neighborlist import neighbor_list as ase_neighbor_list
7from ase.stress import full_3x3_to_voigt_6_stress
10def fcut(r: np.ndarray, r0: float, r1: float) -> np.ndarray:
11 """Piecewise quintic C^{2,1} regular polynomial for use as a smooth cutoff.
13 Ported from JuLIP.jl.
15 https://github.com/JuliaMolSim/JuLIP.jl/blob/master/src/cutoffs.jl
17 https://en.wikipedia.org/wiki/Smoothstep
19 Parameters
20 ----------
21 r : np.ndarray
22 Distances between atoms.
23 r0 : float
24 Inner cutoff radius.
25 r1 : float
26 Outder cutoff radius.
28 Returns
29 -------
30 np.ndarray
31 Sigmoid-like function smoothly interpolating (r0, 1) and (r1, 0).
33 """""
34 s = 1.0 - (r - r0) / (r1 - r0)
35 return (s >= 1.0) + ((s > 0.0) & (s < 1.0)) * (
36 6.0 * s**5 - 15.0 * s**4 + 10.0 * s**3
37 )
40def fcut_d(r: np.ndarray, r0: float, r1: float) -> np.ndarray:
41 """Derivative of fcut() function defined above."""
42 s = 1.0 - (r - r0) / (r1 - r0)
43 return -(
44 ((s > 0.0) & (s < 1.0))
45 * (30.0 * s**4 - 60.0 * s**3 + 30.0 * s**2)
46 / (r1 - r0)
47 )
50class MorsePotential(Calculator):
51 """Morse potential."""
53 implemented_properties = [
54 'energy', 'energies', 'free_energy', 'forces', 'stress',
55 ]
56 default_parameters = {'epsilon': 1.0,
57 'rho0': 6.0,
58 'r0': 1.0,
59 'rcut1': 1.9,
60 'rcut2': 2.7}
61 nolabel = True
63 def __init__(self, neighbor_list=ase_neighbor_list, **kwargs):
64 r"""
66 The pairwise energy between atoms *i* and *j* is given by
68 .. math::
70 V_{ij} = \epsilon \left(
71 \mathrm{e}^{-2 \rho_0 (r_{ij} / r_0 - 1)}
72 - 2 \mathrm{e}^{- \rho_0 (r_{ij} / r_0 - 1)}
73 \right)
75 Parameters
76 ----------
77 epsilon : float, default 1.0
78 Absolute minimum depth.
79 r0 : float, default 1.0
80 Minimum distance.
81 rho0 : float, default 6.0
82 Exponential prefactor.
83 The force constant in the potential minimum is given by
85 .. math::
87 k = 2 \epsilon \left(\frac{\rho_0}{r_0}\right)^2.
89 rcut1 : float, default 1.9
90 Distance starting a smooth cutoff normalized by ``r0``.
91 rcut2 : float, default 2.7
92 Distance ending a smooth cutoff normalized by ``r0``.
93 neighbor_list : callable, optional
94 neighbor_list function compatible with
95 ase.neighborlist.neighbor_list
97 Notes
98 -----
99 The default values are chosen to be similar as Lennard-Jones.
101 """
102 self.neighbor_list = neighbor_list
103 Calculator.__init__(self, **kwargs)
105 def calculate(self, atoms=None, properties=['energy'],
106 system_changes=all_changes):
107 Calculator.calculate(self, atoms, properties, system_changes)
108 epsilon = self.parameters['epsilon']
109 rho0 = self.parameters['rho0']
110 r0 = self.parameters['r0']
111 rcut1 = self.parameters['rcut1'] * r0
112 rcut2 = self.parameters['rcut2'] * r0
114 number_of_atoms = len(self.atoms)
116 forces = np.zeros((number_of_atoms, 3))
118 i, _j, d, D = self.neighbor_list('ijdD', atoms, rcut2)
119 dhat = (D / d[:, None]).T
121 expf = np.exp(rho0 * (1.0 - d / r0))
123 cutoff_fn = fcut(d, rcut1, rcut2)
124 d_cutoff_fn = fcut_d(d, rcut1, rcut2)
126 pairwise_energies = epsilon * expf * (expf - 2.0)
127 self.results['energies'] = np.bincount(
128 i,
129 weights=0.5 * (pairwise_energies * cutoff_fn),
130 minlength=number_of_atoms,
131 )
132 self.results['energy'] = self.results['energies'].sum()
133 self.results['free_energy'] = self.results['energy']
135 # derivatives of `pair_energies` with respect to `d`
136 de = (-2.0 * epsilon * rho0 / r0) * expf * (expf - 1.0)
138 # smoothened `de`
139 de = de * cutoff_fn + pairwise_energies * d_cutoff_fn
141 de_vec = (de * dhat).T
142 for dim in range(3):
143 forces[:, dim] = np.bincount(
144 i,
145 weights=de_vec[:, dim],
146 minlength=number_of_atoms,
147 )
148 self.results['forces'] = forces
150 if self.atoms.cell.rank == 3:
151 stress = 0.5 * (D.T @ de_vec) / self.atoms.get_volume()
152 self.results['stress'] = full_3x3_to_voigt_6_stress(stress)