Skip to content
Open
1 change: 1 addition & 0 deletions doc/changes/dev/13161.newfeature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:meth:`mne.Report.add_projs` now accepts :class:`~mne.Epochs` as ``info`` and adds an ``add_rate`` parameter to show estimated event rates (e.g. heart rate or blink rate) in the report (:gh:`13161` by `Prithvi Chauhan`_).
62 changes: 60 additions & 2 deletions mne/report/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
from ..viz.utils import _ndarray_to_fig

_BEM_VIEWS = ("axial", "sagittal", "coronal")
_RATE_PROJ_KEYWORDS = ("ecg", "eog", "blink", "heart")


# For raw files, we want to support different suffixes + extensions for all
Expand Down Expand Up @@ -1854,6 +1855,7 @@ def add_projs(
title,
projs=None,
topomap_kwargs=None,
add_rate="auto",
tags=("ssp",),
joint=False,
picks_trace=None,
Expand All @@ -1864,14 +1866,23 @@ def add_projs(

Parameters
----------
info : instance of Info | instance of Evoked | path-like
info : instance of Info | instance of Evoked | instance of Epochs | path-like
An `~mne.Info` structure or the path of a file containing one.
title : str
The title corresponding to the :class:`~mne.Projection` object.
projs : iterable of mne.Projection | path-like | None
The projection vectors to add to the report. Can be the path to a
file that will be loaded via `mne.read_proj`. If ``None``, the
projectors are taken from ``info['projs']``.
add_rate : bool | "auto"
Whether to add an estimated event rate to the figure caption when
``info`` is an instance of :class:`~mne.Epochs`. If ``"auto"``
(default), the rate is shown only when the projector descriptions
suggest ECG or EOG content (i.e. contain ``"ecg"``, ``"eog"``, or
``"blink"``). If ``True``, always show the rate. If ``False``, never
show it.

.. versionadded:: 1.13
%(topomap_kwargs)s
%(tags_report)s
joint : bool
Expand Down Expand Up @@ -1903,6 +1914,7 @@ def add_projs(
section=section,
tags=tags,
topomap_kwargs=topomap_kwargs,
add_rate=add_rate,
replace=replace,
joint=joint,
)
Expand Down Expand Up @@ -3611,6 +3623,30 @@ def _add_raw(
replace=replace,
)

@staticmethod
def _event_estimate(epochs, projs):
rate_caption = None
n_events = len(epochs.drop_log)
event_times = [ev[0] / epochs.info["sfreq"] for ev in epochs.events]
if len(event_times) > 1:
duration_sec = event_times[-1] - event_times[0]
duration_min = duration_sec / 60.0
else:
duration_min = 0.0
return None
if duration_min > 0:
rate = n_events / duration_min
unit = (
"BPM"
if any(
any(kw in p["desc"].lower() for kw in _RATE_PROJ_KEYWORDS)
for p in projs
)
else "events/min"
)
rate_caption = f"Estimated rate: {rate:.1f} {unit}"
return rate_caption

@_use_agg
def _add_projs(
self,
Expand All @@ -3622,16 +3658,20 @@ def _add_projs(
tags,
section,
topomap_kwargs,
add_rate,
replace,
picks_trace=None,
joint=False,
):
evoked = None
epochs = None
if isinstance(info, Info): # no-op
pass
elif isinstance(getattr(info, "info", None), Info): # try to get the file name
if isinstance(info, Evoked):
evoked = info
if isinstance(info, BaseEpochs):
epochs = info
info = info.info
else: # read from a file
info = read_info(info, verbose=False)
Expand All @@ -3645,6 +3685,24 @@ def _add_projs(
elif not isinstance(projs, list):
projs = read_proj(projs)

rate_caption = None

if add_rate is False:
pass
elif epochs is None:
if add_rate is True:
raise ValueError(
"add_rate=True requires an Epochs instance to be passed as info"
)
elif add_rate is True:
rate_caption = self._event_estimate(epochs, projs)
elif add_rate == "auto":
if any(
any(kw in p["desc"].lower() for kw in _RATE_PROJ_KEYWORDS)
for p in projs
):
rate_caption = self._event_estimate(epochs, projs)

if not projs:
raise ValueError("No SSP projectors found")

Expand Down Expand Up @@ -3682,7 +3740,7 @@ def _add_projs(
self._add_figure(
fig=fig,
title=title,
caption=None,
caption=rate_caption,
image_format=image_format,
tags=tags,
section=section,
Expand Down
21 changes: 20 additions & 1 deletion mne/report/tests/test_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

from mne import (
Epochs,
compute_proj_epochs,
create_info,
pick_channels_cov,
read_cov,
Expand All @@ -28,7 +29,7 @@
from mne.epochs import make_metadata
from mne.fixes import _reshape_view
from mne.io import RawArray, read_info, read_raw_fif
from mne.preprocessing import ICA
from mne.preprocessing import ICA, create_ecg_epochs
from mne.report import Report, _ReportScraper, open_report, report
from mne.report import report as report_mod
from mne.report.report import (
Expand Down Expand Up @@ -1019,6 +1020,24 @@ def test_manual_report_2d(tmp_path, invisible_fig):
r.add_projs(
info=raw_fname, projs=ecg_proj_fname, title="my proj", tags=("ssp", "ecg")
)
raw_tmp = read_raw_fif(raw_fname).crop(0, 60).load_data()
ecg_epochs = create_ecg_epochs(raw_tmp)
ecg_projs_tmp = compute_proj_epochs(ecg_epochs, n_grad=1, n_mag=1, n_eeg=0)
r.add_projs(
info=ecg_epochs,
projs=ecg_projs_tmp,
title="ECG projs from epochs",
add_rate="auto",
)
assert "Estimated rate" not in r.html[-1] # auto but no ecg in desc
r.add_projs(
info=ecg_epochs,
projs=ecg_projs_tmp,
title="ECG projs add_rate=True",
add_rate=True,
)
assert "Estimated rate" in r.html[-1]
del raw_tmp, ecg_epochs, ecg_projs_tmp
r.add_ica(ica=ica, title="my ica", inst=None)
with pytest.raises(RuntimeError, match="not preloaded"):
r.add_ica(ica=ica, title="ica", inst=raw_non_preloaded)
Expand Down
12 changes: 10 additions & 2 deletions tutorials/intro/70_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,17 @@
# is read from the `~mne.Info`, but projectors potentially included will be
# ignored; instead, only the explicitly passed projectors will be plotted.

ecg_proj_path = sample_dir / "sample_audvis_ecg-proj.fif"
Comment thread
Master-Zero1 marked this conversation as resolved.
report = mne.Report(title="Projectors example")
report.add_projs(info=raw_path, title="Projs from info")
raw_tmp = mne.io.read_raw(raw_path).crop(0, 60).load_data()
ecg_epochs = mne.preprocessing.create_ecg_epochs(raw_tmp)
ecg_projs = mne.compute_proj_epochs(ecg_epochs, n_grad=1, n_mag=1, n_eeg=0)
report.add_projs(
info=ecg_epochs,
projs=ecg_projs,
title="ECG projs with estimated heart rate",
add_rate="auto",
)
del raw_tmp, ecg_epochs, ecg_projs

# Now a joint plot
events = mne.read_events(sample_dir / "sample_audvis_ecg-eve.fif")
Expand Down
Loading