|
| 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