Repository: mne-tools/mne-python

Issue:
Resampling to the identical frequency
### Description of the problem

Resampling of a continuous signal proceeds even if the signal already has the desired new frequency.

### Steps to reproduce

```Python
raw = mne.io.read_raw()
raw.resample(250)
raw.resample(250)
```


### Link to data

_No response_

### Expected results

The resample() function doesn't proceed and doesn't modify the object if it's already has the desired frequency.

### Actual results

Resampling runs again.

### Additional information

```
Platform             Windows-10-10.0.20348-SP0
Python               3.11.3 (tags/v3.11.3:f3909b8, Apr  4 2023, 23:49:59) [MSC v.1934 64 bit (AMD64)]
Executable           C:\Users\Gennadiy\Documents\proj\.venv\Scripts\python.exe
CPU                  Intel64 Family 6 Model 167 Stepping 1, GenuineIntel (16 cores)
Memory               127.9 GB

Core
├☑ mne               1.4.2
├☑ numpy             1.23.5 (OpenBLAS 0.3.20 with 16 threads)
├☑ scipy             1.10.1
├☑ matplotlib        3.7.1 (backend=QtAgg)
├☑ pooch             1.7.0
└☑ jinja2            3.1.2

Numerical (optional)
├☑ sklearn           1.2.2
├☑ numba             0.57.0
├☑ cupy              12.1.0
├☑ pandas            2.0.2
└☐ unavailable       nibabel, nilearn, dipy, openmeeg

Visualization (optional)
├☑ qtpy              2.3.1 (PyQt5=5.15.2)
├☑ ipympl            0.9.3
├☑ pyqtgraph         0.13.3
├☑ mne-qt-browser    0.5.0
└☐ unavailable       pyvista, pyvistaqt, ipyvtklink, vtk

Ecosystem (optional)
└☐ unavailable       mne-bids, mne-nirs, mne-features, mne-connectivity, mne-icalabel, mne-bids-pipeline
```


Discussion:
Seems like we should add a condition to prevent this. Would you like to submit a PR?
Or maybe a short-circuit that just returns `self` immediately if `info['sfreq'] == sfreq`. Seems slightly better to me than raising an error
Yes, but in that case I'd log at least to info.

Resolving patch:
diff --git a/mne/filter.py b/mne/filter.py
--- a/mne/filter.py
+++ b/mne/filter.py
@@ -2435,6 +2435,18 @@ def float_array(c):
     )
 
 
+def _check_resamp_noop(sfreq, o_sfreq, rtol=1e-6):
+    if np.isclose(sfreq, o_sfreq, atol=0, rtol=rtol):
+        logger.info(
+            (
+                f"Sampling frequency of the instance is already {sfreq}, "
+                "returning unmodified."
+            )
+        )
+        return True
+    return False
+
+
 class FilterMixin:
     """Object for Epoch/Evoked filtering."""
 
@@ -2684,10 +2696,12 @@ def resample(
         # mne.io.base.BaseRaw overrides this method
         assert isinstance(self, (BaseEpochs, Evoked))
 
-        _check_preload(self, "inst.resample")
-
         sfreq = float(sfreq)
         o_sfreq = self.info["sfreq"]
+        if _check_resamp_noop(sfreq, o_sfreq):
+            return self
+
+        _check_preload(self, "inst.resample")
         self._data = resample(
             self._data, sfreq, o_sfreq, npad, window=window, n_jobs=n_jobs, pad=pad
         )
diff --git a/mne/io/base.py b/mne/io/base.py
--- a/mne/io/base.py
+++ b/mne/io/base.py
@@ -1316,6 +1316,13 @@ def resample(
         ``self.load_data()``, but this increases memory requirements. The
         resulting raw object will have the data loaded into memory.
         """
+        from ..filter import _check_resamp_noop
+
+        sfreq = float(sfreq)
+        o_sfreq = float(self.info["sfreq"])
+        if _check_resamp_noop(sfreq, o_sfreq):
+            return self
+
         # When no event object is supplied, some basic detection of dropped
         # events is performed to generate a warning. Finding events can fail
         # for a variety of reasons, e.g. if no stim channel is present or it is
@@ -1327,9 +1334,6 @@ def resample(
             except Exception:
                 pass
 
-        sfreq = float(sfreq)
-        o_sfreq = float(self.info["sfreq"])
-
         offsets = np.concatenate(([0], np.cumsum(self._raw_lengths)))
 
         # set up stim channel processing
diff --git a/mne/source_estimate.py b/mne/source_estimate.py
--- a/mne/source_estimate.py
+++ b/mne/source_estimate.py
@@ -849,11 +849,16 @@ def resample(self, sfreq, npad="auto", window="boxcar", n_jobs=None, verbose=Non
 
         Note that the sample rate of the original data is inferred from tstep.
         """
+        from .filter import _check_resamp_noop
+
+        o_sfreq = 1.0 / self.tstep
+        if _check_resamp_noop(sfreq, o_sfreq):
+            return self
+
         # resampling in sensor instead of source space gives a somewhat
         # different result, so we don't allow it
         self._remove_kernel_sens_data_()
 
-        o_sfreq = 1.0 / self.tstep
         data = self.data
         if data.dtype == np.float32:
             data = data.astype(np.float64)


Test patch:
diff --git a/mne/io/tests/test_raw.py b/mne/io/tests/test_raw.py
--- a/mne/io/tests/test_raw.py
+++ b/mne/io/tests/test_raw.py
@@ -972,3 +972,11 @@ def test_get_data_tmin_tmax():
 
     with pytest.raises(TypeError, match="tmax must be .* float"):
         raw.get_data(tmax=[1, 2])
+
+
+def test_resamp_noop():
+    """Tests resampling doesn't affect data if sfreq is identical."""
+    raw = read_raw_fif(raw_fname)
+    data_before = raw.get_data()
+    data_after = raw.resample(sfreq=raw.info["sfreq"]).get_data()
+    assert_array_equal(data_before, data_after)
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
@@ -410,6 +410,14 @@ def test_evoked_resample(tmp_path):
     assert ave_new.info["lowpass"] == 25.0
 
 
+def test_evoked_resamp_noop():
+    """Tests resampling doesn't affect data if sfreq is identical."""
+    ave = read_evokeds(fname, 0)
+    data_before = ave.data
+    data_after = ave.resample(sfreq=ave.info["sfreq"]).data
+    assert_array_equal(data_before, data_after)
+
+
 def test_evoked_filter():
     """Test filtering evoked data."""
     # this is mostly a smoke test as the Epochs and raw tests are more complete
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
@@ -619,6 +619,15 @@ def test_stc_methods():
         assert_array_almost_equal(stc_new.data, stc.data, 5)
 
 
+@testing.requires_testing_data
+def test_stc_resamp_noop():
+    """Tests resampling doesn't affect data if sfreq is identical."""
+    stc = read_source_estimate(fname_stc)
+    data_before = stc.data
+    data_after = stc.resample(sfreq=1.0 / stc.tstep).data
+    assert_array_equal(data_before, data_after)
+
+
 @testing.requires_testing_data
 def test_center_of_mass():
     """Test computing the center of mass on an stc."""