Repository: mne-tools/mne-python

Issue:
Warn when trying to compute cHPI locations on maxfiltered data
### Description of the problem

When I run the function **mni.chpo.compute_chpi_locs**:
**There is an error: Something went wrong in the data-driven estimation of the data rank as it exceeds the theoretical rank from the info (303 > 69).**

But there is no parameter to set rank.

```
Computing 4385 HPI location guesses (1 cm grid in a 10.7 cm sphere)
/tmp/ipykernel_56666/2454704541.py:12: RuntimeWarning: Something went wrong in the data-driven estimation of the data rank as it exceeds the theoretical rank from the info (303 > 69). Consider setting rank to "auto" or setting it explicitly as an integer.
  chpi_locs = mne.chpi.compute_chpi_locs(raw.info, chpi_amplitudes, verbose=True)
HPIFIT: 5 coils digitized in order 1 2 3 5 4
HPI consistency of isotrak and hpifit is OK.
```
Then when I continue to run **mni.chpo.compute_head_pos function**:
**I always got 0/5 good HPI fits**.
For the detail:
```
/tmp/ipykernel_56666/2454704541.py:16: RuntimeWarning:     t=48.000:    0/5 good HPI fits, cannot determine the transformation (0.39, 0.27, 0.51, 0.26, 0.29 GOF)!
  head_pos = mne.chpi.compute_head_pos(raw.info, chpi_locs)
/tmp/ipykernel_56666/2454704541.py:16: RuntimeWarning:     t=48.010:    0/5 good HPI fits, cannot determine the transformation (0.36, 0.25, 0.43, 0.28, 0.28 GOF)!
  head_pos = mne.chpi.compute_head_pos(raw.info, chpi_locs)
/tmp/ipykernel_56666/2454704541.py:16: RuntimeWarning:     t=48.020:    0/5 good HPI fits, cannot determine the transformation (0.39, 0.26, 0.40, 0.34, 0.31 GOF)!
  head_pos = mne.chpi.compute_head_pos(raw.info, chpi_locs)
/tmp/ipykernel_56666/2454704541.py:16: RuntimeWarning:     t=48.030:    0/5 good HPI fits, cannot determine the transformation (0.32, 0.28, 0.34, 0.26, 0.38 GOF)!
```




### Steps to reproduce

```Python

# download from google driver
raw = mne.io.read_raw_fif("cut1s_run1_raw_tsss.fif", preload=True, allow_maxshield=True)

# extract the HPI coil amplitudes as a function of time
print("start mni.chpo.compute_chpi_amplitudes function")
chpi_amplitudes = mne.chpi.compute_chpi_amplitudes(raw, verbose=True)

# Compute time-varying HPI coil locations from these:
print("start mni.chpo.compute_chpi_locs function")
chpi_locs = mne.chpi.compute_chpi_locs(raw.info, chpi_amplitudes, verbose=True)

# Compute head positions from the coil locations:
print("start mni.chpo.compute_head_pos function")
head_pos = mne.chpi.compute_head_pos(raw.info, chpi_locs)
# Visualise head positions
head_pos_fig = mne.viz.plot_head_positions(
    head_pos,
    mode="traces",
    destination=raw.info["dev_head_t"],
    info=raw.info,
    show=False,
)
```


### Link to data

https://drive.google.com/file/d/1QKGqTzHPsv9eXmYjl9_uLRBdsZFQ3x5K/view?usp=sharing

### Expected results

I could visualize a continuous head position.

### Actual results

There is no data to visualize when I run mne.viz.plot_head_positions.

IndexError                                Traceback (most recent call last)
Cell In[6], line 18
     16 head_pos = mne.chpi.compute_head_pos(raw.info, chpi_locs)
     17 # Visualise head positions
---> 18 head_pos_fig = mne.viz.plot_head_positions(
     19     head_pos,
     20     mode="traces",
     21     destination=raw.info["dev_head_t"],
     22     info=raw.info,
     23     show=False,
     24 )
     25 head_pos_fig.savefig(megDir + "/01_head_motion/head_pos.tif")
     26 # save files and record

File ~/mne-python/1.5.1_0/lib/python3.11/site-packages/mne/viz/_3d.py:247, in plot_head_positions(pos, mode, cmap, direction, show, destination, info, color, axes)
    245 for ii, (quat, coord) in enumerate(zip(use_quats.T, use_trans.T)):
    246     axes[ii, 0].plot(t, coord, color, lw=1.0, zorder=3)
--> 247     axes[ii, 0].set(ylabel=labels[0][ii], xlim=t[[0, -1]])
    248     axes[ii, 1].plot(t, quat, color, lw=1.0, zorder=3)
    249     axes[ii, 1].set(ylabel=labels[1][ii], xlim=t[[0, -1]])


### Additional information

Platform             Linux-5.15.0-82-generic-x86_64-with-glibc2.31
Python               3.11.5 | packaged by conda-forge | (main, Aug 27 2023, 03:34:09) [GCC 12.3.0]
Executable           /home/brain/mne-python/1.5.1_0/bin/python
CPU                  x86_64 (48 cores)
Memory               125.4 GB

