Repository: mne-tools/mne-python

Issue:
Retaining complex stc.data when applying a morph
If your issue is a usage question, please consider asking on our [Gitter channel](https://gitter.im/mne-tools/mne-python) or on our [mailing list](https://mail.nmr.mgh.harvard.edu/mailman/listinfo/mne_analysis) instead of opening an issue.

#### Describe the problem
Please provide a clear and concise description of the problem.
Morphing an stc with complex data results in losing the imaginary part


#### Describe your solution
A clear and concise description of what you want to happen.
I would like to retain the values as stc.data as complex numberts

#### Describe possible alternatives
A clear and concise description of any alternative solutions or features you have considered.


#### Additional context
Add any other context or screenshots about the feature request here.
I tried to make mne.morph._interpolate_data inherit the dtype of stc.data (picture from morph.py)
![billede](https://user-images.githubusercontent.com/17312936/90513200-894eb180-e15f-11ea-8ff3-acbe0358fccc.png)

but there seem to be later parts that also drop the imaginary part. I haven't been able to identify those despite an hour's attempt or so
I get the following warning, whose origin I cannot identify

![billede](https://user-images.githubusercontent.com/17312936/90513613-36c1c500-e160-11ea-9d36-a9a79842c4a6.png)





Discussion:
can you share a tiny and self-sufficient script to replicate the problem?
it helps us investigate

>

you'll get it first thing tomorrow

Resolving patch:
diff --git a/mne/fixes.py b/mne/fixes.py
--- a/mne/fixes.py
+++ b/mne/fixes.py
@@ -358,10 +358,9 @@ def _validate_sos(sos):
 
 # Deal with nibabel 2.5 img.get_data() deprecation
 def _get_img_fdata(img):
-    try:
-        return img.get_fdata()
-    except AttributeError:
-        return img.get_data().astype(float)
+    data = np.asanyarray(img.dataobj)
+    dtype = np.complex128 if np.iscomplexobj(data) else np.float64
+    return data.astype(dtype)
 
 
 def _read_volume_info(fobj):
diff --git a/mne/morph.py b/mne/morph.py
--- a/mne/morph.py
+++ b/mne/morph.py
@@ -463,21 +463,33 @@ def _morph_one_vol(self, one):
         assert img_to.ndim == 4 and img_to.shape[-1] == 1
         img_to = img_to[:, :, :, 0]
 
-        # reslice to match morph
-        img_to, img_to_affine = reslice(
-            img_to, self.affine, _get_zooms_orig(self), self.zooms)
-
-        # morph data
-        img_to = self.pre_affine.transform(img_to)
-        if self.sdr_morph is not None:
-            img_to = self.sdr_morph.transform(img_to)
-
-        # subselect the correct cube if src_to is provided
-        if self.src_data['to_vox_map'] is not None:
-            # order=0 (nearest) should be fine since it's just subselecting
-            img_to = SpatialImage(img_to, self.affine)
-            img_to = resample_from_to(img_to, self.src_data['to_vox_map'], 1)
-            img_to = _get_img_fdata(img_to)
+        attrs = ('real', 'imag') if np.iscomplexobj(img_to) else ('real',)
+        img_complex = 0.
+        for attr in attrs:
+            # reslice to match morph
+            img_real, _ = reslice(
+                getattr(img_to, attr), self.affine,
+                _get_zooms_orig(self), self.zooms)
+
+            # morph data
+            img_real = self.pre_affine.transform(img_real)
+            if self.sdr_morph is not None:
+                img_real = self.sdr_morph.transform(img_real)
+
+            # subselect the correct cube if src_to is provided
+            if self.src_data['to_vox_map'] is not None:
+                # order=0 (nearest) should be fine since it's just subselecting
+                img_real = SpatialImage(img_real, self.affine)
+                img_real = resample_from_to(
+                    img_real, self.src_data['to_vox_map'], 1)
+                img_real = _get_img_fdata(img_real)
+
+            # combine real and complex parts
+            if attr == 'real':
+                img_complex = img_complex + img_real
+            else:
+                img_complex = img_complex + 1j * img_real
+        img_to = img_complex
 
         # reshape to nvoxel x nvol:
         # in the MNE definition of volume source spaces,
@@ -800,7 +812,9 @@ def _interpolate_data(stc, morph, mri_resolution, mri_space, output):
 
     n_times = stc.data.shape[1]
     shape = morph.src_data['src_shape'][::-1] + (n_times,)  # SAR->RAST
-    vols = np.zeros((np.prod(shape[:3]), shape[3]), order='F')  # flatten
+    dtype = np.complex128 if np.iscomplexobj(stc.data) else np.float64
+    # order='F' so that F-order flattening is faster
+    vols = np.zeros((np.prod(shape[:3]), shape[3]), dtype=dtype, order='F')
     n_vertices_seen = 0
     for this_inuse in inuse:
         this_inuse = this_inuse.astype(bool)
@@ -818,7 +832,7 @@ def _interpolate_data(stc, morph, mri_resolution, mri_space, output):
         shape = morph.src_data['src_shape_full'][::-1] + (n_times,)
         vols = _csr_dot(
             morph.src_data['interpolator'], vols,
-            np.zeros((np.prod(shape[:3]), shape[3]), order='F'))
+            np.zeros((np.prod(shape[:3]), shape[3]), dtype=dtype, order='F'))
 
     # reshape back to proper shape
     vols = np.reshape(vols, shape, order='F')


Test patch:
diff --git a/mne/tests/test_morph.py b/mne/tests/test_morph.py
--- a/mne/tests/test_morph.py
+++ b/mne/tests/test_morph.py
@@ -174,15 +174,18 @@ def test_xhemi_morph():
 
 
 @testing.requires_testing_data
-@pytest.mark.parametrize('smooth, lower, upper, n_warn', [
-    (None, 0.959, 0.963, 0),
-    (3, 0.968, 0.971, 2),
-    ('nearest', 0.98, 0.99, 0),
+@pytest.mark.parametrize('smooth, lower, upper, n_warn, dtype', [
+    (None, 0.959, 0.963, 0, float),
+    (3, 0.968, 0.971, 2, complex),
+    ('nearest', 0.98, 0.99, 0, float),
 ])
-def test_surface_source_morph_round_trip(smooth, lower, upper, n_warn):
+def test_surface_source_morph_round_trip(smooth, lower, upper, n_warn, dtype):
     """Test round-trip morphing yields similar STCs."""
     kwargs = dict(smooth=smooth, warn=True, subjects_dir=subjects_dir)
     stc = mne.read_source_estimate(fname_smorph)
+    if dtype is complex:
+        stc.data = 1j * stc.data
+        assert_array_equal(stc.data.real, 0.)
     if smooth == 'nearest' and not check_version('scipy', '1.3'):
         with pytest.raises(ValueError, match='required to use nearest'):
             morph = compute_source_morph(stc, 'sample', 'fsaverage', **kwargs)
@@ -206,9 +209,14 @@ def test_surface_source_morph_round_trip(smooth, lower, upper, n_warn):
 def assert_power_preserved(orig, new, limits=(1., 1.05)):
     """Assert that the power is preserved during a round-trip morph."""
     __tracebackhide__ = True
-    power_ratio = np.linalg.norm(orig.data) / np.linalg.norm(new.data)
-    min_, max_ = limits
-    assert min_ < power_ratio < max_, 'Power ratio'
+    for kind in ('real', 'imag'):
+        numer = np.linalg.norm(getattr(orig.data, kind))
+        denom = np.linalg.norm(getattr(new.data, kind))
+        if numer == denom == 0.:  # no data of this type
+            continue
+        power_ratio = numer / denom
+        min_, max_ = limits
+        assert min_ < power_ratio < max_, f'Power ratio {kind}'
 
 
 @requires_h5py
@@ -451,13 +459,13 @@ def test_volume_source_morph(tmpdir):
 @requires_dipy()
 @pytest.mark.slowtest
 @testing.requires_testing_data
-@pytest.mark.parametrize('subject_from, subject_to, lower, upper', [
-    ('sample', 'fsaverage', 8.5, 9),
-    ('fsaverage', 'fsaverage', 7, 7.5),
-    ('sample', 'sample', 6, 7),
+@pytest.mark.parametrize('subject_from, subject_to, lower, upper, dtype', [
+    ('sample', 'fsaverage', 8.5, 9, float),
+    ('fsaverage', 'fsaverage', 7, 7.5, float),
+    ('sample', 'sample', 6, 7, complex),
 ])
 def test_volume_source_morph_round_trip(
-        tmpdir, subject_from, subject_to, lower, upper):
+        tmpdir, subject_from, subject_to, lower, upper, dtype):
     """Test volume source estimate morph round-trips well."""
     import nibabel as nib
     from nibabel.processing import resample_from_to
@@ -492,8 +500,10 @@ def test_volume_source_morph_round_trip(
     morph_to_from = compute_source_morph(
         src=src_to, src_to=src_from, subject_to=subject_from, **kwargs)
     use = np.linspace(0, src_from[0]['nuse'] - 1, 10).round().astype(int)
-    stc_from = VolSourceEstimate(
-        np.eye(src_from[0]['nuse'])[:, use], [src_from[0]['vertno']], 0, 1)
+    data = np.eye(src_from[0]['nuse'])[:, use]
+    if dtype is complex:
+        data = data * 1j
+    stc_from = VolSourceEstimate(data, [src_from[0]['vertno']], 0, 1)
     stc_from_rt = morph_to_from.apply(morph_from_to.apply(stc_from))
     maxs = np.argmax(stc_from_rt.data, axis=0)
     src_rr = src_from[0]['rr'][src_from[0]['vertno']]
@@ -518,7 +528,8 @@ def test_volume_source_morph_round_trip(
     if subject_from == subject_to == 'sample':
         for stc in [stc_from, stc_from_rt]:
             img = stc.as_volume(src_from, mri_resolution=True)
-            img = nib.Nifti1Image(_get_img_fdata(img)[:, :, :, 0], img.affine)
+            img = nib.Nifti1Image(  # abs to convert complex
+                np.abs(_get_img_fdata(img)[:, :, :, 0]), img.affine)
             img = _get_img_fdata(resample_from_to(img, brain, order=1))
             assert img.shape == mask.shape
             in_ = img[mask].astype(bool).mean()