Skip to content

Commit

Permalink
Add open/save to HDF5 ability to mne.Report (mne-tools#5505)
Browse files Browse the repository at this point in the history
This allows you to save `mne.Report` objects in such a way that they can
be read back later. This in turn allows you to incrementally add figures
to a `Report` object across scripts.

The `mne.open_report` function reads back a previously saved
`mne.Report` object, or creates a new one if it doesn't exist yet. In
this way, it mimics the Python buildin `open` function. You can use it
like this:

    report = mne.open_report('report.h5')
    report.add_figures_to_section(...)
    report.save('report.html')  # Save to either HTML or HDF5

Or with a context manager like this:

    with mne.open_report('report.h5') as report:
    	report.add_figures_to_section(...)
    # report is saved to HDF5 upon exiting the context block
  • Loading branch information
wmvanvliet authored and larsoner committed Sep 14, 2018
1 parent ee69dff commit 9aaa663
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 30 deletions.
10 changes: 6 additions & 4 deletions doc/python_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1047,16 +1047,18 @@ MNE-Report

.. currentmodule:: mne

.. automodule:: mne
:no-members:
:no-inherited-members:

.. autosummary::
:toctree: generated/
:template: class.rst

Report

.. autosummary::
:toctree: generated/
:template: function.rst

open_report


Logging and Configuration
=========================
Expand Down
4 changes: 4 additions & 0 deletions doc/whats_new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ Changelog

- Add `split_naming` parameter to the `Raw.save` method to allow for BIDS-compatible raw file name construction by `Teon Brooks`_

- Add capability to save a :class:`mne.Report` to an HDF5 file to :meth:`mne.Report.save` by `Marijn van Vliet`_

- Add :func:`mne.open_report` to read back a :class:`mne.Report` object that was saved to an HDF5 file by `Marijn van Vliet`_

Bug
~~~

Expand Down
2 changes: 1 addition & 1 deletion mne/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
from .selection import read_selection
from .dipole import read_dipole, Dipole, DipoleFixed, fit_dipole
from .channels import equalize_channels, rename_channels, find_layout
from .report import Report
from .report import Report, open_report

from . import beamformer
from . import channels
Expand Down
163 changes: 140 additions & 23 deletions mne/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import time
from glob import glob
import warnings

import numpy as np

from . import read_evokeds, read_events, pick_types, read_cov
Expand All @@ -32,6 +31,7 @@
from .externals.tempita import HTMLTemplate, Template
from .externals.six import BytesIO
from .externals.six import moves
from .externals.h5io import read_hdf5, write_hdf5

VALID_EXTENSIONS = ['raw.fif', 'raw.fif.gz', 'sss.fif', 'sss.fif.gz',
'-eve.fif', '-eve.fif.gz', '-cov.fif', '-cov.fif.gz',
Expand Down Expand Up @@ -316,6 +316,49 @@ def _update_html(html, report_fname, report_sectionlabel):
return htmls, report_fnames, report_sectionlabels


def open_report(fname, **params):
"""Read a saved report or, if it doesn't exist yet, create a new one.
The returned report can be used as a context manager, in which case any
changes to the report are saved when exiting the context block.
Parameters
----------
fname : str
The file containing the report, stored in the HDF5 format. If the file
does not exist yet, a new report is created that will be saved to the
specified file.
**params : list of parameters
When creating a new report, any named parameters other than ``fname``
are passed to the `__init__` function of the `Report` object. When
reading an existing report, the parameters are checked with the
loaded report and an exception is raised when they don't match.
Returns
-------
report : instance of Report
The report.
"""
if op.exists(fname):
# Check **params with the loaded report
state = read_hdf5(fname, title='mnepython')
for param in params.keys():
if param not in state:
raise ValueError('The loaded report has no attribute %s' %
param)
if params[param] != state[param]:
raise ValueError("Attribute '%s' of loaded report does not "
"match the given parameter." % param)
report = Report()
report.__setstate__(state)
else:
report = Report(**params)
# Keep track of the filename in case the Report object is used as a context
# manager.
report._fname = fname
return report


###############################################################################
# IMAGE FUNCTIONS

Expand Down Expand Up @@ -1354,17 +1397,67 @@ class construction.
warn('`subjects_dir` and `subject` not provided. Cannot '
'render MRI and -trans.fif(.gz) files.')

def _get_state_params(self):
"""Obtain all fields that are in the state dictionary of this object.
Returns
-------
non_opt_params : list of str
All parameters that must be present in the state dictionary.
opt_params : list of str
All parameters that are optionally present in the state dictionary.
"""
# Note: self._fname is not part of the state
return (['baseline', 'cov_fname', 'fnames', 'html', 'include',
'image_format', 'info_fname', 'initial_id', 'raw_psd',
'_sectionlabels', 'sections', '_sectionvars',
'_sort_sections', 'subjects_dir', 'subject', 'title',
'verbose'],
['data_path', '_sort'])

def __getstate__(self):
"""Get the state of the report as a dictionary."""
state = dict()
non_opt_params, opt_params = self._get_state_params()
for param in non_opt_params:
state[param] = getattr(self, param)
for param in opt_params:
if hasattr(self, param):
state[param] = getattr(self, param)
return state

def __setstate__(self, state):
"""Set the state of the report."""
non_opt_params, opt_params = self._get_state_params()
for param in non_opt_params:
setattr(self, param, state[param])
for param in opt_params:
if param in state:
setattr(self, param, state[param])
return state

def save(self, fname=None, open_browser=True, overwrite=False):
"""Save html report and open it in browser.
"""Save the report and optionally open it in browser.
Parameters
----------
fname : str
File name of the report.
fname : str | None
File name of the report. If the file name ends in '.h5' or '.hdf5',
the report is saved in HDF5 format, so it can later be loaded again
with :func:`open_report`. If the file name ends in anything else,
the report is rendered to HTML. If ``None``, the report is saved to
'report.html' in the current working directory.
Defaults to ``None``.
open_browser : bool
Open html browser after saving if True.
When saving to HTML, open the rendered HTML file browser after
saving if True. Defaults to True.
overwrite : bool
If True, overwrite report if it already exists.
If True, overwrite report if it already exists. Defaults to False.
Returns
-------
fname : str
The file name to which the report was saved.
"""
if fname is None:
if not hasattr(self, 'data_path'):
Expand All @@ -1375,14 +1468,6 @@ def save(self, fname=None, open_browser=True, overwrite=False):
else:
fname = op.realpath(fname)

self._render_toc()

with warnings.catch_warnings(record=True):
warnings.simplefilter('ignore')
html = footer_template.substitute(date=time.strftime("%B %d, %Y"),
current_year=time.strftime("%Y"))
self.html.append(html)

if not overwrite and op.isfile(fname):
msg = ('Report already exists at location %s. '
'Overwrite it (y/[n])? '
Expand All @@ -1391,23 +1476,55 @@ def save(self, fname=None, open_browser=True, overwrite=False):
if answer.lower() == 'y':
overwrite = True

_, ext = op.splitext(fname)
is_hdf5 = ext.lower() in ['.h5', '.hdf5']

if overwrite or not op.isfile(fname):
logger.info('Saving report to location %s' % fname)
fobj = codecs.open(fname, 'w', 'utf-8')
fobj.write(_fix_global_ids(u''.join(self.html)))
fobj.close()

# remove header, TOC and footer to allow more saves
self.html.pop(0)
self.html.pop(0)
self.html.pop()

if open_browser:
if is_hdf5:
write_hdf5(fname, self.__getstate__(), overwrite=overwrite,
title='mnepython')
else:
self._render_toc()

# Annotate the HTML with a TOC and footer.
with warnings.catch_warnings(record=True):
warnings.simplefilter('ignore')
html = footer_template.substitute(
date=time.strftime("%B %d, %Y"),
current_year=time.strftime("%Y"))
self.html.append(html)

# Writing to disk may fail. However, we need to make sure that
# the TOC and footer are removed regardless, otherwise they
# will be duplicated when the user attempts to save again.
try:
# Write HTML
with codecs.open(fname, 'w', 'utf-8') as fobj:
fobj.write(_fix_global_ids(u''.join(self.html)))
finally:
self.html.pop(0)
self.html.pop(0)
self.html.pop()

if open_browser and not is_hdf5:
import webbrowser
webbrowser.open_new_tab('file://' + fname)

self.fname = fname
return fname

def __enter__(self):
"""Do nothing when entering the context block."""
return self

def __exit__(self, type, value, traceback):
"""Save the report when leaving the context block."""
if self._fname is not None:
self.save(self._fname, open_browser=False, overwrite=True)
return self

@verbose
def _render_toc(self, verbose=None):
"""Render the Table of Contents."""
Expand Down
34 changes: 32 additions & 2 deletions mne/tests/test_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
from mne import Epochs, read_events, read_evokeds
from mne.io import read_raw_fif
from mne.datasets import testing
from mne.report import Report
from mne.report import Report, open_report
from mne.utils import (_TempDir, requires_mayavi, requires_nibabel,
run_tests_if_main, traits_test)
run_tests_if_main, traits_test, requires_h5py)
from mne.viz import plot_alignment

import matplotlib
Expand Down Expand Up @@ -307,4 +307,34 @@ def test_validate_input():
assert_equal(len(comments_new), len(items))


@requires_h5py
def test_open_report():
"""Test the open_report function."""
tempdir = _TempDir()
hdf5 = op.join(tempdir, 'report.h5')
import matplotlib.pyplot as plt
fig = plt.plot([1, 2], [1, 2])[0].figure

# Test creating a new report through the open_report function
with open_report(hdf5, subjects_dir=subjects_dir) as report:
assert report.subjects_dir == subjects_dir
assert report._fname == hdf5
report.add_figs_to_section(figs=fig, captions=['evoked response'])
# Exiting the context block should have triggered saving to HDF5
assert op.exists(hdf5)

# Load the HDF5 version of the report and check equivalency
report2 = open_report(hdf5)
assert report2._fname == hdf5
assert report2.subjects_dir == report.subjects_dir
assert report2.html == report.html
assert report2.__getstate__() == report.__getstate__()
assert '_fname' not in report2.__getstate__()

# Check parameters when loading a report
pytest.raises(ValueError, open_report, hdf5, foo='bar') # non-existing
pytest.raises(ValueError, open_report, hdf5, subjects_dir='foo')
open_report(hdf5, subjects_dir=subjects_dir) # This should work


run_tests_if_main()

0 comments on commit 9aaa663

Please sign in to comment.