MultiOptPy 1.20.5__py3-none-any.whl → 1.20.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,360 +1,305 @@
1
1
  import numpy as np
2
- from scipy.spatial.distance import cdist # Highly recommended for vectorized distance calculation
2
+ from scipy.spatial.distance import cdist
3
3
  from multioptpy.Parameters.parameter import UnitValueLib, covalent_radii_lib
4
4
  from multioptpy.Utils.calc_tools import Calculationtools
5
5
  from multioptpy.Parameters.parameter import D3Parameters, D2_C6_coeff_lib, D2_VDW_radii_lib
6
6
  from multioptpy.Utils.bond_connectivity import BondConnectivity
7
- from multioptpy.ModelHessian.calc_params import torsion2, stretch2, bend2
8
-
7
+ from multioptpy.ModelHessian.calc_params import torsion2, bend2
9
8
 
10
9
  class FischerD3ApproxHessian:
11
10
  def __init__(self):
12
11
  """
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
12
+ Fischer's Model Hessian implementation with Dynamic D3 dispersion correction.
13
+ Robust against linear molecules and singularities.
16
14
  """
17
15
  self.bohr2angstroms = UnitValueLib().bohr2angstroms
18
- self.hartree2kcalmol = UnitValueLib().hartree2kcalmol
19
- self.bond_factor = 1.3 # Bond detection threshold factor
20
16
 
21
- # D3 dispersion correction parameters (default: PBE0)
17
+ # D3 dispersion correction parameters
22
18
  self.d3_params = D3Parameters()
23
19
  self.cart_hess = None
24
20
 
21
+ # Grimme D3 scaling factors for CN calculation
22
+ self.k1 = 16.0
23
+ self.k2 = 4.0 / 3.0
24
+
25
+ # Expanded Reference Coordination Numbers (Default averages)
26
+ # Covers H through Xe roughly. Values are heuristic/typical valencies or coordination.
27
+ self.ref_cn_map = {
28
+ # Period 1
29
+ 'H': 1, 'He': 0,
30
+ # Period 2
31
+ 'Li': 4, 'Be': 4, 'B': 3, 'C': 4, 'N': 3, 'O': 2, 'F': 1, 'Ne': 0,
32
+ # Period 3
33
+ 'Na': 6, 'Mg': 6, 'Al': 6, 'Si': 4, 'P': 5, 'S': 6, 'Cl': 1, 'Ar': 0,
34
+ # Period 4 (Transition Metals set to ~6-12 depending on typical packing/complexes)
35
+ 'K': 8, 'Ca': 6,
36
+ 'Sc': 12, 'Ti': 12, 'V': 12, 'Cr': 6, 'Mn': 6, 'Fe': 6, 'Co': 6, 'Ni': 4, 'Cu': 4, 'Zn': 4,
37
+ 'Ga': 4, 'Ge': 4, 'As': 3, 'Se': 2, 'Br': 1, 'Kr': 0,
38
+ # Period 5
39
+ 'Rb': 8, 'Sr': 6,
40
+ 'Y': 12, 'Zr': 12, 'Nb': 12, 'Mo': 6, 'Tc': 6, 'Ru': 6, 'Rh': 6, 'Pd': 4, 'Ag': 4, 'Cd': 4,
41
+ 'In': 6, 'Sn': 4, 'Sb': 3, 'Te': 2, 'I': 1, 'Xe': 0
42
+ }
43
+
44
+ def calc_coordination_numbers(self, coord, element_list):
45
+ """Calculate fractional coordination numbers (CN) for Dynamic D3."""
46
+ # Pre-fetch covalent radii
47
+ cov_r = np.array([covalent_radii_lib(e) for e in element_list])
48
+
49
+ # Vectorized distance matrix
50
+ r_mat = cdist(coord, coord)
51
+ np.fill_diagonal(r_mat, np.inf)
52
+
53
+ # rcov_sum[i, j] = cov_r[i] + cov_r[j]
54
+ rcov_sum = cov_r[:, None] + cov_r[None, :]
55
+
56
+ # D3 CN formula
57
+ term = -self.k1 * (self.k2 * (r_mat / rcov_sum) - 1.0)
58
+ term = np.clip(term, -100, 100) # Prevent overflow
59
+ cutoff_func = 1.0 / (1.0 + np.exp(term))
60
+
61
+ return np.sum(cutoff_func, axis=1)
62
+
63
+ # --- Fischer Terms (Robust) ---
64
+
25
65
  def calc_bond_force_const(self, r_ab, r_ab_cov):
