Skip to content

Commit 91aa2bc

Browse files
authored
Add files via upload
Fixed an issue in Fischer's model Hessian generation where numerical instability occurred when four atoms were aligned linearly.
1 parent caf5dd4 commit 91aa2bc

3 files changed

Lines changed: 276 additions & 226 deletions

File tree

multioptpy/ModelHessian/fischer.py

Lines changed: 59 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -131,51 +131,84 @@ def fischer_angle(self, coord, element_list):
131131
self.cart_hess[3*k+n, 3*j+m] += force_const * b_vec[2][n] * b_vec[1][m]
132132

133133
def fischer_dihedral(self, coord, element_list, bond_mat):
134-
"""Calculate Hessian components for dihedral torsions"""
134+
"""Calculate Hessian components for dihedral torsions with linearity check"""
135135
BC = BondConnectivity()
136136
b_c_mat = BC.bond_connect_matrix(element_list, coord)
137137
dihedral_indices = BC.dihedral_angle_connect_table(b_c_mat)
138138

139+
# Helper to calculate sin^2 of bond angle
140+
def get_sin_sq_angle(idx1, idx2, idx3):
141+
v1 = coord[idx1] - coord[idx2]
142+
v2 = coord[idx3] - coord[idx2]
143+
# Cross product magnitude squared
144+
cross_prod = np.cross(v1, v2)
145+
cross_sq = np.dot(cross_prod, cross_prod)
146+
# Squared norms
147+
n1_sq = np.dot(v1, v1)
148+
n2_sq = np.dot(v2, v2)
149+
if n1_sq * n2_sq < 1e-12:
150+
return 0.0
151+
return cross_sq / (n1_sq * n2_sq)
152+
139153
for idx in dihedral_indices:
140-
i, j, k, l = idx # i-j-k-l dihedral
154+
# i-j-k-l dihedral
155+
i, j, k, l = idx
156+
157+
# --- Linearity Check ---
158+
# Check bond angles I-J-K and J-K-L.
159+
# If atoms are linear, the torsion definition is singular (B-matrix blows up).
160+
# We use sin^2(theta) because it avoids acos and is efficient.
161+
sin_sq_ijk = get_sin_sq_angle(i, j, k)
162+
sin_sq_jkl = get_sin_sq_angle(j, k, l)
141163

142-
# Central bond in dihedral
164+
# Threshold: if sin(theta) < 0.17 (approx 10 degrees from linear), dampen or skip.
165+
# Using sin^2 < 0.03 as cutoff.
166+
# The Wilson B-matrix scales as 1/sin(theta).
167+
# If we are too close to linear, we must skip to avoid numerical explosion.
168+
if sin_sq_ijk < 1.0e-3 or sin_sq_jkl < 1.0e-3:
169+
continue
170+
# -----------------------
171+
172+
# Central bond in dihedral (j-k)
143173
r_jk = np.linalg.norm(coord[j] - coord[k])
144174
r_jk_cov = covalent_radii_lib(element_list[j]) + covalent_radii_lib(element_list[k])
145175

146176
# Count bonds to central atoms
147177
bond_sum = self.count_bonds_for_dihedral(bond_mat, (j, k))
148178

149-
# Calculate force constant using Fischer's formula
179+
# Calculate force constant
150180
force_const = self.calc_dihedral_force_const(r_jk, r_jk_cov, bond_sum)
151181

182+
# Optional: Additional damping can be applied here if desired,
183+
# but the skip above is the most robust fix for the singularity.
184+
152185
# Convert to Cartesian coordinates
153186
t_xyz = np.array([coord[i], coord[j], coord[k], coord[l]])
154-
tau, b_vec = torsion2(t_xyz)
155187

156-
for n in range(3):
157-
for m in range(3):
158-
self.cart_hess[3*i+n, 3*i+m] += force_const * b_vec[0][n] * b_vec[0][m]
159-
self.cart_hess[3*j+n, 3*j+m] += force_const * b_vec[1][n] * b_vec[1][m]
160-
self.cart_hess[3*k+n, 3*k+m] += force_const * b_vec[2][n] * b_vec[2][m]
161-
self.cart_hess[3*l+n, 3*l+m] += force_const * b_vec[3][n] * b_vec[3][m]
162-
163-
self.cart_hess[3*i+n, 3*j+m] += force_const * b_vec[0][n] * b_vec[1][m]
164-
self.cart_hess[3*i+n, 3*k+m] += force_const * b_vec[0][n] * b_vec[2][m]
165-
self.cart_hess[3*i+n, 3*l+m] += force_const * b_vec[0][n] * b_vec[3][m]
166-
167-
self.cart_hess[3*j+n, 3*i+m] += force_const * b_vec[1][n] * b_vec[0][m]
168-
self.cart_hess[3*j+n, 3*k+m] += force_const * b_vec[1][n] * b_vec[2][m]
169-
self.cart_hess[3*j+n, 3*l+m] += force_const * b_vec[1][n] * b_vec[3][m]
188+
# Note: torsion2 might still be unstable if not skipped
189+
try:
190+
tau, b_vec = torsion2(t_xyz)
191+
except Exception:
192+
# Fallback if torsion2 fails numerically
193+
continue
194+
195+
# Iterate over all pairs of atoms in the dihedral
196+
atom_indices = [i, j, k, l]
197+
198+
for a in range(4):
199+
for b in range(4):
200+
atom_a = atom_indices[a]
201+
atom_b = atom_indices[b]
170202

