Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
11 changes: 10 additions & 1 deletion tutorials/intro/70_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,16 @@

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