Repository: mne-tools/mne-python

Issue:
Fixed orientations with mixed cortical and discrete source spaces?
### Describe the new feature or enhancement

Not sure whether to classify this as a bug or feature request, but at the moment, building an MNE inverse operator for a mixed source space consisting of fixed orientation cortical and fixed orientation discrete source spaces is not allowed. I understand the reasoning for disallowing fixed orientation volume source spaces, but why would a discrete source space be a problem here?

### Describe your proposed implementation

The gatekeeping gets done here.

https://github.com/mne-tools/mne-python/blob/bb607d5e767ce2da068833503c5d025aba4b3ea8/mne/minimum_norm/inverse.py#L1504-L1510


### Describe possible alternatives

Possibly check explicitly against volume sources instead, rather than disallowing any mixed sources?

### Additional context

_No response_


Discussion:
I agree in principle this should be okay. I think the check is just not specific enough -- it needs to guard against a volume being there. Surf + discrete should be okay. Are you up for making a PR to fix it @jshanna100 ?
Sure, I could do a PR. 

> it needs to guard against a volume being there.

The following lines seem to do precisely that.

https://github.com/mne-tools/mne-python/blob/bb607d5e767ce2da068833503c5d025aba4b3ea8/mne/minimum_norm/inverse.py#L1511-L1514

So the solution would just be deleting the lines 1504-1510 I cited in the OP?
Sorry, not deleting those lines exactly, but not doing that check.
I would try making the change and seeing which `pytest mne/minimum_norm` tests break. You might need to adjust some, and/or possibly also add some minimal one that would fail on `main` but pass on your PR.

Resolving patch:
diff --git a/mne/minimum_norm/inverse.py b/mne/minimum_norm/inverse.py
--- a/mne/minimum_norm/inverse.py
+++ b/mne/minimum_norm/inverse.py
@@ -1502,16 +1502,11 @@ def _prepare_forward(forward, info, noise_cov, fixed, loose, rank, pca,
     constrained_inverse = any(v < 1. for v in loose.values())
 
     # We only support fixed orientations for surface and discrete source
-    # spaces. Not volume or mixed.
-    if fixed_inverse:
-        if len(loose) > 1:  # Mixed source space
-            raise ValueError('Computing inverse solutions for mixed source '
-                             'spaces with fixed orientations is not '
-                             'supported.')
-        if 'volume' in loose:
-            raise ValueError('Computing inverse solutions for volume source '
-                             'spaces with fixed orientations is not '
-                             'supported.')
+    # spaces, not volume.
+    if fixed_inverse and 'volume' in loose:
+        raise ValueError('Computing inverse solutions for volume source '
+                         'spaces with fixed orientations is not '
+                         'supported.')
     if loose.get('volume', 1) < 1:
         raise ValueError('Computing inverse solutions with restricted '
                          'orientations (loose < 1) is not supported for '


Test patch:
diff --git a/mne/minimum_norm/tests/test_inverse.py b/mne/minimum_norm/tests/test_inverse.py
--- a/mne/minimum_norm/tests/test_inverse.py
+++ b/mne/minimum_norm/tests/test_inverse.py
@@ -1386,3 +1386,24 @@ def _assert_free_ori_match(ori, max_idx, lower_ori, upper_ori):
     dots = np.abs(np.diagonal(ori, axis1=1, axis2=2))
     mu = np.mean(dots)
     assert lower_ori <= mu <= upper_ori, mu
+
+
+@pytest.mark.filterwarnings('ignore:Projection vector.*has been reduced.*:')
+def test_allow_mixed_source_spaces(mixed_fwd_cov_evoked):
+    """Test mixed surf+discrete source spaces w/fixed ori."""
+    fwd, cov, evoked = mixed_fwd_cov_evoked
+    assert fwd['src'].kind == 'mixed'
+    assert len(fwd['src']) == 4  # 2 surf + 2 vol
+    with pytest.raises(ValueError, match='loose param'):  # no fixed with vol
+        inv_op = make_inverse_operator(evoked.info, fwd, cov, loose=0.)
+    for ii, type_ in enumerate(('surf', 'surf', 'vol', 'vol')):
+        assert fwd['src'][ii]['type'] == type_
+        if type_ == 'vol':
+            fwd['src'][ii]['type'] = 'discrete'
+    assert fwd['src'].kind == 'mixed'
+    inv_op = make_inverse_operator(evoked.info, fwd, cov)
+    stc = apply_inverse(evoked, inv_op, lambda2=1. / 9.)  # magnitude
+    assert (stc.data >= 0).all()
+    inv_op = make_inverse_operator(evoked.info, fwd, cov, loose=0.)
+    stc = apply_inverse(evoked, inv_op, lambda2=1. / 9.)  # normal
+    assert (stc.data < 0).any()