Skip to content

Commit d8b5fc3

Browse files
committed
Added the module implementing Lowdin orthogonalization, added this correction to the cp2k workflow
1 parent dee3019 commit d8b5fc3

File tree

3 files changed

+120
-0
lines changed

3 files changed

+120
-0
lines changed

src/libra_py/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"namd",
4949
"normal_modes",
5050
"nve_md",
51+
"orthogonalizations",
5152
"packages",
5253
"parse_gamess",
5354
"pdos",

src/libra_py/orthogonalizations.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# *********************************************************************************
2+
# * Copyright (C) 2026 Alexey V. Akimov
3+
# *
4+
# * This file is distributed under the terms of the GNU General Public License
5+
# * as published by the Free Software Foundation, either version 3 of
6+
# * the License, or (at your option) any later version.
7+
# * See the file LICENSE in the root directory of this distribution
8+
# * or <http://www.gnu.org/licenses/>.
9+
# *
10+
# *********************************************************************************/
11+
12+
"""
13+
.. module:: orthogonalizations
14+
:platform: Unix, Windows
15+
:synopsis: this module implements various general-purpose orthogonalization procedures
16+
17+
.. moduleauthor:: Alexey V. Akimov, ChatGPT
18+
19+
"""
20+
21+
import numpy as np
22+
23+
def lowdin_inverse_sqrt(S, thresh=1e-10):
24+
"""
25+
Compute the inverse square root of an overlap matrix using Löwdin symmetric orthogonalization.
26+
27+
This function evaluates S^{-1/2} via eigenvalue decomposition:
28+
S = U diag(λ) U† → S^{-1/2} = U diag(λ^{-1/2}) U†
29+
30+
Small eigenvalues below a specified threshold are treated as zero to ensure
31+
numerical stability and avoid divergences. The input matrix is explicitly
32+
symmetrized to enforce Hermiticity before diagonalization.
33+
34+
Parameters
35+
----------
36+
S : array_like (N, N)
37+
Real or complex overlap matrix. It is assumed to be Hermitian
38+
(or close to Hermitian within numerical precision).
39+
thresh : float, optional
40+
Eigenvalue cutoff below which eigenvalues are treated as zero.
41+
This prevents instabilities due to near-linear dependencies.
42+
Default is 1e-10.
43+
44+
Returns
45+
-------
46+
S_inv_sqrt : ndarray (N, N)
47+
The inverse square root of the overlap matrix, S^{-1/2}, computed
48+
in the Löwdin (symmetric) orthogonalization scheme.
49+
50+
Notes
51+
-----
52+
- The function enforces Hermiticity via (S + S†)/2 before diagonalization.
53+
- Input arrays are cast to float64 or complex128 to ensure compatibility
54+
with NumPy linear algebra routines.
55+
- Eigenvalues below `thresh` are set to zero, effectively projecting out
56+
linearly dependent components of the basis.
57+
- The resulting matrix can be used to orthonormalize a non-orthogonal basis:
58+
C_orth = S^{-1/2} C
59+
60+
Raises
61+
------
62+
LinAlgError
63+
If the eigenvalue decomposition fails.
64+
65+
Examples
66+
--------
67+
>>> S = np.array([[1.0, 0.2], [0.2, 1.0]])
68+
>>> S_inv_sqrt = lowdin_inverse_sqrt(S)
69+
>>> np.allclose(S_inv_sqrt @ S @ S_inv_sqrt, np.eye(2))
70+
True
71+
"""
72+
73+
# Ensure numpy array
74+
S = np.array(S)
75+
76+
# Force supported dtype
77+
if np.iscomplexobj(S):
78+
S = S.astype(np.complex128)
79+
else:
80+
S = S.astype(np.float64)
81+
82+
# Enforce Hermiticity
83+
S = 0.5 * (S + S.conj().T)
84+
85+
eigvals, eigvecs = np.linalg.eigh(S)
86+
87+
eigvals_inv_sqrt = np.zeros_like(eigvals)
88+
89+
for i, val in enumerate(eigvals):
90+
if val > thresh:
91+
eigvals_inv_sqrt[i] = 1.0 / np.sqrt(val)
92+
else:
93+
eigvals_inv_sqrt[i] = 0.0
94+
95+
S_inv_sqrt = eigvecs @ np.diag(eigvals_inv_sqrt) @ eigvecs.conj().T
96+
97+
return S_inv_sqrt

