diff --git a/doc/changes/dev/14248.bugfix.rst b/doc/changes/dev/14248.bugfix.rst new file mode 100644 index 00000000000..742d963823d --- /dev/null +++ b/doc/changes/dev/14248.bugfix.rst @@ -0,0 +1 @@ +Fix bug where :func:`mne.io.anonymize_info` could set an incorrect date in ``info["proc_history"]``, causing an ``OverflowError`` when writing FIF files anonymized with a large ``daysback``, by `Eric Larson`_. diff --git a/mne/_fiff/meas_info.py b/mne/_fiff/meas_info.py index d6742531756..d402503f7f5 100644 --- a/mne/_fiff/meas_info.py +++ b/mne/_fiff/meas_info.py @@ -2964,8 +2964,14 @@ def _check_dates(info, prepend_error=""): It's needed because of the limited integer precision of the fix standard. """ - for key in ("file_id", "meas_id"): - value = info.get(key) + to_check = [(f"[{key!r}]", info.get(key)) for key in ("file_id", "meas_id")] + for pi, record in enumerate(info.get("proc_history") or []): + prefix = f"['proc_history'][{pi}]" + to_check.append((f"{prefix}['block_id']", record.get("block_id"))) + date = record.get("date") + if date is not None: + to_check.append((f"{prefix}['date']", dict(zip(("secs", "usecs"), date)))) + for name, value in to_check: if value is not None: assert "msecs" not in value for key_2 in ("secs", "usecs"): @@ -2974,7 +2980,7 @@ def _check_dates(info, prepend_error=""): or value[key_2] > np.iinfo(">i4").max ): raise RuntimeError( - f"{prepend_error}info[{key}][{key_2}] must be between " + f"{prepend_error}info{name}[{key_2!r}] must be between " f'"{np.iinfo(">i4").min!r}" and "{np.iinfo(">i4").max!r}", got ' f'"{value[key_2]!r}"' ) @@ -3874,10 +3880,16 @@ def anonymize_info(info, daysback=None, keep_his=False, verbose=None): record["date"] = DATE_NONE else: this_t0 = (record["block_id"]["secs"], record["block_id"]["usecs"]) - this_t1 = _add_timedelta_to_stamp(this_t0, -delta_t) + if this_t0 == DATE_NONE: + this_t1 = DATE_NONE + else: + this_t1 = _add_timedelta_to_stamp(this_t0, -delta_t) record["block_id"]["secs"] = this_t1[0] record["block_id"]["usecs"] = this_t1[1] - record["date"] = _add_timedelta_to_stamp(record["date"], -delta_t) + this_date = tuple(record["date"]) + if this_date != DATE_NONE: + this_date = _add_timedelta_to_stamp(this_date, -delta_t) + record["date"] = this_date hi = info.get("helium_info") if hi is not None: diff --git a/mne/_fiff/tests/test_meas_info.py b/mne/_fiff/tests/test_meas_info.py index 108060e2e2f..ae298be92f6 100644 --- a/mne/_fiff/tests/test_meas_info.py +++ b/mne/_fiff/tests/test_meas_info.py @@ -1090,10 +1090,26 @@ def test_anonymize_with_io(tmp_path, daysback): """Test that IO does not break anonymization and all fields.""" raw = read_raw_fif(raw_fname).crop(0, 1) _complete_info(raw.info) + # maxwell_filter writes records whose stamps are the DATE_NONE placeholder; + # shifting those overflows the int32 stamp for large daysback (gh-14236) + raw.info["proc_history"].insert( + 0, + dict( + block_id=_generate_meas_id(), + experimenter="m", + max_info=dict( + max_st=dict(), sss_ctc=dict(), sss_cal=dict(), sss_info=dict(in_order=8) + ), + date=DATE_NONE, + ), + ) temp_path = tmp_path / "tmp_raw.fif" raw.save(temp_path) raw2 = read_raw_fif(temp_path).load_data() raw2.anonymize(daysback=daysback) + record = raw2.info["proc_history"][0] + assert (record["block_id"]["secs"], record["block_id"]["usecs"]) == DATE_NONE + assert record["date"] == DATE_NONE raw2.save(temp_path, overwrite=True) raw3 = read_raw_fif(temp_path) d = object_diff(raw2.info, raw3.info)