Repository: fjosw/pyerrors

Issue:
projected not working for correlators with "None" elements
I think the reason for this problem is the same which was solved in matrix_symmetric() recently. A minimal working example is

```python
import pyerrors as pe
import numpy as np

a = pe.pseudo_Obs(1.0,0.1,'a')
l = np.asarray([[a,a],[a,a]])
n = np.asarray([[None,None],[None,None]])
x = [l,n]
matr = pe.Corr(x)
matr.projected(np.asarray([1.0,0.0]))
```

which results in

```python
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [1], in <cell line: 9>()
      7 x = [l,n]
      8 matr = pe.Corr(x)
----> 9 matr.projected(np.asarray([1.0,0.0]))

File ~/.local/lib/python3.8/site-packages/pyerrors/correlators.py:158, in Corr.projected(self, vector_l, vector_r, normalize)
    156     if normalize:
    157         vector_l, vector_r = vector_l / np.sqrt((vector_l @ vector_l)), vector_r / np.sqrt(vector_r @ vector_r)
--> 158     newcontent = [None if (item is None) else np.asarray([vector_l.T @ item @ vector_r]) for item in self.content]
    160 else:
    161     # There are no checks here yet. There are so many possible scenarios, where this can go wrong.
    162     if normalize:

File ~/.local/lib/python3.8/site-packages/pyerrors/correlators.py:158, in <listcomp>(.0)
    156     if normalize:
    157         vector_l, vector_r = vector_l / np.sqrt((vector_l @ vector_l)), vector_r / np.sqrt(vector_r @ vector_r)
--> 158     newcontent = [None if (item is None) else np.asarray([vector_l.T @ item @ vector_r]) for item in self.content]
    160 else:
    161     # There are no checks here yet. There are so many possible scenarios, where this can go wrong.
    162     if normalize:

TypeError: unsupported operand type(s) for *: 'float' and 'NoneType'
```

As far as I understand it, the part

```python
None if (item is None) ...
```

in line 158 in correlators.py is not working as expected since

```python
n = np.asarray([[None,None],[None,None]])
n is None
```

returns

```python
False
```


Discussion:

Patch:
diff --git a/pyerrors/correlators.py b/pyerrors/correlators.py
index 7046135..d4d6206 100644
--- a/pyerrors/correlators.py
+++ b/pyerrors/correlators.py
@@ -155,7 +155,7 @@ class Corr:
                 raise Exception("Vectors are of wrong shape!")
             if normalize:
                 vector_l, vector_r = vector_l / np.sqrt((vector_l @ vector_l)), vector_r / np.sqrt(vector_r @ vector_r)
-            newcontent = [None if (item is None) else np.asarray([vector_l.T @ item @ vector_r]) for item in self.content]
+            newcontent = [None if _check_for_none(self, item) else np.asarray([vector_l.T @ item @ vector_r]) for item in self.content]
 
         else:
             # There are no checks here yet. There are so many possible scenarios, where this can go wrong.
@@ -163,7 +163,7 @@ class Corr:
                 for t in range(self.T):
                     vector_l[t], vector_r[t] = vector_l[t] / np.sqrt((vector_l[t] @ vector_l[t])), vector_r[t] / np.sqrt(vector_r[t] @ vector_r[t])
 
-            newcontent = [None if (self.content[t] is None or vector_l[t] is None or vector_r[t] is None) else np.asarray([vector_l[t].T @ self.content[t] @ vector_r[t]]) for t in range(self.T)]
+            newcontent = [None if (_check_for_none(self, self.content[t]) or vector_l[t] is None or vector_r[t] is None) else np.asarray([vector_l[t].T @ self.content[t] @ vector_r[t]]) for t in range(self.T)]
         return Corr(newcontent)
 
     def item(self, i, j):
@@ -197,6 +197,8 @@ class Corr:
 
     def symmetric(self):
         """ Symmetrize the correlator around x0=0."""
+        if self.N != 1:
+            raise Exception('symmetric cannot be safely applied to multi-dimensional correlators.')
         if self.T % 2 != 0:
             raise Exception("Can not symmetrize odd T")
 
@@ -215,6 +217,8 @@ class Corr:
 
     def anti_symmetric(self):
         """Anti-symmetrize the correlator around x0=0."""
+        if self.N != 1:
+            raise Exception('anti_symmetric cannot be safely applied to multi-dimensional correlators.')
         if self.T % 2 != 0:
             raise Exception("Can not symmetrize odd T")
 
@@ -236,7 +240,7 @@ class Corr:
     def matrix_symmetric(self):
         """Symmetrizes the correlator matrices on every timeslice."""
         if self.N > 1:
