Skip to content

Commit f5662a8

Browse files
Fix orthgonality loss in Davidson using Gram-Schmidt reorthogonalisation (#191)
* * Fix orthgonality loss in Davidson using Gram-Schmidt reorthogonalisation in case orthogonality loss is detected. * * Differentiate between block size and number of guesses in Davidson procedure. In cases where n_guesses > n_states, this may lead to a decrease in the number of matrix applies needed till convergence. * fix bug in call to estimate_n_guesses * * Davidson block size as keyword argument 'n_block' and corresponding sanity checks. * adapt tests to changed number of guesses * move n_block sanity checks to eigsh * only update entries for the last n_ss_added vectors in the projected matrix * add more davidson tests * more explicit n_block test * add codecov token in CI --------- Co-authored-by: jonasleitner <[email protected]>
1 parent cc47611 commit f5662a8

File tree

7 files changed

+192
-50
lines changed

7 files changed

+192
-50
lines changed

.github/workflows/ci.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ jobs:
8989
if: matrix.documentation
9090
#
9191
- name: Upload coverage to codecov
92-
# Note: lcov curerntly produces some error and therefore requires the keep-going flag to complete.
92+
# Note: lcov currently produces some error and therefore requires the keep-going flag to complete.
9393
# Since the error might be the result of some gcc/gcov bug, I don't know how to resolve it currently.
9494
# lcov < 2.0 apparently did hide a lot of errors from the user so the problem might have been around for some time already...
9595
run: |
@@ -99,6 +99,8 @@ jobs:
9999
lcov --ignore-errors unused --remove coverage.info '/opt/*' '/Applications/*' '/Library/*' '/usr/*' "${HOME}"'/.cache/*' "${HOME}"'/.local/*' "${PWD}"'/build/*' "${PWD}"'/libadcc/tests/*' --output-file coverage.info
100100
lcov --list coverage.info
101101
codecov -X gcov -f coverage.info
102+
env:
103+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
102104
if: contains(matrix.os, 'ubuntu')
103105

104106
- name: Upload coverage to coveralls

adcc/solver/SolverStateBase.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def __init__(self, matrix):
3939
self.converged = False # Flag whether iteration is converged
4040
self.n_iter = 0 # Number of iterations
4141
self.n_applies = 0 # Number of applies
42+
self.reortho_triggers = [] # List of reorthogonalisation triggers
4243
self.timer = Timer() # Construct a new timer
4344

4445
def describe(self):
@@ -56,6 +57,10 @@ def describe(self):
5657
text += "| {0:<41s} {1:>15s} |\n".format(algorithm, conv)
5758
text += ("| {0:30s} n_iter={1:<3d} n_applies={2:<5d} |\n"
5859
"".format(problem[:30], self.n_iter, self.n_applies))
60+
text += ("| n_reortho={0:<7d} max_overlap_before_reortho={1:<10s} |\n"
61+
"".format(len(self.reortho_triggers),
62+
"{:<10.4E}".format(max(self.reortho_triggers))
63+
if len(self.reortho_triggers) > 0 else "N/A"))
5964
text += "+" + 60 * "-" + "+\n"
6065
text += ("| # eigenvalue res. norm "
6166
"dominant elements |\n")

adcc/solver/davidson.py

Lines changed: 86 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def default_print(state, identifier, file=sys.stdout):
7575

7676

7777
# TODO This function should be merged with eigsh
78-
def davidson_iterations(matrix, state, max_subspace, max_iter, n_ep,
78+
def davidson_iterations(matrix, state, max_subspace, max_iter, n_ep, n_block,
7979
is_converged, which, callback=None, preconditioner=None,
8080
preconditioning_method="Davidson", debug_checks=False,
8181
residual_min_norm=None, explicit_symmetrisation=None):
@@ -87,12 +87,15 @@ def davidson_iterations(matrix, state, max_subspace, max_iter, n_ep,
8787
Matrix to diagonalise
8888
state
8989
DavidsonState containing the eigenvector guess
90-
max_subspace : int or NoneType, optional
90+
max_subspace : int
9191
Maximal subspace size
92-
max_iter : int, optional
92+
max_iter : int
9393
Maximal number of iterations
94-
n_ep : int or NoneType, optional
94+
n_ep : int
9595
Number of eigenpairs to be computed
96+
n_block : int
97+
Davidson block size: the number of vectors that are added to the subspace
98+
in each iteration
9699
is_converged
97100
Function to test for convergence
98101
callback : callable, optional
@@ -131,11 +134,11 @@ def callback(state, identifier):
131134
# The problem size
132135
n_problem = matrix.shape[1]
133136

134-
# The block size
135-
n_block = len(state.subspace_vectors)
137+
# The current subspace size == Number of guesses
138+
n_ss_vec = len(state.subspace_vectors)
136139

137-
# The current subspace size
138-
n_ss_vec = n_block
140+
# Sanity checks for block size
141+
assert n_block >= n_ep and n_block <= n_ss_vec
139142

140143
# The current subspace
141144
SS = state.subspace_vectors
@@ -157,21 +160,23 @@ def callback(state, identifier):
157160
Ax = evaluate(matrix @ SS)
158161
state.n_applies += n_ss_vec
159162

163+
# Get the worksize view for the first iteration
164+
Ass = Ass_cont[:n_ss_vec, :n_ss_vec]
165+
166+
# Initiall projection of Ax onto the subspace exploiting the hermiticity
167+
with state.timer.record("projection"):
168+
for i in range(n_ss_vec):
169+
for j in range(i, n_ss_vec):
170+
Ass[i, j] = SS[i] @ Ax[j]
171+
if i != j:
172+
Ass[j, i] = Ass[i, j]
173+
160174
while state.n_iter < max_iter:
161175
state.n_iter += 1
162176

163177
assert len(SS) >= n_block
164178
assert len(SS) <= max_subspace
165179

166-
# Project A onto the subspace, keeping in mind
167-
# that the values Ass[:-n_block, :-n_block] are already valid,
168-
# since they have been computed in the previous iterations already.
169-
with state.timer.record("projection"):
170-
Ass = Ass_cont[:n_ss_vec, :n_ss_vec] # Increase the work view size
171-
for i in range(n_block):
172-
Ass[:, -n_block + i] = Ax[-n_block + i] @ SS
173-
Ass[-n_block:, :] = np.transpose(Ass[:, -n_block:])
174-
175180
# Compute the which(== largest, smallest, ...) eigenpair of Ass
176181
# and the associated ritz vector as well as residual
177182
with state.timer.record("rayleigh_ritz"):
@@ -237,7 +242,10 @@ def form_residual(rval, rvec):
237242
# Update projection of ADC matrix A onto subspace
238243
Ass = Ass_cont[:n_ss_vec, :n_ss_vec]
239244
for i in range(n_ss_vec):
240-
Ass[:, i] = Ax[i] @ SS
245+
for j in range(i, n_ss_vec):
246+
Ass[i, j] = SS[i] @ Ax[j]
247+
if i != j:
248+
Ass[j, i] = Ass[i, j]
241249
# continue to add residuals to space
242250

243251
with state.timer.record("preconditioner"):
@@ -266,12 +274,29 @@ def form_residual(rval, rvec):
266274
n_ss_added = 0
267275
for i in range(n_block):
268276
pvec = preconds[i]
269-
# Project out the components of the current subspace
277+
# Project out the components of the current subspace using
278+
# conventional Gram-Schmidt (CGS) procedure.
270279
# That is form (1 - SS * SS^T) * pvec = pvec + SS * (-SS^T * pvec)
271280
coefficients = np.hstack(([1], -(pvec @ SS)))
272281
pvec = lincomb(coefficients, [pvec] + SS, evaluate=True)
273282
pnorm = np.sqrt(pvec @ pvec)
274-
if pnorm > residual_min_norm:
283+
if pnorm < residual_min_norm:
284+
continue
285+
# Perform reorthogonalisation if loss of orthogonality is
286+
# detected; this comes at the expense of computing n_ss_vec
287+
# additional scalar products but avoids linear dependence
288+
# within the subspace.
289+
with state.timer.record("reorthogonalisation"):
290+
ss_overlap = np.array(pvec @ SS)
291+
max_ortho_loss = np.max(np.abs(ss_overlap)) / pnorm
292+
if max_ortho_loss > n_problem * eps:
293+
# Update pvec by instance reorthogonalised against SS
294+
# using a second CGS. Also update pnorm.
295+
coefficients = np.hstack(([1], -ss_overlap))
296+
pvec = lincomb(coefficients, [pvec] + SS, evaluate=True)
297+
pnorm = np.sqrt(pvec @ pvec)
298+
state.reortho_triggers.append(max_ortho_loss)
299+
if pnorm >= residual_min_norm:
275300
# Extend the subspace
276301
SS.append(evaluate(pvec / pnorm))
277302
n_ss_added += 1
@@ -284,8 +309,10 @@ def form_residual(rval, rvec):
284309
state.subspace_orthogonality = np.max(np.abs(orth))
285310
if state.subspace_orthogonality > n_problem * eps:
286311
warnings.warn(la.LinAlgWarning(
287-
"Subspace in davidson has lost orthogonality. "
288-
"Expect inaccurate results."
312+
"Subspace in Davidson has lost orthogonality. "
313+
"Max. deviation from orthogonality is {:.4E}. "
314+
"Expect inaccurate results.".format(
315+
state.subspace_orthogonality)
289316
))
290317

291318
if n_ss_added == 0:
@@ -300,12 +327,26 @@ def form_residual(rval, rvec):
300327
"be aborted without convergence. Try a different guess."))
301328
return state
302329

330+
# Matrix applies for the new vectors
303331
with state.timer.record("projection"):
304332
Ax.extend(matrix @ SS[-n_ss_added:])
305333
state.n_applies += n_ss_added
306334

335+
# Update the worksize view for the next iteration
336+
Ass = Ass_cont[:n_ss_vec, :n_ss_vec]
307337

308-
def eigsh(matrix, guesses, n_ep=None, max_subspace=None,
338+
# Project Ax onto the subspace, keeping in mind
339+
# that the values Ass[:-n_ss_added, :-n_ss_added] are already valid,
340+
# since they have been computed in the previous iterations already.
341+
with state.timer.record("projection"):
342+
for i in range(n_ss_vec - n_ss_added, n_ss_vec):
343+
for j in range(i + 1):
344+
Ass[i, j] = SS[i] @ Ax[j]
345+
if i != j:
346+
Ass[j, i] = Ass[i, j]
347+
348+
349+
def eigsh(matrix, guesses, n_ep=None, n_block=None, max_subspace=None,
309350
conv_tol=1e-9, which="SA", max_iter=70,
310351
callback=None, preconditioner=None,
311352
preconditioning_method="Davidson", debug_checks=False,
@@ -320,6 +361,9 @@ def eigsh(matrix, guesses, n_ep=None, max_subspace=None,
320361
Guess vectors (fixes also the Davidson block size)
321362
n_ep : int or NoneType, optional
322363
Number of eigenpairs to be computed
364+
n_block : int or NoneType, optional
365+
The solver block size: the number of vectors that are added to the subspace
366+
in each iteration
323367
max_subspace : int or NoneType, optional
324368
Maximal subspace size
325369
conv_tol : float, optional
@@ -364,11 +408,28 @@ def eigsh(matrix, guesses, n_ep=None, max_subspace=None,
364408
if n_ep is None:
365409
n_ep = len(guesses)
366410
elif n_ep > len(guesses):
367-
raise ValueError("n_ep cannot exceed the number of guess vectors.")
411+
raise ValueError(f"n_ep (= {n_ep}) cannot exceed the number of guess "
412+
f"vectors (= {len(guesses)}).")
413+
414+
if n_block is None:
415+
n_block = n_ep
416+
elif n_block < n_ep:
417+
raise ValueError(f"n_block (= {n_block}) cannot be smaller than the number "
418+
f"of states requested (= {n_ep}).")
419+
elif n_block > len(guesses):
420+
raise ValueError(f"n_block (= {n_block}) cannot exceed the number of guess "
421+
f"vectors (= {len(guesses)}).")
422+
368423
if not max_subspace:
369424
# TODO Arnoldi uses this:
370425
# max_subspace = max(2 * n_ep + 1, 20)
371426
max_subspace = max(6 * n_ep, 20, 5 * len(guesses))
427+
elif max_subspace < 2 * n_block:
428+
raise ValueError(f"max_subspace (= {max_subspace}) needs to be at least "
429+
f"twice as large as n_block (n_block = {n_block}).")
430+
elif max_subspace < len(guesses):
431+
raise ValueError(f"max_subspace (= {max_subspace}) cannot be smaller than "
432+
f"the number of guess vectors (= {len(guesses)}).")
372433

373434
def convergence_test(state):
374435
state.residuals_converged = state.residual_norms < conv_tol
@@ -385,7 +446,7 @@ def convergence_test(state):
385446

386447
state = DavidsonState(matrix, guesses)
387448
davidson_iterations(matrix, state, max_subspace, max_iter,
388-
n_ep=n_ep, is_converged=convergence_test,
449+
n_ep=n_ep, n_block=n_block, is_converged=convergence_test,
389450
callback=callback, which=which,
390451
preconditioner=preconditioner,
391452
preconditioning_method=preconditioning_method,

adcc/tests/backends/backends_crossref_test.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,14 @@ def test_adc2_h2o(self, system, case):
6565
pytest.skip("Veloxchem does not support f-functions. "
6666
"Not enough backends available.")
6767

68-
# fewer states available for fc-fv-cvs
69-
n_states = 5
70-
if "fc" in case and "fv" in case and "cvs" in case:
71-
n_states = 4
68+
kwargs = {"n_singlets": 5}
69+
# fewer states available for fc-fv-cvs (4) and fv-cvs (5)
70+
if "fv" in case and "cvs" in case:
71+
kwargs["n_singlets"] = 3
72+
kwargs["n_guesses"] = 3
73+
elif "cvs" in case:
74+
# state 5 and 6 are degenerate -> can't compare the eigenvectors
75+
kwargs["n_singlets"] = 4
7276

7377
method = "cvs-adc2" if "cvs" in case else "adc2"
7478
core_orbitals = system.core_orbitals if "cvs" in case else None
@@ -79,9 +83,9 @@ def test_adc2_h2o(self, system, case):
7983
for b in backends_test:
8084
scfres = cached_backend_hf(b, system, conv_tol=1e-10)
8185
results[b] = adcc.run_adc(
82-
scfres, method=method, n_singlets=n_states, conv_tol=1e-9,
86+
scfres, method=method, conv_tol=1e-9,
8387
core_orbitals=core_orbitals, frozen_core=frozen_core,
84-
frozen_virtual=frozen_virtual
88+
frozen_virtual=frozen_virtual, **kwargs
8589
)
8690
assert results[b].converged
8791
compare_adc_results(results, 5e-8)

adcc/tests/functionality_test.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -133,12 +133,16 @@ def test_functionality(self, system: str, case: str, method: str, kind: str,
133133
n_states = testcases.kinds_to_nstates([kind]).pop()
134134

135135
kwargs = {n_states: 3}
136-
# only few states available for h2o sto3g adc0/adc1
137-
if system.name == "h2o" and system.basis == "sto-3g" and method.level < 2:
138-
if "cvs" in case and "fv" in case:
139-
kwargs[n_states] = 1
140-
elif "cvs" in case:
141-
kwargs[n_states] = 2
136+
# only few states available for h2o sto3g
137+
if system.name == "h2o" and system.basis == "sto-3g":
138+
if method.level < 2: # adc0/adc1
139+
if "cvs" in case and "fv" in case:
140+
kwargs[n_states] = 1
141+
elif "cvs" in case:
142+
kwargs[n_states] = 2
143+
elif method.level < 4: # adc2/adc3
144+
if "cvs" in case and "fv" in case: # only 5 states available
145+
kwargs["n_guesses"] = 3
142146

143147
self.base_test(
144148
system=system, case=case, method=method.name, kind=kind,

0 commit comments

Comments
 (0)