26
- """Calculate force constant for bond stretching using Fischer formula"""
27
66
  return 0.3601 * np.exp(-1.944 * (r_ab - r_ab_cov))
28
67
 
29
68
  def calc_bend_force_const(self, r_ab, r_ac, r_ab_cov, r_ac_cov):
30
- """Calculate force constant for angle bending"""
31
69
  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
- )
70
+ if abs(val) < 1e-10: return 0.0
71
+ return 0.089 + 0.11 / (val)**(-0.42) * np.exp(-0.44 * (r_ab + r_ac - r_ab_cov - r_ac_cov))
38
72
 
39
73
  def calc_dihedral_force_const(self, r_ab, r_ab_cov, bond_sum):
40
- """Calculate force constant for dihedral torsion"""
41
74
  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
75
+ if abs(val) < 1e-10: return 0.0
76
+ return 0.0015 + 14.0 * max(bond_sum, 0)**0.57 / (val)**4.0 * np.exp(-2.85 * (r_ab - r_ab_cov))
83
77
 
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
78
+ def _add_block(self, i, j, block):
79
+ start_i, end_i = 3*i, 3*i+3
80
+ start_j, end_j = 3*j, 3*j+3
81
+ self.cart_hess[start_i:end_i, start_j:end_j] += block
128
82
 
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
-
83
+ def fischer_bond(self, coord, element_list, bond_indices):
158
84
  for idx in bond_indices:
159
85
  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)
86
+ r_vec = coord[i] - coord[j]
87
+ r_ij = np.linalg.norm(r_vec)
88
+ if r_ij < 0.1: continue
89
+
90
+ r_cov = covalent_radii_lib(element_list[i]) + covalent_radii_lib(element_list[j])
91
+ fc = self.calc_bond_force_const(r_ij, r_cov)
167
92
 
168
- # Optimized: Use NumPy slicing and outer product
169
- b_vec_i, b_vec_j = b_vec[0], b_vec[1]
93
+ # Use safe unit vector calculation
94
+ u = r_vec / r_ij
95
+ block = fc * np.outer(u, u)
170
96
 
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
-
97
+ self._add_block(i, i, block)
98
+ self._add_block(j, j, block)
99
+ self._add_block(i, j, -block)
100
+ self._add_block(j, i, -block)
184
101
 
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
-
102
+ def fischer_angle(self, coord, element_list, angle_indices):
191
103
  for idx in angle_indices:
192
- i, j, k = idx # i-j-k angle
193
- r_ij = np.linalg.norm(coord[i] - coord[j])
194
- r_jk = np.linalg.norm(coord[j] - coord[k])
195
- r_ij_cov = covalent_radii_lib(element_list[i]) + covalent_radii_lib(element_list[j])
196
- r_jk_cov = covalent_radii_lib(element_list[j]) + covalent_radii_lib(element_list[k])
104
+ i, j, k = idx
105
+ r_ij_vec = coord[i] - coord[j]
106
+ r_jk_vec = coord[k] - coord[j] # Center is j
197
107
 
198
- force_const = self.calc_bend_force_const(r_ij, r_jk, r_ij_cov, r_jk_cov)
108
+ r_ij = np.linalg.norm(r_ij_vec)
109
+ r_jk = np.linalg.norm(r_jk_vec)
199
110
 
200
- t_xyz = np.array([coord[i], coord[j], coord[k]])
201
- theta, b_vec = bend2(t_xyz)
111
+ if r_ij < 0.1 or r_jk < 0.1:
112
+ continue
113
+
114
+ # Check for linearity (180 deg) or overlap (0 deg)
115
+ cos_theta = np.dot(r_ij_vec, r_jk_vec) / (r_ij * r_jk)
116
+ if abs(cos_theta) > 0.9999:
117
+ continue
118
+
119
+ r_cov_ij = covalent_radii_lib(element_list[i]) + covalent_radii_lib(element_list[j])
120
+ r_cov_jk = covalent_radii_lib(element_list[j]) + covalent_radii_lib(element_list[k])
202
121
 