src/libra_py/packages/cp2k/methods.py

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

3434
from libra_py import data_outs, data_stat, data_conv
3535
from libra_py import units, molden_methods
36+
import libra_py.orthogonalizations as ortho
3637

3738

3839
def ndigits(integer_number: int):
@@ -2867,6 +2868,7 @@ def cp2k_compute_adi(q, params, full_id):
28672868
params.setdefault("MO_prev", {})
28682869
params.setdefault("data_prev", {})
28692870
params.setdefault("coordinates_prev", {})
2871+
params.setdefault("s_ci_inv_prev", {})
28702872
params.setdefault("is_first_time", {})
28712873
params.setdefault("act_state", {})
28722874

@@ -2918,6 +2920,7 @@ def cp2k_compute_adi(q, params, full_id):
29182920
obj.hvib_adi = CMATRIX(nstates, nstates)
29192921
obj.basis_transform = CMATRIX(nstates, nstates)
29202922
obj.time_overlap_adi = CMATRIX(nstates, nstates)
2923+
obj.overlap_adi = CMATRIX(nstates, nstates)
29212924

29222925
# ================= Compute overlaps =================
29232926
st_mo = sp.load_npz(params["time_overlap_filename"])
@@ -2927,6 +2930,9 @@ def cp2k_compute_adi(q, params, full_id):
29272930
else:
29282931
st_mo = np.asarray(st_mo)
29292932
st_mo = st_mo.astype(np.float64, copy=False)
2933+
2934+
s_mo = sp.load_npz(params["overlap_filename"]).todense()
2935+
29302936

29312937
ndim = st_mo.shape[0] // 2
29322938

@@ -2964,6 +2970,20 @@ def cp2k_compute_adi(q, params, full_id):
29642970

29652971
st_ci = ci.overlap(st_mo, data_prev, data_curr, ovlp_params)
29662972

2973+
s_ci = ci.overlap(s_mo, data_curr, data_curr, ovlp_params)
2974+
2975+
s_ci_inv_curr = ortho.lowdin_inverse_sqrt(s_ci)
2976+
s_ci_inv_prev = None
2977+
if is_first_time:
2978+
s_ci_inv_prev = copy.deepcopy(s_ci_inv_curr)
2979+
else:
2980+
s_ci_inv_prev = params["s_ci_inv_prev"].get(itraj, s_ci_inv_curr)
2981+
2982+
s_ci = s_ci_inv_curr @ s_ci @ s_ci_inv_curr
2983+
st_ci = s_ci_inv_prev @ st_ci @ s_ci_inv_curr
2984+
2985+
2986+
29672987
# ================= Populate Hamiltonian =================
29682988
for i in range(nstates):
29692989

@@ -2977,6 +2997,7 @@ def cp2k_compute_adi(q, params, full_id):
29772997

29782998
for j in range(nstates):
29792999
obj.time_overlap_adi.set(i, j, float(st_ci[i, j]))
3000+
obj.overlap_adi.set(i, j, float(s_ci[i,j]) )
29803001

29813002
# ================= Forces =================
29823003
obj.d1ham_adi = CMATRIXList()
@@ -3018,6 +3039,7 @@ def cp2k_compute_adi(q, params, full_id):
30183039
# params["MO_prev"][itraj] = MO_curr.copy()
30193040
params["data_prev"][itraj] = copy.deepcopy(data_curr)
30203041
params["coordinates_prev"][itraj] = coordinates.copy()
3042+
params["s_ci_inv_prev"][itraj] = copy.deepcopy(s_ci_inv_curr)
30213043
params["is_first_time"][itraj] = False
30223044

30233045
return obj

0 commit comments

Comments
 (0)