-            transposed = [None if len(list(filter(None, np.asarray(G).flatten()))) < self.N ** 2 else G.T for G in self.content]
+            transposed = [None if _check_for_none(self, G) else G.T for G in self.content]
             return 0.5 * (Corr(transposed) + self)
         if self.N == 1:
             raise Exception("Trying to symmetrize a correlator matrix, that already has N=1.")
@@ -419,13 +423,15 @@ class Corr:
             Can either be an Obs which is correlated with all entries of the
             correlator or a Corr of same length.
         """
+        if self.N != 1:
+            raise Exception("Only one-dimensional correlators can be safely correlated.")
         new_content = []
         for x0, t_slice in enumerate(self.content):
-            if t_slice is None:
+            if _check_for_none(self, t_slice):
                 new_content.append(None)
             else:
                 if isinstance(partner, Corr):
-                    if partner.content[x0] is None:
+                    if _check_for_none(partner, partner.content[x0]):
                         new_content.append(None)
                     else:
                         new_content.append(np.array([correlate(o, partner.content[x0][0]) for o in t_slice]))
@@ -449,9 +455,11 @@ class Corr:
             the reweighting factor on all configurations in weight.idl and not
             on the configurations in obs[i].idl.
         """
+        if self.N != 1:
+            raise Exception("Reweighting only implemented for one-dimensional correlators.")
         new_content = []
         for t_slice in self.content:
-            if t_slice is None:
+            if _check_for_none(self, t_slice):
                 new_content.append(None)
             else:
                 new_content.append(np.array(reweight(weight, t_slice, **kwargs)))
@@ -467,6 +475,8 @@ class Corr:
         partity : int
             Parity quantum number of the correlator, can be +1 or -1
         """
+        if self.N != 1:
+            raise Exception("T_symmetry only implemented for one-dimensional correlators.")
         if not isinstance(partner, Corr):
             raise Exception("T partner has to be a Corr object.")
         if parity not in [+1, -1]:
@@ -494,6 +504,8 @@ class Corr:
             decides which definition of the finite differences derivative is used.
             Available choice: symmetric, forward, backward, improved, default: symmetric
         """
+        if self.N != 1:
+            raise Exception("deriv only implemented for one-dimensional correlators.")
         if variant == "symmetric":
             newcontent = []
             for t in range(1, self.T - 1):