203
- # Optimized: Use NumPy slicing and outer product
204
- atoms = [i, j, k]
122
+ fc = self.calc_bend_force_const(r_ij, r_jk, r_cov_ij, r_cov_jk)
205
123
 
206
- for m_idx, m_atom in enumerate(atoms):
207
- for n_idx, n_atom in enumerate(atoms):
208
- start_m, end_m = 3 * m_atom, 3 * m_atom + 3
209
- start_n, end_n = 3 * n_atom, 3 * n_atom + 3
210
-
211
- H_mn_block = force_const * np.outer(b_vec[m_idx], b_vec[n_idx])
212
- self.cart_hess[start_m:end_m, start_n:end_n] += H_mn_block
213
-
124
+ try:
125
+ # bend2 expects: [atom_end1, atom_center, atom_end2]
126
+ t_xyz = np.array([coord[i], coord[j], coord[k]])
127
+ _, b_vec = bend2(t_xyz)
128
+
129
+ atoms = [i, j, k]
130
+ for m_idx, m_atom in enumerate(atoms):
131
+ for n_idx, n_atom in enumerate(atoms):
132
+ block = fc * np.outer(b_vec[m_idx], b_vec[n_idx])
133
+ self._add_block(m_atom, n_atom, block)
134
+ except Exception:
135
+ continue
214
136
 
215
- def fischer_dihedral(self, coord, element_list, bond_mat):
216
- """Calculate Hessian components for dihedral torsion (Optimized with singularity damping)"""
217
- BC = BondConnectivity()
218
- b_c_mat = BC.bond_connect_matrix(element_list, coord)
219
- dihedral_indices = BC.dihedral_angle_connect_table(b_c_mat)
220
-
221
- # Calculate bond count for central atoms in dihedrals
222
- tors_atom_bonds = {}
223
- for idx in dihedral_indices:
224
- i, j, k, l = idx # i-j-k-l dihedral
225
- bond_sum = bond_mat[j].sum() + bond_mat[k].sum() - 2
226
- tors_atom_bonds[(j, k)] = bond_sum
137
+ def fischer_dihedral(self, coord, element_list, dihedral_indices, bond_mat):
138
+ neighbor_counts = np.sum(bond_mat, axis=1)
227
139
 
228
140
  for idx in dihedral_indices:
229
141
  i, j, k, l = idx
230
142
 
231
- # Vector calculations
143
+ # Check linearity of the axis bond j-k and its connections
232
144
  vec_ji = coord[i] - coord[j]
233
145
  vec_jk = coord[k] - coord[j]
234
146
  vec_kl = coord[l] - coord[k]
235
147
 
236
- r_jk = np.linalg.norm(vec_jk)
237
- r_jk_cov = covalent_radii_lib(element_list[j]) + covalent_radii_lib(element_list[k])
238
-
239
- bond_sum = tors_atom_bonds.get((j, k), 0)
240
-
241
- # Calculate base force constant
242
- force_const = self.calc_dihedral_force_const(r_jk, r_jk_cov, bond_sum)
243
-
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)
148
+ n_ji, n_jk, n_kl = np.linalg.norm(vec_ji), np.linalg.norm(vec_jk), np.linalg.norm(vec_kl)
149
+ if min(n_ji, n_jk, n_kl) < 0.1: continue
263
150
 
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)
151
+ # Cosine check for linearity: i-j-k and j-k-l
152
+ cos1 = np.dot(vec_ji, vec_jk) / (n_ji * n_jk)
153
+ cos2 = np.dot(-vec_jk, vec_kl) / (n_jk * n_kl)
268
154
 
269
- sin2_theta1 = 1.0 - min(cos_theta1**2, 1.0)
270
- sin2_theta2 = 1.0 - min(cos_theta2**2, 1.0)
155
+ # Use sin^2 to dampen force constant as it approaches linearity
156
+ sin2_1 = 1.0 - min(cos1**2, 1.0)
157
+ sin2_2 = 1.0 - min(cos2**2, 1.0)
271
158
 
