Skip to content

Commit 38daab3

Browse files
committed
Change orthonormalization when projecting out modes in statmech.
We were doing a manually coded Gram-Schmidt orthonormalization. This is now replaced with a QR decomposition built in to Numpy, which should be more robust (and probably faster). Added in some checks that print warnings.
1 parent 60386ca commit 38daab3

1 file changed

Lines changed: 21 additions & 27 deletions

File tree

arkane/statmech.py

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,39 +1078,33 @@ def project_rotors(conformer, hessian, rotors, linear, is_ts, get_projected_out_
10781078
d[3 * i + 2, 5] = (p[i, 0] * inertia_xyz[2, 1] - p[i, 1] * inertia_xyz[2, 0]) * amass[i]
10791079

10801080
# Make sure projection matrix is orthonormal
1081-
identity = np.identity(n_atoms * 3, float)
1082-
p = np.zeros((n_atoms * 3, 3 * n_atoms + external), float)
1083-
p[:, 0:external] = d[:, 0:external]
1084-
p[:, external:external + 3 * n_atoms] = identity[:, 0:3 * n_atoms]
1081+
p = np.hstack((d, np.identity(n_atoms * 3, float)))
10851082

1086-
# modified Gram–Schmidt orthonormalization
1087-
for i in range(3 * n_atoms + external):
1088-
norm = 0.0
1089-
for j in range(3 * n_atoms):
1090-
norm += p[j, i] * p[j, i]
1091-
for j in range(3 * n_atoms):
1092-
if norm > 1E-15:
1093-
p[j, i] /= np.sqrt(norm)
1094-
else:
1095-
p[j, i] = 0.0 # zeroing out vectors that are nearly zero or dependent, could lose a basis
1096-
for j in range(i + 1, 3 * n_atoms + external):
1097-
proj = 0.0
1098-
for k in range(3 * n_atoms):
1099-
proj += p[k, i] * p[k, j]
1100-
for k in range(3 * n_atoms):
1101-
p[k, j] -= proj * p[k, i]
1083+
# Compute the QR decomposition in reduced mode
1084+
Q, R = np.linalg.qr(p, mode='reduced')
1085+
1086+
# Verify that Q has orthonormal columns:
1087+
if np.isclose(np.dot(Q.T, Q), np.eye(Q.shape[1])).all():
1088+
logging.debug("Q has orthonormal columns")
1089+
else:
1090+
logging.warning("Q does not have orthonormal columns")
1091+
# Verify the reconstruction
1092+
reconstructed_p = Q @ R
1093+
if np.isclose(p, reconstructed_p).all():
1094+
logging.debug("Reconstruction of projection matrix is correct")
1095+
else:
1096+
logging.warning("Reconstruction of projection matrix is incorrect")
1097+
1098+
# Check for nearly zero columns in the QR decomposition
1099+
for i, rkk in enumerate(R.diagonal()):
1100+
if abs(rkk) < 1E-15:
1101+
logging.warning(f'Column {i} of the QR decomposition is nearly zero, could lose a basis')
11021102

1103-
# Order p, since there will be vectors that are 0.0
1104-
i = 0
1105-
is_zero_column = (p*p).sum(axis=0) < 0.5
1106-
# put the columns that are zeros at the end
1107-
temporary = np.hstack((p[:, ~is_zero_column], p[:, is_zero_column]))
1108-
p[:, :] = temporary
11091103

11101104
# T is the transformation vector from cartesian to internal coordinates
11111105
T = np.zeros((n_atoms * 3, 3 * n_atoms - external), float)
11121106

1113-
T[:, 0:3 * n_atoms - external] = p[:, external:3 * n_atoms]
1107+
T[:, :] = Q[:, external:3 * n_atoms]
11141108

11151109
# Generate mass-weighted force constant matrix
11161110
# This converts the axes to mass-weighted Cartesian axes

0 commit comments

Comments
 (0)