Skip to content

Commit e1a4d0c

Browse files
Additional GEVP method with errors (#195)
* Added the function error_gevp() to compute the gevp with statistical errors. * Changed method name from error_gevp to error_GEVP and removed automatic gamma method * added auto_gamma to error_GEVP * Specified exceptions in Corr.error_GEVP * Fixed a wrong path. It should be np.linalg.LinAlgError * Added a test for error_GEVP * The tests of error_gevp loads a test matrix * Incorporated eigenvectors with uncertainties in GEVP routine * Cleaned up GEVP routines * Cleaned up breaking change from merge * Released tolerance in test of GEVP * Repaired broken GEVP test --------- Co-authored-by: Simon Kuberski <simon.kuberski@uni-muenster.de>
1 parent a8d1663 commit e1a4d0c

File tree

5 files changed

+141
-23
lines changed

5 files changed

+141
-23
lines changed

pyerrors/correlators.py

Lines changed: 104 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from .misc import dump_object, _assert_equal_properties
99
from .fits import least_squares
1010
from .roots import find_root
11+
from . import linalg
1112

1213

1314
class Corr:
@@ -298,7 +299,7 @@ def matrix_symmetric(self):
298299
transposed = [None if _check_for_none(self, G) else G.T for G in self.content]
299300
return 0.5 * (Corr(transposed) + self)
300301

301-
def GEVP(self, t0, ts=None, sort="Eigenvalue", **kwargs):
302+
def GEVP(self, t0, ts=None, sort="Eigenvalue", vector_obs=False, **kwargs):
302303
r'''Solve the generalized eigenvalue problem on the correlator matrix and returns the corresponding eigenvectors.
303304
304305
The eigenvectors are sorted according to the descending eigenvalues, the zeroth eigenvector(s) correspond to the
@@ -317,14 +318,21 @@ def GEVP(self, t0, ts=None, sort="Eigenvalue", **kwargs):
317318
If sort="Eigenvector" it gives a reference point for the sorting method.
318319
sort : string
319320
If this argument is set, a list of self.T vectors per state is returned. If it is set to None, only one vector is returned.
320-
- "Eigenvalue": The eigenvector is chosen according to which eigenvalue it belongs individually on every timeslice.
321+
- "Eigenvalue": The eigenvector is chosen according to which eigenvalue it belongs individually on every timeslice. (default)
321322
- "Eigenvector": Use the method described in arXiv:2004.10472 to find the set of v(t) belonging to the state.
322323
The reference state is identified by its eigenvalue at $t=t_s$.
324+
- None: The GEVP is solved only at ts, no sorting is necessary
325+
vector_obs : bool
326+
If True, uncertainties are propagated in the eigenvector computation (default False).
323327
324328
Other Parameters
325329
----------------
326330
state : int
327331
Returns only the vector(s) for a specified state. The lowest state is zero.
332+
method : str
333+
Method used to solve the GEVP.
334+
- "eigh": Use scipy.linalg.eigh to solve the GEVP. (default for vector_obs=False)
335+
- "cholesky": Use manually implemented solution via the Cholesky decomposition. Automatically chosen if vector_obs==True.
328336
'''
329337

330338
if self.N == 1:
@@ -342,25 +350,43 @@ def GEVP(self, t0, ts=None, sort="Eigenvalue", **kwargs):
342350
else:
343351
symmetric_corr = self.matrix_symmetric()
344352

345-
G0 = np.vectorize(lambda x: x.value)(symmetric_corr[t0])
346-
np.linalg.cholesky(G0) # Check if matrix G0 is positive-semidefinite.
353+
def _get_mat_at_t(t, vector_obs=vector_obs):
354+
if vector_obs:
355+
return symmetric_corr[t]
356+
else:
357+
return np.vectorize(lambda x: x.value)(symmetric_corr[t])
358+
G0 = _get_mat_at_t(t0)
359+
360+
method = kwargs.get('method', 'eigh')
361+
if vector_obs:
362+
chol = linalg.cholesky(G0)
363+
chol_inv = linalg.inv(chol)
364+
method = 'cholesky'
365+
else:
366+
chol = np.linalg.cholesky(_get_mat_at_t(t0, vector_obs=False)) # Check if matrix G0 is positive-semidefinite.
367+
if method == 'cholesky':
368+
chol_inv = np.linalg.inv(chol)
369+
else:
370+
chol_inv = None
347371

348372
if sort is None:
349373
if (ts is None):
350374
raise Exception("ts is required if sort=None.")
351375
if (self.content[t0] is None) or (self.content[ts] is None):
352376
raise Exception("Corr not defined at t0/ts.")
353-
Gt = np.vectorize(lambda x: x.value)(symmetric_corr[ts])
354-
reordered_vecs = _GEVP_solver(Gt, G0)
377+
Gt = _get_mat_at_t(ts)
378+
reordered_vecs = _GEVP_solver(Gt, G0, method=method, chol_inv=chol_inv)
379+
if kwargs.get('auto_gamma', False) and vector_obs:
380+
[[o.gm() for o in ev if isinstance(o, Obs)] for ev in reordered_vecs]
355381

356382
elif sort in ["Eigenvalue", "Eigenvector"]:
357383
if sort == "Eigenvalue" and ts is not None:
358384
warnings.warn("ts has no effect when sorting by eigenvalue is chosen.", RuntimeWarning)
359385
all_vecs = [None] * (t0 + 1)
360386
for t in range(t0 + 1, self.T):
361387
try:
362-
Gt = np.vectorize(lambda x: x.value)(symmetric_corr[t])
363-
all_vecs.append(_GEVP_solver(Gt, G0))
388+
Gt = _get_mat_at_t(t)
389+
all_vecs.append(_GEVP_solver(Gt, G0, method=method, chol_inv=chol_inv))
364390
except Exception:
365391
all_vecs.append(None)
366392
if sort == "Eigenvector":
@@ -369,15 +395,17 @@ def GEVP(self, t0, ts=None, sort="Eigenvalue", **kwargs):
369395
all_vecs = _sort_vectors(all_vecs, ts)
370396

371397
reordered_vecs = [[v[s] if v is not None else None for v in all_vecs] for s in range(self.N)]
398+
if kwargs.get('auto_gamma', False) and vector_obs:
399+
[[[o.gm() for o in evn] for evn in ev if evn is not None] for ev in reordered_vecs]
372400
else:
373-
raise Exception("Unkown value for 'sort'.")
401+
raise Exception("Unknown value for 'sort'. Choose 'Eigenvalue', 'Eigenvector' or None.")
374402

375403
if "state" in kwargs:
376404
return reordered_vecs[kwargs.get("state")]
377405
else:
378406
return reordered_vecs
379407

380-
def Eigenvalue(self, t0, ts=None, state=0, sort="Eigenvalue"):
408+
def Eigenvalue(self, t0, ts=None, state=0, sort="Eigenvalue", **kwargs):
381409
"""Determines the eigenvalue of the GEVP by solving and projecting the correlator
382410
383411
Parameters
@@ -387,7 +415,7 @@ def Eigenvalue(self, t0, ts=None, state=0, sort="Eigenvalue"):
387415
388416
All other parameters are identical to the ones of Corr.GEVP.
389417
"""
390-
vec = self.GEVP(t0, ts=ts, sort=sort)[state]
418+
vec = self.GEVP(t0, ts=ts, sort=sort, **kwargs)[state]
391419
return self.projected(vec)
392420

393421
def Hankel(self, N, periodic=False):
@@ -1386,8 +1414,13 @@ def prune(self, Ntrunc, tproj=3, t0proj=2, basematrix=None):
13861414
return Corr(newcontent)
13871415

13881416

1389-
def _sort_vectors(vec_set, ts):
1417+
def _sort_vectors(vec_set_in, ts):
13901418
"""Helper function used to find a set of Eigenvectors consistent over all timeslices"""
1419+
1420+
if isinstance(vec_set_in[ts][0][0], Obs):
1421+
vec_set = [anp.vectorize(lambda x: float(x))(vi) if vi is not None else vi for vi in vec_set_in]
1422+
else:
1423+
vec_set = vec_set_in
13911424
reference_sorting = np.array(vec_set[ts])
13921425
N = reference_sorting.shape[0]
13931426
sorted_vec_set = []
@@ -1406,9 +1439,9 @@ def _sort_vectors(vec_set, ts):
14061439
if current_score > best_score:
14071440
best_score = current_score
14081441
best_perm = perm
1409-
sorted_vec_set.append([vec_set[t][k] for k in best_perm])
1442+
sorted_vec_set.append([vec_set_in[t][k] for k in best_perm])
14101443
else:
1411-
sorted_vec_set.append(vec_set[t])
1444+
sorted_vec_set.append(vec_set_in[t])
14121445

14131446
return sorted_vec_set
14141447

@@ -1418,10 +1451,63 @@ def _check_for_none(corr, entry):
14181451
return len(list(filter(None, np.asarray(entry).flatten()))) < corr.N ** 2
14191452

14201453

1421-
def _GEVP_solver(Gt, G0):
1422-
"""Helper function for solving the GEVP and sorting the eigenvectors.
1454+
def _GEVP_solver(Gt, G0, method='eigh', chol_inv=None):
1455+
r"""Helper function for solving the GEVP and sorting the eigenvectors.
1456+
1457+
Solves $G(t)v_i=\lambda_i G(t_0)v_i$ and returns the eigenvectors v_i
14231458
14241459
The helper function assumes that both provided matrices are symmetric and
14251460
only processes the lower triangular part of both matrices. In case the matrices
1426-
are not symmetric the upper triangular parts are effectively discarded."""
1427-
return scipy.linalg.eigh(Gt, G0, lower=True)[1].T[::-1]
1461+
are not symmetric the upper triangular parts are effectively discarded.
1462+
1463+
Parameters
1464+
----------
1465+
Gt : array
1466+
The correlator at time t for the left hand side of the GEVP
1467+
G0 : array
1468+
The correlator at time t0 for the right hand side of the GEVP
1469+
Method used to solve the GEVP.
1470+
- "eigh": Use scipy.linalg.eigh to solve the GEVP.
1471+
- "cholesky": Use manually implemented solution via the Cholesky decomposition.
1472+
chol_inv : array, optional
1473+
Inverse of the Cholesky decomposition of G0. May be provided to
1474+
speed up the computation in the case of method=='cholesky'
1475+
1476+
"""
1477+
if isinstance(G0[0][0], Obs):
1478+
vector_obs = True
1479+
else:
1480+
vector_obs = False
1481+
1482+
if method == 'cholesky':
1483+
if vector_obs:
1484+
cholesky = linalg.cholesky
1485+
inv = linalg.inv
1486+
eigv = linalg.eigv
1487+
matmul = linalg.matmul
1488+
else:
1489+
cholesky = np.linalg.cholesky
1490+
inv = np.linalg.inv
1491+
1492+
def eigv(x, **kwargs):
1493+
return np.linalg.eigh(x)[1]
1494+
1495+
def matmul(*operands):
1496+
return np.linalg.multi_dot(operands)
1497+
N = Gt.shape[0]
1498+
output = [[] for j in range(N)]
1499+
if chol_inv is None:
1500+
chol = cholesky(G0) # This will automatically report if the matrix is not pos-def
1501+
chol_inv = inv(chol)
1502+
1503+
try:
1504+
new_matrix = matmul(chol_inv, Gt, chol_inv.T)
1505+
ev = eigv(new_matrix)
1506+
ev = matmul(chol_inv.T, ev)
1507+
output = np.flip(ev, axis=1).T
1508+
except (np.linalg.LinAlgError, TypeError, ValueError): # The above code can fail because of linalg-errors or because the entry of the corr is None
1509+
for s in range(N):
1510+
output[s] = None
1511+
return output
1512+
elif method == 'eigh':
1513+
return scipy.linalg.eigh(Gt, G0, lower=True)[1].T[::-1]

pyerrors/linalg.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,12 @@ def eig(obs, **kwargs):
271271
return w
272272

273273

274+
def eigv(obs, **kwargs):
275+
"""Computes the eigenvectors of a given hermitian matrix of Obs according to np.linalg.eigh."""
276+
v = derived_observable(lambda x, **kwargs: anp.linalg.eigh(x)[1], obs)
277+
return v
278+
279+
274280
def pinv(obs, **kwargs):
275281
"""Computes the Moore-Penrose pseudoinverse of a matrix of Obs."""
276282
return derived_observable(lambda x, **kwargs: anp.linalg.pinv(x), obs)

tests/correlators_test.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -200,17 +200,17 @@ def test_padded_correlator():
200200

201201
def test_corr_exceptions():
202202
obs_a = pe.Obs([np.random.normal(0.1, 0.1, 100)], ['test'])
203-
obs_b= pe.Obs([np.random.normal(0.1, 0.1, 99)], ['test'])
203+
obs_b = pe.Obs([np.random.normal(0.1, 0.1, 99)], ['test'])
204204
with pytest.raises(Exception):
205205
pe.Corr([obs_a, obs_b])
206206

207207
obs_a = pe.Obs([np.random.normal(0.1, 0.1, 100)], ['test'])
208-
obs_b= pe.Obs([np.random.normal(0.1, 0.1, 100)], ['test'], idl=[range(1, 200, 2)])
208+
obs_b = pe.Obs([np.random.normal(0.1, 0.1, 100)], ['test'], idl=[range(1, 200, 2)])
209209
with pytest.raises(Exception):
210210
pe.Corr([obs_a, obs_b])
211211

212212
obs_a = pe.Obs([np.random.normal(0.1, 0.1, 100)], ['test'])
213-
obs_b= pe.Obs([np.random.normal(0.1, 0.1, 100)], ['test2'])
213+
obs_b = pe.Obs([np.random.normal(0.1, 0.1, 100)], ['test2'])
214214
with pytest.raises(Exception):
215215
pe.Corr([obs_a, obs_b])
216216

@@ -436,6 +436,7 @@ def test_GEVP_solver():
436436
sp_vecs = [v / np.sqrt((v.T @ mat2 @ v)) for v in sp_vecs]
437437

438438
assert np.allclose(sp_vecs, pe.correlators._GEVP_solver(mat1, mat2), atol=1e-14)
439+
assert np.allclose(np.abs(sp_vecs), np.abs(pe.correlators._GEVP_solver(mat1, mat2, method='cholesky')))
439440

440441

441442
def test_GEVP_none_entries():
@@ -552,7 +553,7 @@ def test_corr_no_filtering():
552553
li = [-pe.pseudo_Obs(.2, .1, 'a', samples=10) for i in range(96)]
553554
for i in range(len(li)):
554555
li[i].idl['a'] = range(1, 21, 2)
555-
c= pe.Corr(li)
556+
c = pe.Corr(li)
556557
b = pe.pseudo_Obs(1, 1e-11, 'a', samples=30)
557558
c *= b
558559
assert np.all([c[0].idl == o.idl for o in c])
@@ -572,6 +573,28 @@ def test_corr_symmetric():
572573
assert scorr[0] == corr[0]
573574

574575

576+
def test_error_GEVP():
577+
corr = pe.input.json.load_json("tests/data/test_matrix_corr.json.gz")
578+
t0, ts, state = 3, 6, 0
579+
vec_regular = corr.GEVP(t0=t0)[state][ts]
580+
vec_errors = corr.GEVP(t0=t0, vector_obs=True, auto_gamma=True)[state][ts]
581+
vec_regular_chol = corr.GEVP(t0=t0, method='cholesky')[state][ts]
582+
print(vec_errors)
583+
print(type(vec_errors[0]))
584+
assert(np.isclose(np.asarray([e.value for e in vec_errors]), vec_regular).all())
585+
assert(all([e.dvalue > 0. for e in vec_errors]))
586+
587+
projected_regular = corr.projected(vec_regular).content[ts + 1][0]
588+
projected_errors = corr.projected(vec_errors).content[ts + 1][0]
589+
projected_regular.gamma_method()
590+
projected_errors.gamma_method()
591+
assert(projected_errors.dvalue > projected_regular.dvalue)
592+
assert(corr.GEVP(t0, vector_obs=True)[state][42] is None)
593+
594+
assert(np.isclose(vec_regular_chol, vec_regular).all())
595+
assert(np.isclose(corr.GEVP(t0=t0, state=state)[ts], vec_regular).all())
596+
597+
575598
def test_corr_array_ndim1_init():
576599
y = [pe.pseudo_Obs(2 + np.random.normal(0.0, 0.1), .1, 't') for i in np.arange(5)]
577600
cc1 = pe.Corr(y)
@@ -688,7 +711,6 @@ def test_matrix_trace():
688711
for el in corr.trace():
689712
assert el == 0
690713

691-
692714
with pytest.raises(ValueError):
693715
corr.item(0, 0).trace()
694716

181 KB
Binary file not shown.

tests/linalg_test.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,10 @@ def test_matrix_functions():
297297
for j in range(dim):
298298
assert tmp[j].is_zero()
299299

300+
# Check eigv
301+
v2 = pe.linalg.eigv(sym)
302+
assert(np.all(v - v2).is_zero())
303+
300304
# Check eig function
301305
e2 = pe.linalg.eig(sym)
302306
assert np.all(np.sort(e) == np.sort(e2))

0 commit comments

Comments
 (0)