272
- # Hard cutoff: If geometry is extremely linear, skip to avoid NaN
273
- if sin2_theta1 < 1e-4 or sin2_theta2 < 1e-4:
159
+ if sin2_1 < 1e-3 or sin2_2 < 1e-3:
274
160
  continue
275
161
 
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
-
283
- t_xyz = np.array([coord[i], coord[j], coord[k], coord[l]])
162
+ # Damping factor
163
+ scaling = sin2_1 * sin2_2
284
164
 
285
- try:
286
- tau, b_vec = torsion2(t_xyz)
287
- except (ValueError, ArithmeticError):
288
- # Skip if numerical errors occur in torsion calculation
289
- continue
165
+ bond_sum = neighbor_counts[j] + neighbor_counts[k] - 2
166
+ r_cov_jk = covalent_radii_lib(element_list[j]) + covalent_radii_lib(element_list[k])
290
167
 
291
- # Optimized: Use NumPy slicing and outer product
292
- atoms = [i, j, k, l]
293
-
294
- for m_idx, m_atom in enumerate(atoms):
295
- for n_idx, n_atom in enumerate(atoms):
296
- start_m, end_m = 3 * m_atom, 3 * m_atom + 3
297
- start_n, end_n = 3 * n_atom, 3 * n_atom + 3
298
-
299
- H_mn_block = force_const * np.outer(b_vec[m_idx], b_vec[n_idx])
300
- self.cart_hess[start_m:end_m, start_n:end_n] += H_mn_block
168
+ fc = self.calc_dihedral_force_const(n_jk, r_cov_jk, bond_sum)
169
+ fc *= scaling
301
170
 
302
- # --- Optimized: Block assignment using slicing (and fixed logic) ---
303
- def d3_dispersion_hessian(self, coord, element_list, bond_mat):
304
- """Calculate Hessian correction based on D3 dispersion forces (Optimized/Corrected)"""
305
- n_atoms = len(coord)
306
-
307
- # Calculate D3 dispersion correction for all atom pairs (i > j)
308
- for i in range(n_atoms):
309
- for j in range(i):
310
- # Skip bonded atom pairs
311
- if bond_mat[i, j]:
312
- continue
313
-
314
- r_vec = coord[i] - coord[j]
315
- r_ij = np.linalg.norm(r_vec)
171
+ try:
172
+ t_xyz = np.array([coord[i], coord[j], coord[k], coord[l]])
173
+ _, b_vec = torsion2(t_xyz)
316
174
 
317
- if r_ij < 0.1:
175
+ if not np.all(np.isfinite(b_vec)):
318
176
  continue
319
-
320
- # Calculate D3 Hessian contribution (3x3 block)
321
- hess_block = self.d3_hessian_contribution(r_vec, r_ij, element_list[i], element_list[j])
322
-
323
- # Use slicing for efficient block assignment
324
- # H_ii += H_block, H_jj += H_block, H_ij -= H_block, H_ji -= H_block
325
- start_i, end_i = 3 * i, 3 * i + 3
326
- start_j, end_j = 3 * j, 3 * j + 3
327
-
328
- self.cart_hess[start_i:end_i, start_i:end_i] += hess_block
329
- self.cart_hess[start_j:end_j, start_j:end_j] += hess_block
330
- self.cart_hess[start_i:end_i, start_j:end_j] -= hess_block
331
- self.cart_hess[start_j:end_j, start_i:end_i] -= hess_block
332
-
333
- # --- Optimized: Main function flow ---
177
+
178
+ atoms = [i, j, k, l]
179
+ for m_idx, m_atom in enumerate(atoms):
180
+ for n_idx, n_atom in enumerate(atoms):
181
+ block = fc * np.outer(b_vec[m_idx], b_vec[n_idx])
182
+ self._add_block(m_atom, n_atom, block)
183
+ except Exception:
184
+ continue
185
+
334
186
  def main(self, coord, element_list, cart_gradient):
