Skip to content

Commit 8aaa65b

Browse files
authored
Add files via upload
1 parent b1814ef commit 8aaa65b

2 files changed

Lines changed: 385 additions & 0 deletions

File tree

multioptpy/ModelHessian/approx_hessian.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from multioptpy.ModelHessian.fischer import FischerApproxHessian
44
from multioptpy.ModelHessian.fischerd3 import FischerD3ApproxHessian
5+
from multioptpy.ModelHessian.fischerd3old import FischerD3ApproxHessianOld
56
from multioptpy.ModelHessian.fischerd4 import FischerD4ApproxHessian
67
from multioptpy.ModelHessian.gfnff import GFNFFApproxHessian
78
from multioptpy.ModelHessian.gfn0xtb import GFN0XTBApproxHessian
@@ -35,6 +36,9 @@ def main(self, coord, element_list, cart_gradient, approx_hess_type="lindh2007d3
3536
elif "gfn0xtb" in approx_hess_type.lower():
3637
GFN0AH = GFN0XTBApproxHessian()
3738
hess_proj = GFN0AH.main(coord, element_list, cart_gradient)
39+
elif "fischerd3old" in approx_hess_type.lower():
40+
FAHD3O = FischerD3ApproxHessianOld()
41+
hess_proj = FAHD3O.main(coord, element_list, cart_gradient)
3842
elif "fischerd3" in approx_hess_type.lower():
3943
FAHD3 = FischerD3ApproxHessian()
4044
hess_proj = FAHD3.main(coord, element_list, cart_gradient)
Lines changed: 381 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
1+
import numpy as np
2+
from scipy.spatial.distance import cdist # Highly recommended for vectorized distance calculation
3+
from multioptpy.Parameters.parameter import UnitValueLib, covalent_radii_lib
4+
from multioptpy.Utils.calc_tools import Calculationtools
5+
from multioptpy.Parameters.parameter import D3Parameters, D2_C6_coeff_lib, D2_VDW_radii_lib
6+
from multioptpy.Utils.bond_connectivity import BondConnectivity
7+
from multioptpy.ModelHessian.calc_params import torsion2, stretch2, bend2
8+
9+
10+
class FischerD3ApproxHessianOld:
11+
def __init__(self):
12+
"""
13+
Fischer's Model Hessian implementation with D3 dispersion correction
14+
Ref: Fischer and Almlöf, J. Phys. Chem., 1992, 96, 24, 9768–9774
15+
Implementation Ref.: pysisyphus.optimizers.guess_hessians
16+
"""
17+
self.bohr2angstroms = UnitValueLib().bohr2angstroms
18+
self.hartree2kcalmol = UnitValueLib().hartree2kcalmol
19+
self.bond_factor = 1.3 # Bond detection threshold factor
20+
21+
# D3 dispersion correction parameters (default: PBE0)
22+
self.d3_params = D3Parameters()
23+
self.cart_hess = None
24+
25+
def calc_bond_force_const(self, r_ab, r_ab_cov):
26+
"""Calculate force constant for bond stretching using Fischer formula"""
27+
return 0.3601 * np.exp(-1.944 * (r_ab - r_ab_cov))
28+
29+
def calc_bend_force_const(self, r_ab, r_ac, r_ab_cov, r_ac_cov):
30+
"""Calculate force constant for angle bending"""
31+
val = r_ab_cov * r_ac_cov
32+
if abs(val) < 1.0e-10:
33+
return 0.0
34+
35+
return 0.089 + 0.11 / (val) ** (-0.42) * np.exp(
36+
-0.44 * (r_ab + r_ac - r_ab_cov - r_ac_cov)
37+
)
38+
39+
def calc_dihedral_force_const(self, r_ab, r_ab_cov, bond_sum):
40+
"""Calculate force constant for dihedral torsion"""
41+
val = r_ab * r_ab_cov
42+
if abs(val) < 1.0e-10:
43+
return 0.0
44+
return 0.0015 + 14.0 * max(bond_sum, 0) ** 0.57 / (val) ** 4.0 * np.exp(
45+
-2.85 * (r_ab - r_ab_cov)
46+
)
47+
48+
def get_c6_coefficient(self, element_i, element_j):
49+
"""Get C6 coefficient based on D3 model (simplified)"""
50+
c6_i = D2_C6_coeff_lib(element_i)
51+
c6_j = D2_C6_coeff_lib(element_j)
52+
c6_ij = np.sqrt(c6_i * c6_j)
53+
return c6_ij
54+
55+
def get_c8_coefficient(self, element_i, element_j):
56+
"""Calculate C8 coefficient based on D3 model using reference r4r2 values"""
57+
c6_ij = self.get_c6_coefficient(element_i, element_j)
58+
r4r2_i = self.d3_params.get_r4r2(element_i)
59+
r4r2_j = self.d3_params.get_r4r2(element_j)
60+
c8_ij = 3.0 * c6_ij * np.sqrt(r4r2_i * r4r2_j)
61+
return c8_ij
62+
63+
def get_r0_value(self, element_i, element_j):
64+
"""Calculate R0 value for D3 model (characteristic distance for atom pair)"""
65+
try:
66+
r_i = D2_VDW_radii_lib(element_i)
67+
r_j = D2_VDW_radii_lib(element_j)
68+
return r_i + r_j
69+
except:
70+
r_i = covalent_radii_lib(element_i) * 1.5
71+
r_j = covalent_radii_lib(element_j) * 1.5
72+
return r_i + r_j
73+
74+
def d3_damping_function(self, r_ij, r0, order=6):
75+
"""BJ (Becke-Johnson) damping function for D3"""
76+
if order == 6:
77+
a1, a2 = self.d3_params.a1, self.d3_params.a2
78+
else:
79+
a1, a2 = self.d3_params.a1, self.d3_params.a2 + 2.0
80+
81+
denominator = r_ij**order + (a1 * r0 + a2)**order
82+
return r_ij**order / denominator
83+
84+
def d3_hessian_contribution(self, r_vec, r_ij, element_i, element_j):
85+
"""Calculate D3 dispersion contribution to Hessian"""
86+
if r_ij < 0.1:
87+
return np.zeros((3, 3))
88+
89+
c6_ij = self.get_c6_coefficient(element_i, element_j)
90+
c8_ij = self.get_c8_coefficient(element_i, element_j)
91+
r0 = self.get_r0_value(element_i, element_j)
92+
93+
f_damp6 = self.d3_damping_function(r_ij, r0, order=6)
94+
f_damp8 = self.d3_damping_function(r_ij, r0, order=8)
95+
96+
# Derivatives of damping functions
97+
a1, a2 = self.d3_params.a1, self.d3_params.a2
98+
a1_8, a2_8 = self.d3_params.a1, self.d3_params.a2 + 2.0
99+
100+
denom6 = r_ij**6 + (a1 * r0 + a2)**6
101+
denom8 = r_ij**8 + (a1_8 * r0 + a2_8)**8
102+
103+
# df_damp/dr
104+
df_damp6 = 6 * r_ij**5 / denom6 - 6 * r_ij**12 / denom6**2
105+
df_damp8 = 8 * r_ij**7 / denom8 - 8 * r_ij**16 / denom8**2
106+
107+
# dE/dr (Gradient magnitude)
108+
g6 = -self.d3_params.s6 * c6_ij * ((-6.0 / r_ij**7) * f_damp6 + (1.0 / r_ij**6) * df_damp6)
109+
g8 = -self.d3_params.s8 * c8_ij * ((-8.0 / r_ij**9) * f_damp8 + (1.0 / r_ij**8) * df_damp8)
110+
111+
# Unit vector and projection operator
112+
unit_vec = r_vec / r_ij
113+
proj_op = np.outer(unit_vec, unit_vec) # P = r_hat * r_hat^T
114+
115+
# Coefficients for H = (d^2E/dr^2) * P + (1/r * dE/dr) * (I - P)
116+
# Using the simplified structure from the original code for d^2E/dr^2 approximation:
117+
h6_proj_coeff = self.d3_params.s6 * c6_ij / r_ij**8 * (42.0 * f_damp6 - r_ij * df_damp6)
118+
h8_proj_coeff = self.d3_params.s8 * c8_ij / r_ij**10 * (72.0 * f_damp8 - r_ij * df_damp8)
119+
120+
h_proj = h6_proj_coeff + h8_proj_coeff # d^2E/dr^2 approximation
121+
h_perp = (g6 + g8) / r_ij # 1/r * dE/dr (Perpendicular coefficient)
122+
123+
# Construct Hessian matrix
124+
identity = np.eye(3)
125+
hessian = h_proj * proj_op + h_perp * (identity - proj_op)
126+
127+
return hessian
128+
129+
# --- Optimized: Vectorized connectivity calculation ---
130+
def get_bond_connectivity(self, coord, element_list):
131+
"""Calculate bond connectivity matrix and related data (Optimized with vectorization)"""
132+
n_atoms = len(coord)
133+
134+
# 1. Distance matrix (Vectorized)
135+
try:
136+
dist_mat = cdist(coord, coord)
137+
except NameError:
138+
diff = coord[:, None, :] - coord[None, :, :]
139+
dist_mat = np.linalg.norm(diff, axis=-1)
140+
141+
# 2. Covalent radii sums (Vectorized)
142+
cov_radii = np.array([covalent_radii_lib(e) for e in element_list])
143+
pair_cov_radii_mat = cov_radii[:, None] + cov_radii[None, :]
144+
145+
# 3. Bond connectivity matrix
146+
bond_mat = dist_mat <= (pair_cov_radii_mat * self.bond_factor)
147+
np.fill_diagonal(bond_mat, False)
148+
149+
return bond_mat, dist_mat, pair_cov_radii_mat
150+
151+
# --- Optimized: Block assignment using slicing ---
152+
def fischer_bond(self, coord, element_list):
153+
"""Calculate Hessian components for bond stretching (Optimized with slicing)"""
154+
BC = BondConnectivity()
155+
b_c_mat = BC.bond_connect_matrix(element_list, coord)
156+
bond_indices = BC.bond_connect_table(b_c_mat)
157+
158+
for idx in bond_indices:
159+
i, j = idx
160+
r_ij = np.linalg.norm(coord[i] - coord[j])
161+
r_ij_cov = covalent_radii_lib(element_list[i]) + covalent_radii_lib(element_list[j])
162+
163+
force_const = self.calc_bond_force_const(r_ij, r_ij_cov)
164+
165+
t_xyz = np.array([coord[i], coord[j]])
166+
r, b_vec = stretch2(t_xyz)
167+
168+
# Optimized: Use NumPy slicing and outer product
169+
b_vec_i, b_vec_j = b_vec[0], b_vec[1]
170+
171+
H_ii_block = force_const * np.outer(b_vec_i, b_vec_i)
172+
H_jj_block = force_const * np.outer(b_vec_j, b_vec_j)
173+
H_ij_block = force_const * np.outer(b_vec_i, b_vec_j)
174+
H_ji_block = force_const * np.outer(b_vec_j, b_vec_i)
175+
176+
start_i, end_i = 3 * i, 3 * i + 3
177+
start_j, end_j = 3 * j, 3 * j + 3
178+
179+
self.cart_hess[start_i:end_i, start_i:end_i] += H_ii_block
180+
self.cart_hess[start_j:end_j, start_j:end_j] += H_jj_block
181+
self.cart_hess[start_i:end_i, start_j:end_j] += H_ij_block
182+
self.cart_hess[start_j:end_j, start_i:end_i] += H_ji_block
183+
184+
185+
def fischer_angle(self, coord, element_list):
186+
"""Calculate Hessian components for angle bending (Optimized with slicing)"""
187+
BC = BondConnectivity()
188+
b_c_mat = BC.bond_connect_matrix(element_list, coord)
189+
angle_indices = BC.angle_connect_table(b_c_mat)
190+
191+
for idx in angle_indices:
192+
i, j, k = idx # i-j-k angle
193+
194+
# --- Linear molecule handling: Check for linearity before processing ---
195+
r_ij_vec = coord[i] - coord[j]
196+
r_jk_vec = coord[k] - coord[j]
197+
198+
r_ij = np.linalg.norm(r_ij_vec)
199+
r_jk = np.linalg.norm(r_jk_vec)
200+
201+
# Skip if atoms overlap
202+
if r_ij < 0.1 or r_jk < 0.1:
203+
continue
204+
205+
# Check for linearity (180 deg) or overlap (0 deg)
206+
cos_theta = np.dot(r_ij_vec, r_jk_vec) / (r_ij * r_jk)
207+
if abs(cos_theta) > 0.9999:
208+
continue
209+
# ----------------------------------------------------------------
210+
211+
r_ij_cov = covalent_radii_lib(element_list[i]) + covalent_radii_lib(element_list[j])
212+
r_jk_cov = covalent_radii_lib(element_list[j]) + covalent_radii_lib(element_list[k])
213+
214+
force_const = self.calc_bend_force_const(r_ij, r_jk, r_ij_cov, r_jk_cov)
215+
216+
try:
217+
t_xyz = np.array([coord[i], coord[j], coord[k]])
218+
theta, b_vec = bend2(t_xyz)
219+
220+
# Optimized: Use NumPy slicing and outer product
221+
atoms = [i, j, k]
222+
223+
for m_idx, m_atom in enumerate(atoms):
224+
for n_idx, n_atom in enumerate(atoms):
225+
start_m, end_m = 3 * m_atom, 3 * m_atom + 3
226+
start_n, end_n = 3 * n_atom, 3 * n_atom + 3
227+
228+
H_mn_block = force_const * np.outer(b_vec[m_idx], b_vec[n_idx])
229+
self.cart_hess[start_m:end_m, start_n:end_n] += H_mn_block
230+
except Exception:
231+
continue
232+
233+
234+
def fischer_dihedral(self, coord, element_list, bond_mat):
235+
"""Calculate Hessian components for dihedral torsion (Optimized with singularity damping)"""
236+
BC = BondConnectivity()
237+
b_c_mat = BC.bond_connect_matrix(element_list, coord)
238+
dihedral_indices = BC.dihedral_angle_connect_table(b_c_mat)
239+
240+
# Calculate bond count for central atoms in dihedrals
241+
tors_atom_bonds = {}
242+
for idx in dihedral_indices:
243+
i, j, k, l = idx # i-j-k-l dihedral
244+
bond_sum = bond_mat[j].sum() + bond_mat[k].sum() - 2
245+
tors_atom_bonds[(j, k)] = bond_sum
246+
247+
for idx in dihedral_indices:
248+
i, j, k, l = idx
249+
250+
# Vector calculations
251+
vec_ji = coord[i] - coord[j]
252+
vec_jk = coord[k] - coord[j]
253+
vec_kl = coord[l] - coord[k]
254+
255+
r_jk = np.linalg.norm(vec_jk)
256+
r_jk_cov = covalent_radii_lib(element_list[j]) + covalent_radii_lib(element_list[k])
257+
258+
bond_sum = tors_atom_bonds.get((j, k), 0)
259+
260+
# Calculate base force constant
261+
force_const = self.calc_dihedral_force_const(r_jk, r_jk_cov, bond_sum)
262+
263+
# --- Singularity Handling (Damping for linear angles) ---
264+
# Determine linearity of angles i-j-k and j-k-l
265+
266+
# Angle 1: i-j-k
267+
n_ji = np.linalg.norm(vec_ji)
268+
n_jk = r_jk # already calculated
269+
270+
# Avoid division by zero if atoms overlap
271+
if n_ji < 1e-8 or n_jk < 1e-8:
272+
continue
273+
274+
cos_theta1 = np.dot(vec_ji, vec_jk) / (n_ji * n_jk)
275+
276+
# Angle 2: j-k-l (Note: vec_kj is -vec_jk)
277+
vec_kj = -vec_jk
278+
n_kl = np.linalg.norm(vec_kl)
279+
280+
if n_kl < 1e-8:
281+
continue
282+
283+
cos_theta2 = np.dot(vec_kj, vec_kl) / (n_jk * n_kl)
284+
285+
# --- Damping Factor Calculation ---
286+
# The Wilson B-matrix contains 1/sin(theta) terms which diverge at 180 deg.
287+
# We scale the force constant by sin^2(theta) to cancel this divergence.
288+
# sin^2(theta) = 1 - cos^2(theta)
289+
290+
sin2_theta1 = 1.0 - min(cos_theta1**2, 1.0)
291+
sin2_theta2 = 1.0 - min(cos_theta2**2, 1.0)
292+
293+
# Hard cutoff: If geometry is extremely linear, skip to avoid NaN
294+
if sin2_theta1 < 1e-4 or sin2_theta2 < 1e-4:
295+
continue
296+
297+
# Apply scaling factor
298+
# This ensures the force constant goes to 0 as the angle becomes linear
299+
scaling_factor = sin2_theta1 * sin2_theta2
300+
force_const *= scaling_factor
301+
302+
# --------------------------------------------------------
303+
304+
t_xyz = np.array([coord[i], coord[j], coord[k], coord[l]])
305+
306+
try:
307+
tau, b_vec = torsion2(t_xyz)
308+
except (ValueError, ArithmeticError):
309+
# Skip if numerical errors occur in torsion calculation
310+
continue
311+
312+
# Optimized: Use NumPy slicing and outer product
313+
atoms = [i, j, k, l]
314+
315+
for m_idx, m_atom in enumerate(atoms):
316+
for n_idx, n_atom in enumerate(atoms):
317+
start_m, end_m = 3 * m_atom, 3 * m_atom + 3
318+
start_n, end_n = 3 * n_atom, 3 * n_atom + 3
319+
320+
H_mn_block = force_const * np.outer(b_vec[m_idx], b_vec[n_idx])
321+
self.cart_hess[start_m:end_m, start_n:end_n] += H_mn_block
322+
323+
# --- Optimized: Block assignment using slicing (and fixed logic) ---
324+
def d3_dispersion_hessian(self, coord, element_list, bond_mat):
325+
"""Calculate Hessian correction based on D3 dispersion forces (Optimized/Corrected)"""
326+
n_atoms = len(coord)
327+
328+
# Calculate D3 dispersion correction for all atom pairs (i > j)
329+
for i in range(n_atoms):
330+
for j in range(i):
331+
# Skip bonded atom pairs
332+
if bond_mat[i, j]:
333+
continue
334+
335+
r_vec = coord[i] - coord[j]
336+
r_ij = np.linalg.norm(r_vec)
337+
338+
if r_ij < 0.1:
339+
continue
340+
341+
# Calculate D3 Hessian contribution (3x3 block)
342+
hess_block = self.d3_hessian_contribution(r_vec, r_ij, element_list[i], element_list[j])
343+
344+
# Use slicing for efficient block assignment
345+
# H_ii += H_block, H_jj += H_block, H_ij -= H_block, H_ji -= H_block
346+
start_i, end_i = 3 * i, 3 * i + 3
347+
start_j, end_j = 3 * j, 3 * j + 3
348+
349+
self.cart_hess[start_i:end_i, start_i:end_i] += hess_block
350+
self.cart_hess[start_j:end_j, start_j:end_j] += hess_block
351+
self.cart_hess[start_i:end_i, start_j:end_j] -= hess_block
352+
self.cart_hess[start_j:end_j, start_i:end_i] -= hess_block
353+
354+
# --- Optimized: Main function flow ---
355+
def main(self, coord, element_list, cart_gradient):
356+
"""
357+
Calculate Hessian combining Fischer model and D3 dispersion correction
358+
"""
359+
print("Generating Hessian using Fischer model using D3 dispersion parameter...")
360+
361+
n_atoms = len(coord)
362+
self.cart_hess = np.zeros((n_atoms*3, n_atoms*3), dtype="float64")
363+
364+
# Calculate bond connectivity matrix ONCE (Optimized internally)
365+
bond_mat, dist_mat, pair_cov_radii_mat = self.get_bond_connectivity(coord, element_list)
366+
367+
# Calculate Hessian components from Fischer model (Optimized internally with slicing)
368+
self.fischer_bond(coord, element_list)
369+
self.fischer_angle(coord, element_list)
370+
self.fischer_dihedral(coord, element_list, bond_mat)
371+
372+
# Calculate Hessian components from D3 dispersion correction (Optimized internally with slicing)
373+
self.d3_dispersion_hessian(coord, element_list, bond_mat)
374+
375+
# Optimized: Symmetrize the Hessian matrix
376+
self.cart_hess = (self.cart_hess + self.cart_hess.T) / 2.0
377+
378+
# Project out rotational and translational modes
379+
hess_proj = Calculationtools().project_out_hess_tr_and_rot_for_coord(self.cart_hess, element_list, coord)
380+
381+
return hess_proj

0 commit comments

Comments
 (0)