Core
├☑ mne               1.5.1
├☑ numpy             1.24.4 (OpenBLAS 0.3.24 with 48 threads)
├☑ scipy             1.11.2
├☑ matplotlib        3.7.2 (backend=module://matplotlib_inline.backend_inline)
├☑ pooch             1.7.0
└☑ jinja2            3.1.2

Numerical (optional)
├☑ sklearn           1.3.0
├☑ numba             0.57.1
├☑ nibabel           5.1.0
├☑ nilearn           0.10.1
├☑ dipy              1.7.0
├☑ openmeeg          2.5.6
├☑ pandas            2.1.0
└☐ unavailable       cupy

Visualization (optional)
...
├☑ mne-features      0.3
├☑ mne-connectivity  0.5.0
├☑ mne-icalabel      0.4
└☑ mne-bids-pipeline 1.4.0



Discussion:
Hello! 👋 Thanks for opening your first issue here! ❤️ We will try to get back to you soon. 🚴

It looks like you are probably trying to compute cHPI locations on data that have already been maxfiltered which is unlikely to work.

We shouldn't close this issue, though, because we should probably detect this and at least emit an informative warning. There are cases where it could in theory work (I think) but most of the time it's probably a mistake. So maybe we should actually have `on_maxwell_filtered=...` at some point, but I think a warning to start is probably good enough.
Thanks for your quick response @larsoner 

I tried without being max-filtered.
It recommended me to do this.

`RuntimeWarning: This file contains raw Internal Active Shielding data. It may be distorted. Elekta recommends it be run through MaxFilter to produce reliable results. Consider closing the file and running MaxFilter on the data.
`
This is the data I used: https://drive.google.com/file/d/1p6wM_nEygKitwgpJyKkqll_SMozxzEFR/view?usp=sharing

That's true if you plan on analyzing the raw data. But for computing cHPI locations you should indeed process the raw data itself. You can load it with `read_raw_fif(..., allow_maxshield='yes')` to suppress that warning
Yes, I actually run the code:
`raw = mne.io.read_raw_fif(megDir + "cut1s_run1_raw.fif", preload=True, allow_maxshield=True)
`
However, but still, there is nothing to visualize:
```
IndexError                                Traceback (most recent call last)
Cell In[14], line 18
     16 head_pos = mne.chpi.compute_head_pos(raw.info, chpi_locs)
     17 # Visualise head positions
---> 18 head_pos_fig = mne.viz.plot_head_positions(
     19     head_pos,
     20     mode="traces",
     21     destination=raw.info["dev_head_t"],
     22     info=raw.info,
     23     show=False,
     24 )
     25 head_pos_fig.savefig(megDir + "/01_head_motion/head_pos.tif")
     26 # save files and record

File ~/mne-python/1.5.1_0/lib/python3.11/site-packages/mne/viz/_3d.py:247, in plot_head_positions(pos, mode, cmap, direction, show, destination, info, color, axes)
    245 for ii, (quat, coord) in enumerate(zip(use_quats.T, use_trans.T)):
    246     axes[ii, 0].plot(t, coord, color, lw=1.0, zorder=3)
--> 247     axes[ii, 0].set(ylabel=labels[0][ii], xlim=t[[0, -1]])
    248     axes[ii, 1].plot(t, quat, color, lw=1.0, zorder=3)
    249     axes[ii, 1].set(ylabel=labels[1][ii], xlim=t[[0, -1]])

IndexError: index 0 is out of bounds for axis 0 with size 0
```

I think that's what you would get if the cHPI signal was not present in the data. What do you see in `head_pos` and `chpi_locs`?
<img width="993" alt="Screenshot 2023-09-09 at 09 52 09" src="https://github.com/mne-tools/mne-python/assets/33126567/46160f77-a3ba-468d-8dae-f91a49bb6703">

This is the output.
You mean the data lost the cHPI signal. How could I add to the data?

> You mean the data lost the cHPI signal. How could I add to the data?

cHPI needs to be turned on an the time of recording (during acquisition), it cannot be added later. So if you are sure you're working with the data as it came off the scanner, then it looks like there is nothing you can do to get continuous head positions from your data.
Thanks very much for the explanation about this problem. I will come back to you later.
I'm going to reopen this and re-title it, see https://github.com/mne-tools/mne-python/issues/11968#issuecomment-1711046430
I am sorry I closed it. Feel free to reopen it.

Resolving patch:
diff --git a/mne/chpi.py b/mne/chpi.py
--- a/mne/chpi.py
+++ b/mne/chpi.py
@@ -624,6 +624,16 @@ def _setup_hpi_amplitude_fitting(
     on_missing = "raise" if not allow_empty else "ignore"
     hpi_freqs, hpi_pick, hpi_ons = get_chpi_info(info, on_missing=on_missing)
 
+    # check for maxwell filtering
+    for ent in info["proc_history"]:
+        for key in ("sss_info", "max_st"):
+            if len(ent["max_info"]["sss_info"]) > 0:
+                warn(
+                    "Fitting cHPI amplutudes after Maxwell filtering may not to work, "
+                    "consider fitting on the original data"
+                )
+                break
+
     _validate_type(t_window, (str, "numeric"), "t_window")
     if info["line_freq"] is not None:
         line_freqs = np.arange(


Test patch:
diff --git a/mne/tests/test_chpi.py b/mne/tests/test_chpi.py
--- a/mne/tests/test_chpi.py
+++ b/mne/tests/test_chpi.py
@@ -422,6 +422,15 @@ def test_calculate_chpi_positions_artemis():
     )
 
 
+@testing.requires_testing_data
+def test_warn_maxwell_filtered():
+    """Test that trying to compute locations on Maxwell filtered data warns."""
+    raw = read_raw_fif(sss_fif_fname).crop(0, 1)
+    with pytest.warns(RuntimeWarning, match="Maxwell filter"):
+        amps = compute_chpi_amplitudes(raw)
+    assert len(amps["times"]) > 0  # but for this file, it does work!
+
+
 @testing.requires_testing_data
 def test_initial_fit_redo():
     """Test that initial fits can be redone based on moments."""