335
- """
336
- Calculate Hessian combining Fischer model and D3 dispersion correction
337
- """
338
- print("Generating Hessian using Fischer model with D3 dispersion correction...")
187
+ print("Generating Hessian using Fischer model with Dynamic D3 correction (Robust)...")
339
188
 
340
189
  n_atoms = len(coord)
341
190
  self.cart_hess = np.zeros((n_atoms*3, n_atoms*3), dtype="float64")
342
191
 
343
- # Calculate bond connectivity matrix ONCE (Optimized internally)
344
- bond_mat, dist_mat, pair_cov_radii_mat = self.get_bond_connectivity(coord, element_list)
192
+ # 1. Topology
193
+ BC = BondConnectivity()
194
+ bond_mat = BC.bond_connect_matrix(element_list, coord)
195
+ bond_indices = BC.bond_connect_table(bond_mat)
196
+ angle_indices = BC.angle_connect_table(bond_mat)
197
+ dihedral_indices = BC.dihedral_angle_connect_table(bond_mat)
345
198
 
346
- # Calculate Hessian components from Fischer model (Optimized internally with slicing)
347
- self.fischer_bond(coord, element_list)
348
- self.fischer_angle(coord, element_list)
349
- self.fischer_dihedral(coord, element_list, bond_mat)
199
+ # 2. Dynamic D3 Parameters (Coordination Numbers)
200
+ cn_list = self.calc_coordination_numbers(coord, element_list)
350
201
 
351
- # Calculate Hessian components from D3 dispersion correction (Optimized internally with slicing)
352
- self.d3_dispersion_hessian(coord, element_list, bond_mat)
202
+ # 3. Fischer Contributions (with strict linearity checks)
203
+ self.fischer_bond(coord, element_list, bond_indices)
204
+ self.fischer_angle(coord, element_list, angle_indices)
205
+ self.fischer_dihedral(coord, element_list, dihedral_indices, bond_mat)
353
206
 