171-
self.cart_hess[3*k+n, 3*i+m] += force_const * b_vec[2][n] * b_vec[0][m]
172-
self.cart_hess[3*k+n, 3*j+m] += force_const * b_vec[2][n] * b_vec[1][m]
173-
self.cart_hess[3*k+n, 3*l+m] += force_const * b_vec[2][n] * b_vec[3][m]
203+
vec_a = b_vec[a]
204+
vec_b = b_vec[b]
174205

175-
self.cart_hess[3*l+n, 3*i+m] += force_const * b_vec[3][n] * b_vec[0][m]
176-
self.cart_hess[3*l+n, 3*j+m] += force_const * b_vec[3][n] * b_vec[1][m]
177-
self.cart_hess[3*l+n, 3*k+m] += force_const * b_vec[3][n] * b_vec[2][m]
178-
206+
# Update the 3x3 Hessian block
207+
for n in range(3):
208+
for m in range(3):
209+
val = force_const * vec_a[n] * vec_b[m]
210+
self.cart_hess[3*atom_a+n, 3*atom_b+m] += val
211+
179212
def main(self, coord, element_list, cart_gradient):
180213
"""Main function to generate Fischer's approximate Hessian"""
181214
print("Generating Fischer's approximate hessian...")

multioptpy/ModelHessian/fischerd3.py

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -211,8 +211,9 @@ def fischer_angle(self, coord, element_list):
211211
H_mn_block = force_const * np.outer(b_vec[m_idx], b_vec[n_idx])
212212
self.cart_hess[start_m:end_m, start_n:end_n] += H_mn_block
213213

214+
214215
def fischer_dihedral(self, coord, element_list, bond_mat):
215-
"""Calculate Hessian components for dihedral torsion (Optimized with slicing)"""
216+
"""Calculate Hessian components for dihedral torsion (Optimized with singularity damping)"""
216217
BC = BondConnectivity()
217218
b_c_mat = BC.bond_connect_matrix(element_list, coord)
218219
dihedral_indices = BC.dihedral_angle_connect_table(b_c_mat)
@@ -221,22 +222,71 @@ def fischer_dihedral(self, coord, element_list, bond_mat):
221222
tors_atom_bonds = {}
222223
for idx in dihedral_indices:
223224
i, j, k, l = idx # i-j-k-l dihedral
224-
# bond_sum is count_bonds_for_dihedral(bond_mat, (j, k))
225225
bond_sum = bond_mat[j].sum() + bond_mat[k].sum() - 2
226226
tors_atom_bonds[(j, k)] = bond_sum
227227

228228
for idx in dihedral_indices:
229229
i, j, k, l = idx
230230

231-
r_jk = np.linalg.norm(coord[j] - coord[k])
231+
# Vector calculations
232+
vec_ji = coord[i] - coord[j]
233+
vec_jk = coord[k] - coord[j]
234+
vec_kl = coord[l] - coord[k]
235+
236+
r_jk = np.linalg.norm(vec_jk)
232237
r_jk_cov = covalent_radii_lib(element_list[j]) + covalent_radii_lib(element_list[k])
233238

234239
bond_sum = tors_atom_bonds.get((j, k), 0)
235240

241+
# Calculate base force constant
236242
force_const = self.calc_dihedral_force_const(r_jk, r_jk_cov, bond_sum)
237243

244+
# --- Singularity Handling (Damping for linear angles) ---
245+
# Determine linearity of angles i-j-k and j-k-l
246+
247+
# Angle 1: i-j-k
248+
n_ji = np.linalg.norm(vec_ji)
249+
n_jk = r_jk # already calculated
250+
251+
# Avoid division by zero if atoms overlap
252+
if n_ji < 1e-8 or n_jk < 1e-8: continue
253+
254+
cos_theta1 = np.dot(vec_ji, vec_jk) / (n_ji * n_jk)
255+
256+
# Angle 2: j-k-l (Note: vec_kj is -vec_jk)
257+
vec_kj = -vec_jk
258+
n_kl = np.linalg.norm(vec_kl)
259+
260+
if n_kl < 1e-8: continue
261+
262+
cos_theta2 = np.dot(vec_kj, vec_kl) / (n_jk * n_kl)
263+
264+
# --- Damping Factor Calculation ---
265+
# The Wilson B-matrix contains 1/sin(theta) terms which diverge at 180 deg.
266+
# We scale the force constant by sin^2(theta) to cancel this divergence.
267+
# sin^2(theta) = 1 - cos^2(theta)
268+
269+
sin2_theta1 = 1.0 - min(cos_theta1**2, 1.0)
270+
sin2_theta2 = 1.0 - min(cos_theta2**2, 1.0)
271+
272+
# Hard cutoff: If geometry is extremely linear, skip to avoid NaN
273+
if sin2_theta1 < 1e-4 or sin2_theta2 < 1e-4:
274+
continue
275+
276+
# Apply scaling factor
277+
# This ensures the force constant goes to 0 as the angle becomes linear
278+
scaling_factor = sin2_theta1 * sin2_theta2
279+
force_const *= scaling_factor
280+
281+
# --------------------------------------------------------
282+
238283
t_xyz = np.array([coord[i], coord[j], coord[k], coord[l]])
239-
tau, b_vec = torsion2(t_xyz)
284+
285+
try:
286+
tau, b_vec = torsion2(t_xyz)
287+
except (ValueError, ArithmeticError):
288+
# Skip if numerical errors occur in torsion calculation
289+
continue
240290

241291
# Optimized: Use NumPy slicing and outer product
242292
atoms = [i, j, k, l]

0 commit comments

Comments
 (0)