@@ -546,6 +558,8 @@ class Corr:
             decides which definition of the finite differences derivative is used.
             Available choice: symmetric, improved, default: symmetric
         """
+        if self.N != 1:
+            raise Exception("second_deriv only implemented for one-dimensional correlators.")
         if variant == "symmetric":
             newcontent = []
             for t in range(1, self.T - 1):
@@ -923,7 +937,7 @@ class Corr:
                 raise Exception("Addition of Corrs with different shape")
             newcontent = []
             for t in range(self.T):
-                if (self.content[t] is None) or (y.content[t] is None):
+                if _check_for_none(self, self.content[t]) or _check_for_none(y, y.content[t]):
                     newcontent.append(None)
                 else:
                     newcontent.append(self.content[t] + y.content[t])
@@ -932,7 +946,7 @@ class Corr:
         elif isinstance(y, (Obs, int, float, CObs)):
             newcontent = []
             for t in range(self.T):
-                if (self.content[t] is None):
+                if _check_for_none(self, self.content[t]):
                     newcontent.append(None)
                 else:
                     newcontent.append(self.content[t] + y)
@@ -951,7 +965,7 @@ class Corr:
                 raise Exception("Multiplication of Corr object requires N=N or N=1 and T=T")
             newcontent = []
             for t in range(self.T):
-                if (self.content[t] is None) or (y.content[t] is None):
+                if _check_for_none(self, self.content[t]) or _check_for_none(y, y.content[t]):
                     newcontent.append(None)
                 else:
                     newcontent.append(self.content[t] * y.content[t])
@@ -960,7 +974,7 @@ class Corr:
         elif isinstance(y, (Obs, int, float, CObs)):
             newcontent = []
             for t in range(self.T):
-                if (self.content[t] is None):
+                if _check_for_none(self, self.content[t]):
                     newcontent.append(None)
                 else:
                     newcontent.append(self.content[t] * y)
@@ -979,12 +993,12 @@ class Corr:
                 raise Exception("Multiplication of Corr object requires N=N or N=1 and T=T")
             newcontent = []
             for t in range(self.T):
-                if (self.content[t] is None) or (y.content[t] is None):
+                if _check_for_none(self, self.content[t]) or _check_for_none(y, y.content[t]):
                     newcontent.append(None)
                 else:
                     newcontent.append(self.content[t] / y.content[t])
             for t in range(self.T):
-                if newcontent[t] is None:
+                if _check_for_none(self, newcontent[t]):
                     continue
                 if np.isnan(np.sum(newcontent[t]).value):
                     newcontent[t] = None
@@ -1003,7 +1017,7 @@ class Corr:
 
             newcontent = []
             for t in range(self.T):
-                if (self.content[t] is None):
+                if _check_for_none(self, self.content[t]):
                     newcontent.append(None)
                 else:
                     newcontent.append(self.content[t] / y)
@@ -1014,7 +1028,7 @@ class Corr:
                 raise Exception('Division by zero will return undefined correlator')
             newcontent = []
             for t in range(self.T):
-                if (self.content[t] is None):
+                if _check_for_none(self, self.content[t]):
                     newcontent.append(None)
                 else:
                     newcontent.append(self.content[t] / y)
@@ -1028,7 +1042,7 @@ class Corr:
             raise TypeError('Corr / wrong type')
 
     def __neg__(self):
-        newcontent = [None if (item is None) else -1. * item for item in self.content]
+        newcontent = [None if _check_for_none(self, item) else -1. * item for item in self.content]
         return Corr(newcontent, prange=self.prange)
 
     def __sub__(self, y):
@@ -1036,31 +1050,31 @@ class Corr:
 
     def __pow__(self, y):
         if isinstance(y, (Obs, int, float, CObs)):
-            newcontent = [None if (item is None) else item**y for item in self.content]
+            newcontent = [None if _check_for_none(self, item) else item**y for item in self.content]
             return Corr(newcontent, prange=self.prange)
         else:
             raise TypeError('Type of exponent not supported')
 
     def __abs__(self):
-        newcontent = [None if (item is None) else np.abs(item) for item in self.content]
+        newcontent = [None if _check_for_none(self, item) else np.abs(item) for item in self.content]
         return Corr(newcontent, prange=self.prange)
 
     # The numpy functions:
     def sqrt(self):
-        return self**0.5
+        return self ** 0.5
 
     def log(self):
-        newcontent = [None if (item is None) else np.log(item) for item in self.content]
+        newcontent = [None if _check_for_none(self, item) else np.log(item) for item in self.content]
         return Corr(newcontent, prange=self.prange)
 
     def exp(self):
-        newcontent = [None if (item is None) else np.exp(item) for item in self.content]
+        newcontent = [None if _check_for_none(self, item) else np.exp(item) for item in self.content]
         return Corr(newcontent, prange=self.prange)
 
     def _apply_func_to_corr(self, func):
-        newcontent = [None if (item is None) else func(item) for item in self.content]
+        newcontent = [None if _check_for_none(self, item) else func(item) for item in self.content]
         for t in range(self.T):
-            if newcontent[t] is None:
+            if _check_for_none(self, newcontent[t]):
                 continue
             if np.isnan(np.sum(newcontent[t]).value):
                 newcontent[t] = None
@@ -1222,6 +1236,11 @@ def _sort_vectors(vec_set, ts):
     return sorted_vec_set
 
 
+def _check_for_none(corr, entry):
+    """Checks if entry for correlator corr is None"""
+    return len(list(filter(None, np.asarray(entry).flatten()))) < corr.N ** 2
+
+
 def _GEVP_solver(Gt, G0):
     """Helper function for solving the GEVP and sorting the eigenvectors.
 


Test patch:
diff --git a/tests/correlators_test.py b/tests/correlators_test.py
index 4f642f8..0aadcb8 100644
--- a/tests/correlators_test.py
+++ b/tests/correlators_test.py
@@ -246,6 +246,21 @@ def test_matrix_corr():
     corr_mat.Eigenvalue(2, state=0)
 
 
+def test_corr_none_entries():
+    a = pe.pseudo_Obs(1.0, 0.1, 'a')
+    l = np.asarray([[a, a], [a, a]])
+    n = np.asarray([[None, None], [None, None]])
+    x = [l, n]
+    matr = pe.Corr(x)
+    matr.projected(np.asarray([1.0, 0.0]))
+
+    matr * 2 - 2 * matr
+    matr * matr + matr ** 2 / matr
+
+    for func in [np.sqrt, np.log, np.exp, np.sin, np.cos, np.tan, np.sinh, np.cosh, np.tanh]:
+        func(matr)
+
+
 def test_GEVP_warnings():
     corr_aa = _gen_corr(1)
     corr_ab = 0.5 * corr_aa