Skip to content

Commit 6e07bbb

Browse files
committed
Merge remote-tracking branch 'upstream/main' into hoechenberger/issue10533
* upstream/main: MAINT: Extra test for coreg (mne-tools#10549) BUG: Fix annot meas_date / crop (mne-tools#10491) Update latest.inc
2 parents c117114 + ac45cbe commit 6e07bbb

File tree

7 files changed

+101
-19
lines changed

7 files changed

+101
-19
lines changed

doc/changes/latest.inc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ Enhancements
2727

2828
- The ``pick_channels`` method gained a ``verbose`` parameter, allowing e.g. to suppress messages about removed projectors (:gh:`10544` by `Richard Höchenberger`_)
2929

30+
- The :func:`mne.make_forward_dipole` function can now take a list of dipoles to make a multi-dipole forward models (:gh:`10464` by `Marijn van Vliet`_)
31+
32+
- Add example of Xfit-style ECD modeling using multiple dipoles (:gh:`10464` by `Marijn van Vliet`_)
33+
34+
- It is now possible to compute inverse solutions with restricted source orientations using discrete forward models (:gh:`10464` by `Marijn van Vliet`_)
35+
3036
- The new function :func:`mne.preprocessing.maxwell_filter_prepare_emptyroom` simplifies the preconditioning of an empty-room recording for our Maxwell filtering operations (:gh:`10533` by `Richard Höchenberger`_)
3137

3238
Bugs
@@ -39,6 +45,8 @@ Bugs
3945

4046
- Fix bug where ``theme`` was not handled properly in :meth:`mne.io.Raw.plot` (:gh:`10487`, :gh:`10500` by `Mathieu Scheltienne`_ and `Eric Larson`_)
4147

48+
- Fix bug in :meth:`raw.crop(start, stop) <mne.io.Raw.crop>` that would cause annotations to be erroneously shifted when ``start != 0`` and no measurement date was set. (:gh:`10491` by `Eric Larson`_)
49+
4250
- Rendering issues with recent MESA releases can be avoided by setting the new environment variable``MNE_3D_OPTION_MULTI_SAMPLES=1`` or using :func:`mne.viz.set_3d_options` (:gh:`10513` by `Eric Larson`_)
4351

4452
- Fix behavior for the ``pyvista`` 3D renderer's ``quiver3D`` function so that default arguments plot a glyph in ``arrow`` mode (:gh:`10493` by `Alex Rockhill`_)

mne/annotations.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ class Annotations(object):
111111
starting time of annotation acquisition. If None (default),
112112
starting time is determined from beginning of raw data acquisition.
113113
In general, ``raw.info['meas_date']`` (or None) can be used for syncing
114-
the annotations with raw data if their acquisiton is started at the
114+
the annotations with raw data if their acquisition is started at the
115115
same time. If it is a string, it should conform to the ISO8601 format.
116116
More precisely to this '%%Y-%%m-%%d %%H:%%M:%%S.%%f' particular case of
117117
the ISO8601 format where the delimiter between date and time is ' '.
@@ -231,6 +231,14 @@ class Annotations(object):
231231
e +------+
232232
orig_time onset[0]'
233233
234+
.. warning::
235+
This means that when ``raw.info['meas_date'] is None``, doing
236+
``raw.set_annotations(raw.annotations)`` will not alter ``raw`` if and
237+
only if ``raw.first_samp == 0``. When it's non-zero,
238+
``raw.set_annotations`` will assume that the "new" annotations refer to
239+
the original data (with ``first_samp==0``), and will be re-referenced to
240+
the new time offset!
241+
234242
**Specific annotation**
235243
236244
``BAD_ACQ_SKIP`` annotation leads to specific reading/writing file
@@ -1187,7 +1195,7 @@ def _read_brainstorm_annotations(fname, orig_time=None):
11871195
starting time of annotation acquisition. If None (default),
11881196
starting time is determined from beginning of raw data acquisition.
11891197
In general, ``raw.info['meas_date']`` (or None) can be used for syncing
1190-
the annotations with raw data if their acquisiton is started at the
1198+
the annotations with raw data if their acquisition is started at the
11911199
same time.
11921200
11931201
Returns

mne/io/base.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1353,9 +1353,15 @@ def crop(self, tmin=0.0, tmax=None, include_tmax=True, *, verbose=None):
13531353
self._data = self._data[:, smin:smax + 1].copy()
13541354

13551355
annotations = self.annotations
1356-
if annotations.orig_time is None:
1357-
annotations.onset -= tmin
13581356
# now call setter to filter out annotations outside of interval
1357+
if annotations.orig_time is None:
1358+
assert self.info['meas_date'] is None
1359+
# When self.info['meas_date'] is None (which is guaranteed if
1360+
# self.annotations.orig_time is None), when we do the
1361+
# self.set_annotations, it's assumed that the annotations onset
1362+
# are relative to first_time, so we have to subtract it, then
1363+
# set_annotations will put it back.
1364+
annotations.onset -= self.first_time
13591365
self.set_annotations(annotations, False)
13601366

13611367
return self

mne/io/ctf/info.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def _convert_time(date_str, time_str):
9898
raise RuntimeError('Illegal time: %s' % time_str)
9999
# MNE-C uses mktime which uses local time, but here we instead decouple
100100
# conversion location from the process, and instead assume that the
101-
# acquisiton was in GMT. This will be wrong for most sites, but at least
101+
# acquisition was in GMT. This will be wrong for most sites, but at least
102102
# the value we obtain here won't depend on the geographical location
103103
# that the file was converted.
104104
res = timegm((date.tm_year, date.tm_mon, date.tm_mday,

mne/io/meas_info.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -723,7 +723,7 @@ class Info(dict, MontageMixin, ContainsMixin):
723723
smartshield : dict
724724
MaxShield information. This dictionary is (always?) empty,
725725
but its presence implies that MaxShield was used during
726-
acquisiton.
726+
acquisition.
727727
728728
* ``subject_info`` dict:
729729

mne/tests/test_annotations.py

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1440,22 +1440,34 @@ def test_annotation_duration_setting():
14401440

14411441

14421442
@pytest.mark.parametrize('meas_date', (None, 1))
1443-
@pytest.mark.parametrize('first_samp', (0, 100))
1444-
def test_annot_noop(meas_date, first_samp):
1443+
@pytest.mark.parametrize('set_meas_date', ('before', 'after'))
1444+
@pytest.mark.parametrize('first_samp', (0, 100, 3000))
1445+
def test_annot_noop(meas_date, first_samp, set_meas_date):
14451446
"""Show some unintuitive behavior of annotations."""
14461447
sfreq = 1000.
1447-
raw = RawArray(np.zeros((1, 2000)), create_info(1, sfreq, 'eeg'),
1448-
first_samp=first_samp)
1449-
annot = Annotations(0.5, 0.1, 'bad')
1450-
raw.set_annotations(annot)
1451-
raw.set_meas_date(meas_date)
1448+
info = create_info(1, sfreq, 'eeg')
1449+
onset = 0.5
1450+
annot_kwargs = dict()
1451+
if set_meas_date == 'before':
1452+
with info._unlock():
1453+
info['meas_date'] = _handle_meas_date(meas_date)
1454+
if meas_date is not None:
1455+
onset += first_samp / sfreq
1456+
annot_kwargs['orig_time'] = meas_date
1457+
raw = RawArray(np.zeros((1, 2000)), info, first_samp=first_samp)
1458+
annot = Annotations(onset, 0.1, 'bad', **annot_kwargs)
1459+
raw.set_annotations(annot, verbose='debug')
1460+
if set_meas_date == 'after':
1461+
raw.set_meas_date(meas_date)
14521462
first_annot = raw.annotations
1453-
raw.set_annotations(first_annot) # should be a no-op...
1463+
if meas_date is None:
1464+
first_annot.onset -= raw.first_time
1465+
raw.set_annotations(first_annot, verbose='debug') # should be a no-op...
14541466
second_annot = raw.annotations
14551467
want = first_annot.onset[0]
14561468
# it has been shifted when meas_date is None!
14571469
if meas_date is None:
1458-
want = want + first_samp / sfreq
1470+
want = want + raw.first_time
14591471
assert_allclose(second_annot.onset[0], want)
14601472

14611473

@@ -1532,9 +1544,8 @@ def _create_raw(eeg, sfreq, onset, description, meas_date, first_samp,
15321544
end_idx = np.where(raw.annotations.description == 'off')[0]
15331545
tmins = raw.annotations.onset[start_idx]
15341546
tmaxs = raw.annotations.onset[end_idx]
1535-
if meas_date_1 is not None: # need to reference to what will be 0 time
1536-
tmins -= raw.first_time
1537-
tmaxs -= raw.first_time
1547+
tmins -= raw.first_time
1548+
tmaxs -= raw.first_time
15381549
assert len(tmins) == len(tmaxs) == 2
15391550
assert raw.info['meas_date'] == meas_date_1
15401551
_assert_annotations_equal(raw.annotations, want_annot)
@@ -1571,3 +1582,41 @@ def _create_raw(eeg, sfreq, onset, description, meas_date, first_samp,
15711582
assert sess.first_samp == want_first_samp
15721583
assert sess.annotations.orig_time == meas_date_1
15731584
assert list(sess.annotations.description) == descs
1585+
1586+
1587+
@pytest.mark.parametrize('first_samp', (0, 10000))
1588+
@pytest.mark.parametrize('meas_date', (None, 24 * 60 * 60))
1589+
def test_annot_meas_date_first_samp_crop(meas_date, first_samp):
1590+
"""Test yet another meas_date / first_samp issue."""
1591+
sfreq = 1000.
1592+
info = mne.create_info(1, sfreq, 'eeg')
1593+
raw = mne.io.RawArray(
1594+
np.random.RandomState(0).randn(1, 3000), info, first_samp=first_samp)
1595+
raw.set_meas_date(meas_date)
1596+
onset = np.array([0, 1, 2], float)
1597+
if meas_date is not None:
1598+
onset += first_samp / sfreq
1599+
annot = mne.Annotations(
1600+
onset=onset,
1601+
duration=[0.1, 0.2, 0.3],
1602+
description=["a", "b", "c"],
1603+
orig_time=raw.info['meas_date'])
1604+
assert len(annot) == 3
1605+
raw.set_annotations(annot)
1606+
assert len(raw.annotations) == 3
1607+
raw_crop = raw.copy().crop(0, 1.5, verbose='debug')
1608+
assert len(raw_crop.annotations) == 2
1609+
assert_array_equal(raw_crop.annotations.description, annot.description[:2])
1610+
assert_array_equal(raw_crop.annotations.duration, annot.duration[:2])
1611+
# these two should be the equivalent
1612+
raw_crop = raw.copy().crop(2, 2.5, verbose='debug')
1613+
raw_crop_2 = raw.copy().crop(1, None).crop(1, 1.5)
1614+
assert_allclose(raw_crop.get_data(), raw_crop_2.get_data())
1615+
assert raw_crop.first_samp == raw_crop_2.first_samp
1616+
want_onset = onset[2:]
1617+
if meas_date is None:
1618+
want_onset = want_onset + raw.first_time
1619+
for this_raw in (raw_crop, raw_crop_2):
1620+
assert len(this_raw.annotations) == 1
1621+
assert_allclose(this_raw.annotations.onset, want_onset)
1622+
assert_allclose(this_raw.annotations.duration, annot.duration[2:])

mne/tests/test_coreg.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from mne.datasets import testing
1414
from mne.transforms import (Transform, apply_trans, rotation, translation,
1515
scaling, read_trans, _angle_between_quats,
16-
rot_to_quat)
16+
rot_to_quat, invert_transform)
1717
from mne.coreg import (fit_matched_points, create_default_subject, scale_mri,
1818
_is_mri_subject, scale_labels, scale_source_space,
1919
coregister_fiducials, get_mni_fiducials, Coregistration)
@@ -235,6 +235,17 @@ def test_scale_mri_xfm(tmp_path, few_surfaces):
235235
mni = mne.vertex_to_mni(vertices, hemis, subject_to,
236236
subjects_dir=tempdir)
237237
assert_allclose(mni, mni_from, atol=1e-3) # 0.001 mm
238+
# Check head_to_mni (the `trans` here does not really matter)
239+
trans = rotation(0.001, 0.002, 0.003) @ translation(0.01, 0.02, 0.03)
240+
trans = Transform('head', 'mri', trans)
241+
pos_head_from = np.random.RandomState(0).randn(4, 3)
242+
pos_mni_from = mne.head_to_mni(
243+
pos_head_from, subject_from, trans, tempdir)
244+
pos_mri_from = apply_trans(trans, pos_head_from)
245+
pos_mri = pos_mri_from * scale
246+
pos_head = apply_trans(invert_transform(trans), pos_mri)
247+
pos_mni = mne.head_to_mni(pos_head, subject_to, trans, tempdir)
248+
assert_allclose(pos_mni, pos_mni_from, atol=1e-3)
238249

239250

240251
def test_fit_matched_points():

0 commit comments

Comments
 (0)