354
- # Optimized: Symmetrize the Hessian matrix
355
- self.cart_hess = (self.cart_hess + self.cart_hess.T) / 2.0
207
+ # 4. Dynamic D3 Dispersion (Vectorized)
208
+ mask = np.tril(np.ones((n_atoms, n_atoms), dtype=bool), k=-1) & ~bond_mat
209
+
210
+ diff_tensor = coord[:, None, :] - coord[None, :, :] # (N, N, 3)
211
+ dist_matrix = np.linalg.norm(diff_tensor, axis=-1)
212
+
213
+ mask &= (dist_matrix > 0.1)
214
+ i_idx, j_idx = np.where(mask)
215
+
216
+ if len(i_idx) > 0:
217
+ # --- Pre-compute atomic parameters for all atoms (O(N)) ---
218
+ c6_atoms = np.array([D2_C6_coeff_lib(e) for e in element_list])
219
+ r4r2_atoms = np.array([self.d3_params.get_r4r2(e) for e in element_list])
220
+ ref_cn_atoms = np.array([self.ref_cn_map.get(e, 4) for e in element_list])
221
+
222
+ # Safe VDW radii retrieval
223
+ vdw_atoms = np.zeros(n_atoms)
224
+ for k, e in enumerate(element_list):
225
+ try: vdw_atoms[k] = D2_VDW_radii_lib(e)
226
+ except: vdw_atoms[k] = covalent_radii_lib(e) * 1.5
227
+
228
+ # --- Gather Pair Data (O(M)) ---
229
+ r_vecs = diff_tensor[i_idx, j_idx] # Vector r_ij (M, 3)
230
+ r_ijs = dist_matrix[i_idx, j_idx] # Distance scalar (M,)
231
+
232
+ # Dynamic C6 Scaling
233
+ cn_i = cn_list[i_idx]
234
+ cn_j = cn_list[j_idx]
235
+ scale_i = np.clip(1.0 - 0.05 * (cn_i - ref_cn_atoms[i_idx]), 0.75, 1.25)
236
+ scale_j = np.clip(1.0 - 0.05 * (cn_j - ref_cn_atoms[j_idx]), 0.75, 1.25)
237
+
238
+ c6_ijs = np.sqrt((c6_atoms[i_idx] * scale_i) * (c6_atoms[j_idx] * scale_j))
239
+
240
+ # C8 and R0
241
+ c8_ijs = 3.0 * c6_ijs * np.sqrt(r4r2_atoms[i_idx] * r4r2_atoms[j_idx])
242
+ r0s = vdw_atoms[i_idx] + vdw_atoms[j_idx]
243
+
244
+ # --- Vectorized Gradient & Hessian Calculation ---
245
+ a1 = self.d3_params.a1
246
+ a2_6 = self.d3_params.a2
247
+ a2_8 = self.d3_params.a2 + 2.0
248
+
249
+ # Damping terms
250
+ denom6 = r_ijs**6 + (a1 * r0s + a2_6)**6
251
+ denom8 = r_ijs**8 + (a1 * r0s + a2_8)**8
252
+
253
+ f_damp6 = r_ijs**6 / denom6
254
+ f_damp8 = r_ijs**8 / denom8
255
+
256
+ df_damp6 = 6 * r_ijs**5 / denom6 - 6 * r_ijs**12 / denom6**2
257
+ df_damp8 = 8 * r_ijs**7 / denom8 - 8 * r_ijs**16 / denom8**2
258
+
259
+ # Gradients (Forces)
260
+ g6 = -self.d3_params.s6 * c6_ijs * ((-6.0/r_ijs**7)*f_damp6 + (1.0/r_ijs**6)*df_damp6)
261
+ g8 = -self.d3_params.s8 * c8_ijs * ((-8.0/r_ijs**9)*f_damp8 + (1.0/r_ijs**8)*df_damp8)
262
+
263
+ # Second Derivatives (Projection coefficients)
264
+ h6_proj = self.d3_params.s6 * c6_ijs / r_ijs**8 * (42.0*f_damp6 - r_ijs*df_damp6)
265
+ h8_proj = self.d3_params.s8 * c8_ijs / r_ijs**10 * (72.0*f_damp8 - r_ijs*df_damp8)
266
+
267
+ h_proj_vals = h6_proj + h8_proj
268
+ h_perp_vals = (g6 + g8) / r_ijs
269
+
270
+ # Construct Hessian Blocks (M, 3, 3)
271
+ # P = u * u.T
272
+ unit_vecs = r_vecs / r_ijs[:, None]
273
+ proj_ops = np.einsum('bi,bj->bij', unit_vecs, unit_vecs) # (M, 3, 3)
274
+ identity = np.eye(3)[None, :, :]
275
+
276
+ hess_blocks = h_proj_vals[:, None, None] * proj_ops + h_perp_vals[:, None, None] * (identity - proj_ops)
277
+
278
+ # --- Accumulate into Cartesian Hessian ---
279
+ for k, (idx_i, idx_j) in enumerate(zip(i_idx, j_idx)):
280
+ block = hess_blocks[k]
281
+ self._add_block(idx_i, idx_i, block)
282
+ self._add_block(idx_j, idx_j, block)
283
+ self._add_block(idx_i, idx_j, -block)
284
+ self._add_block(idx_j, idx_i, -block)
356
285
 
357
- # Project out rotational and translational modes
358
- hess_proj = Calculationtools().project_out_hess_tr_and_rot_for_coord(self.cart_hess, element_list, coord)
286
+ # 5. Symmetrize & Clean
287
+ self.cart_hess = (self.cart_hess + self.cart_hess.T) / 2.0
359
288
 
289
+ # Project out TR/ROT modes
290
+ try:
291
+ hess_proj = Calculationtools().project_out_hess_tr_and_rot_for_coord(
292
+ self.cart_hess, element_list, coord
293
+ )
294
+ if not np.all(np.isfinite(hess_proj)):
295
+ print("Warning: Projection produced NaNs. Using unprojected Hessian.")
296
+ hess_proj = self.cart_hess
297
+ except Exception as e:
298
+ print(f"Warning: Projection failed ({e}). Using unprojected Hessian.")
299
+ hess_proj = self.cart_hess
300
+
301
+ if not np.all(np.isfinite(hess_proj)):
302
+ print("CRITICAL: Hessian contains NaNs after generation. Resetting to Identity.")
303
+ hess_proj = np.eye(n_atoms*3)
304
+
360
305
  return hess_proj