Repository: mne-tools/mne-python

Issue:
Complex data cannot be saved or read (stc's and evokeds)
#### Describe the bug
When complex data stored in an stc or and evokeds object is saved, the imaginary part is lost.
I found a quick fix for saving the complex data in stc's, but when reading it in again, it is lost again


#### Steps to reproduce
```
import mne
from os.path import join
from pathlib import Path
home = str(Path.home())

data_path = mne.datasets.sample.data_path()
subjects_dir = join(data_path, 'subjects')
subject = 'sample'
MEG_path = join(data_path, 'MEG', subject)

raw = mne.io.read_raw(join(MEG_path, 'sample_audvis_raw.fif'), preload=True)
raw.pick_types(meg=True)
cov = mne.compute_raw_covariance(raw)
raw.filter(14, 30)
raw.apply_hilbert()
events = mne.read_events(join(MEG_path, 'sample_audvis_raw-eve.fif'))
epochs = mne.Epochs(raw, events, 1, tmin=-0.200, tmax=0.600, baseline=None)
evokeds = epochs.average()
data_cov = mne.compute_covariance(epochs)

fwd = mne.read_forward_solution(join(MEG_path, 
                                     'sample_audvis-meg-vol-7-fwd.fif'))
filters = mne.beamformer.make_lcmv(epochs.info, fwd, data_cov, 0.05, cov,
                                   pick_ori='max-power', 
                                   weight_norm='unit-noise-gain')


stc = mne.beamformer.apply_lcmv(evokeds, filters)

# save stc
stc.save(join(home, 'delete_me'))

# save evokeds
mne.write_evokeds(join(home, 'delete_me-ave.fif'), evokeds)
```
#### Quick fix for saving data
Change line 119 (commented) to line 120 of `_write_stc` in _source_estimate.py_
It cannot be read again though since `_read_w` of _source_estimate.py_ throws away the imaginary part as well
![billede](https://user-images.githubusercontent.com/17312936/90864274-aa4a1900-e390-11ea-8aea-ee3aadba9d7e.png)


#### Expected results
A saved object with complex data in it


#### Actual results
![billede](https://user-images.githubusercontent.com/17312936/90862299-889b6280-e38d-11ea-8cc2-4d3cba851666.png)

![billede](https://user-images.githubusercontent.com/17312936/90862208-5f7ad200-e38d-11ea-8824-1ab6cab2df5f.png)



#### Additional information
![billede](https://user-images.githubusercontent.com/17312936/90862388-a799f480-e38d-11ea-9e5d-956965892bae.png)




Discussion:
try saving as .h5

>

thanks for your suggestion
both saving and loading works in the h5 format
ok close then.

thanks

doesn't solve the issue for evokeds though
did you try saving the evoked in .h5

>

mne.write_evokeds does not have an h5 option as far as I can tell, or is there a separate function for that?

ok then we need to fix something...

can you share a code snippet highlighting the pb?

>

Assuming that pb means problem, the code under "steps to reproduce" allows for reproducing the problem and warning.
Let me know if I misunderstood you
Raw and epochs handle complex data fine, so we can probably fix evoked

stc should probably raise an error if you try to save complex data without using h5 format

@ualsbombe do you want to try making either or both of these changes in a PR?
@larsoner I will try to make a pull request Monday - sounds good?
That would be great!

Resolving patch:
diff --git a/mne/evoked.py b/mne/evoked.py
--- a/mne/evoked.py
+++ b/mne/evoked.py
@@ -36,7 +36,7 @@
 from .io.proj import ProjMixin
 from .io.write import (start_file, start_block, end_file, end_block,
                        write_int, write_string, write_float_matrix,
-                       write_id, write_float)
+                       write_id, write_float, write_complex_float_matrix)
 from .io.base import TimeMixin, _check_maxshield
 
 _aspect_dict = {
@@ -1091,7 +1091,10 @@ def _read_evoked(fname, condition=None, kind='average', allow_maxshield=False):
         else:
             # Put the old style epochs together
             data = np.concatenate([e.data[None, :] for e in epoch], axis=0)
-        data = data.astype(np.float64)
+        if np.isrealobj(data):
+            data = data.astype(np.float64)
+        else:
+            data = data.astype(np.complex128)
 
         if first_time is not None and nsamp is not None:
             times = first_time + np.arange(nsamp) / info['sfreq']
@@ -1203,7 +1206,12 @@ def _write_evokeds(fname, evoked, check=True):
                 decal[k] = 1.0 / (e.info['chs'][k]['cal'] *
                                   e.info['chs'][k].get('scale', 1.0))
 
-            write_float_matrix(fid, FIFF.FIFF_EPOCH, decal * e.data)
+            if np.iscomplexobj(e.data):
+                write_function = write_complex_float_matrix
+            else:
+                write_function = write_float_matrix
+
+            write_function(fid, FIFF.FIFF_EPOCH, decal * e.data)
             end_block(fid, aspect)
             end_block(fid, FIFF.FIFFB_EVOKED)
 
diff --git a/mne/source_estimate.py b/mne/source_estimate.py
--- a/mne/source_estimate.py
+++ b/mne/source_estimate.py
@@ -2195,6 +2195,9 @@ def save(self, fname, ftype='stc', verbose=None):
         if ftype != 'h5' and len(self.vertices) != 1:
             raise ValueError('Can only write to .stc or .w if a single volume '
                              'source space was used, use .h5 instead')
+        if ftype != 'h5' and self.data.dtype == 'complex':
+            raise ValueError('Can only write non-complex data to .stc or .w'
+                             ', use .h5 instead')
         if ftype == 'stc':
             logger.info('Writing STC to disk...')
             if not (fname.endswith('-vl.stc') or fname.endswith('-vol.stc')):


Test patch:
diff --git a/mne/tests/test_evoked.py b/mne/tests/test_evoked.py
--- a/mne/tests/test_evoked.py
+++ b/mne/tests/test_evoked.py
@@ -201,6 +201,14 @@ def test_io_evoked(tmpdir):
             assert_equal(av1.first, av2.first)
             assert_equal(av1.comment, av2.comment)
 
+    # test saving and reading complex numbers in evokeds
+    ave_complex = ave.copy()
+    ave_complex._data = 1j * ave_complex.data
+    fname_temp = str(tmpdir.join('complex-ave.fif'))
+    ave_complex.save(fname_temp)
+    ave_complex = read_evokeds(fname_temp)[0]
+    assert_allclose(ave.data, ave_complex.data.imag)
+
     # test warnings on bad filenames
     fname2 = tmpdir.join('test-bad-name.fif')
     with pytest.warns(RuntimeWarning, match='-ave.fif'):
diff --git a/mne/tests/test_source_estimate.py b/mne/tests/test_source_estimate.py
--- a/mne/tests/test_source_estimate.py
+++ b/mne/tests/test_source_estimate.py
@@ -261,14 +261,22 @@ def test_expand():
         pytest.raises(ValueError, stc.__add__, stc.in_label(labels_lh[0]))
 
 
-def _fake_stc(n_time=10):
+def _fake_stc(n_time=10, is_complex=False):
+    np.random.seed(7)
     verts = [np.arange(10), np.arange(90)]
-    return SourceEstimate(np.random.rand(100, n_time), verts, 0, 1e-1, 'foo')
+    data = np.random.rand(100, n_time)
+    if is_complex:
+        data.astype(complex)
+    return SourceEstimate(data, verts, 0, 1e-1, 'foo')
 
 
-def _fake_vec_stc(n_time=10):
+def _fake_vec_stc(n_time=10, is_complex=False):
+    np.random.seed(7)
     verts = [np.arange(10), np.arange(90)]
-    return VectorSourceEstimate(np.random.rand(100, 3, n_time), verts, 0, 1e-1,
+    data = np.random.rand(100, 3, n_time)
+    if is_complex:
+        data.astype(complex)
+    return VectorSourceEstimate(data, verts, 0, 1e-1,
                                 'foo')
 
 
@@ -379,27 +387,32 @@ def test_io_stc(tmpdir):
 
 
 @requires_h5py
-def test_io_stc_h5(tmpdir):
+@pytest.mark.parametrize('is_complex', (True, False))
+@pytest.mark.parametrize('vector', (True, False))
+def test_io_stc_h5(tmpdir, is_complex, vector):
     """Test IO for STC files using HDF5."""
-    for stc in [_fake_stc(), _fake_vec_stc()]:
-        pytest.raises(ValueError, stc.save, tmpdir.join('tmp'),
-                      ftype='foo')
-        out_name = tmpdir.join('tmp')
-        stc.save(out_name, ftype='h5')
-        stc.save(out_name, ftype='h5')  # test overwrite
-        stc3 = read_source_estimate(out_name)
-        stc4 = read_source_estimate(out_name + '-stc')
-        stc5 = read_source_estimate(out_name + '-stc.h5')
-        pytest.raises(RuntimeError, read_source_estimate, out_name,
-                      subject='bar')
-        for stc_new in stc3, stc4, stc5:
-            assert_equal(stc_new.subject, stc.subject)
-            assert_array_equal(stc_new.data, stc.data)
-            assert_array_equal(stc_new.tmin, stc.tmin)
-            assert_array_equal(stc_new.tstep, stc.tstep)
-            assert_equal(len(stc_new.vertices), len(stc.vertices))
-            for v1, v2 in zip(stc_new.vertices, stc.vertices):
-                assert_array_equal(v1, v2)
+    if vector:
+        stc = _fake_vec_stc(is_complex=is_complex)
+    else:
+        stc = _fake_stc(is_complex=is_complex)
+    pytest.raises(ValueError, stc.save, tmpdir.join('tmp'),
+                  ftype='foo')
+    out_name = tmpdir.join('tmp')
+    stc.save(out_name, ftype='h5')
+    stc.save(out_name, ftype='h5')  # test overwrite
+    stc3 = read_source_estimate(out_name)
+    stc4 = read_source_estimate(out_name + '-stc')
+    stc5 = read_source_estimate(out_name + '-stc.h5')
+    pytest.raises(RuntimeError, read_source_estimate, out_name,
+                  subject='bar')
+    for stc_new in stc3, stc4, stc5:
+        assert_equal(stc_new.subject, stc.subject)
+        assert_array_equal(stc_new.data, stc.data)
+        assert_array_equal(stc_new.tmin, stc.tmin)
+        assert_array_equal(stc_new.tstep, stc.tstep)
+        assert_equal(len(stc_new.vertices), len(stc.vertices))
+        for v1, v2 in zip(stc_new.vertices, stc.vertices):
+            assert_array_equal(v1, v2)
 
 
 def test_io_w(tmpdir):