Skip to content

Commit

Permalink
Fix bug with subject_info when loading from and exporting to EDF file (
Browse files Browse the repository at this point in the history
…#11952)

Co-authored-by: Paul ROUJANSKY <[email protected]>
  • Loading branch information
2 people authored and larsoner committed Sep 6, 2023
1 parent a37a787 commit f95a9c5
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 19 deletions.
1 change: 1 addition & 0 deletions doc/changes/1.5.inc
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Bugs
- Fix bug with multi-plot 3D rendering where only one plot was updated (:gh:`11896` by `Eric Larson`_)
- Fix bug with :func:`mne.chpi.compute_head_pos` for CTF data where digitization points were modified in-place, producing an incorrect result during a save-load round-trip (:gh:`11934` by `Eric Larson`_)
- Fix bug with notebooks when using PyVista 0.42 by implementing ``trame`` backend support (:gh:`11956` by `Eric Larson`_)
- Fix bug with ``subject_info`` when loading data from and exporting to EDF file (:gh:`11952` by `Paul Roujansky`_)


.. _changes_1_5_0:
Expand Down
29 changes: 18 additions & 11 deletions mne/export/_edf.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,31 +189,38 @@ def _export_raw(fname, raw, physical_range, add_ch_type):
# set patient info
subj_info = raw.info.get("subject_info")
if subj_info is not None:
birthday = subj_info.get("birthday")

# get the full name of subject if available
first_name = subj_info.get("first_name")
last_name = subj_info.get("last_name")
first_name = first_name or ""
last_name = last_name or ""
joiner = ""
if len(first_name) and len(last_name):
joiner = " "
name = joiner.join([first_name, last_name])
first_name = subj_info.get("first_name", "")
middle_name = subj_info.get("middle_name", "")
last_name = subj_info.get("last_name", "")
name = " ".join(filter(None, [first_name, middle_name, last_name]))

birthday = subj_info.get("birthday")
hand = subj_info.get("hand")
weight = subj_info.get("weight")
height = subj_info.get("height")
sex = subj_info.get("sex")

additional_patient_info = []
for key, value in [("height", height), ("weight", weight), ("hand", hand)]:
if value:
additional_patient_info.append(f"{key}={value}")
if len(additional_patient_info) == 0:
additional_patient_info = None
else:
additional_patient_info = " ".join(additional_patient_info)

if birthday is not None:
if hdl.setPatientBirthDate(birthday[0], birthday[1], birthday[2]) != 0:
raise RuntimeError(
f"Setting patient birth date to {birthday} "
f"returned an error"
)
for key, val in [
("PatientCode", subj_info.get("his_id", "")),
("PatientName", name),
("PatientGender", sex),
("AdditionalPatientInfo", f"hand={hand}"),
("AdditionalPatientInfo", additional_patient_info),
]:
# EDFwriter compares integer encodings of sex and will
# raise a TypeError if value is None as returned by
Expand Down
14 changes: 13 additions & 1 deletion mne/export/tests/test_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,19 @@ def test_double_export_edf(tmp_path):
"bio",
]
info = create_info(len(ch_types), sfreq=1000, ch_types=ch_types)
info = info.set_meas_date("2023-09-04 14:53:09.000")
data = rng.random(size=(len(ch_types), 1000)) * 1e-5

# include subject info and measurement date
info["subject_info"] = dict(
first_name="mne", last_name="python", birthday=(1992, 1, 20), sex=1, hand=3
his_id="12345",
first_name="mne",
last_name="python",
birthday=(1992, 1, 20),
sex=1,
weight=78.3,
height=1.75,
hand=3,
)
raw = RawArray(data, info)

Expand All @@ -163,6 +171,10 @@ def test_double_export_edf(tmp_path):
)
assert_allclose(raw.times, raw_read.times[:orig_raw_len], rtol=0, atol=1e-5)

# check info
for key in set(raw.info) - {"chs"}:
assert raw.info[key] == raw_read.info[key]

# check channel types except for 'bio', which loses its type
orig_ch_types = raw.get_channel_types()
read_ch_types = raw_read.get_channel_types()
Expand Down
55 changes: 54 additions & 1 deletion mne/io/edf/edf.py
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,49 @@ def _get_info(
info["chs"] = chs
info["ch_names"] = ch_names

# Subject information
info["subject_info"] = {}

# String subject identifier
if edf_info["subject_info"].get("id") is not None:
info["subject_info"]["his_id"] = edf_info["subject_info"]["id"]
# Subject sex (0=unknown, 1=male, 2=female)
if edf_info["subject_info"].get("sex") is not None:
if edf_info["subject_info"]["sex"] == "M":
info["subject_info"]["sex"] = 1
elif edf_info["subject_info"]["sex"] == "F":
info["subject_info"]["sex"] = 2
else:
info["subject_info"]["sex"] = 0
# Subject names (first, middle, last).
if edf_info["subject_info"].get("name") is not None:
sub_names = edf_info["subject_info"]["name"].split("_")
if len(sub_names) < 2 or len(sub_names) > 3:
info["subject_info"]["last_name"] = edf_info["subject_info"]["name"]
elif len(sub_names) == 2:
info["subject_info"]["first_name"] = sub_names[0]
info["subject_info"]["last_name"] = sub_names[1]
else:
info["subject_info"]["first_name"] = sub_names[0]
info["subject_info"]["middle_name"] = sub_names[1]
info["subject_info"]["last_name"] = sub_names[2]
# Birthday in (year, month, day) format.
if isinstance(edf_info["subject_info"].get("birthday"), datetime):
info["subject_info"]["birthday"] = (
edf_info["subject_info"]["birthday"].year,
edf_info["subject_info"]["birthday"].month,
edf_info["subject_info"]["birthday"].day,
)
# Handedness (1=right, 2=left, 3=ambidextrous).
if edf_info["subject_info"].get("hand") is not None:
info["subject_info"]["hand"] = int(edf_info["subject_info"]["hand"])
# Height in meters.
if edf_info["subject_info"].get("height") is not None:
info["subject_info"]["height"] = float(edf_info["subject_info"]["height"])
# Weight in kilograms.
if edf_info["subject_info"].get("weight") is not None:
info["subject_info"]["weight"] = float(edf_info["subject_info"]["weight"])

# Filter settings
highpass = edf_info["highpass"]
lowpass = edf_info["lowpass"]
Expand Down Expand Up @@ -766,14 +809,24 @@ def _read_edf_header(fname, exclude, infer_types, include=None):
id_info = id_info.split(" ")
if len(id_info):
patient["id"] = id_info[0]
if len(id_info) == 4:
if len(id_info) >= 4:
try:
birthdate = datetime.strptime(id_info[2], "%d-%b-%Y")
except ValueError:
birthdate = "X"
patient["sex"] = id_info[1]
patient["birthday"] = birthdate
patient["name"] = id_info[3]
if len(id_info) > 4:
for info in id_info[4:]:
if "=" in info:
key, value = info.split("=")
if key in ["weight", "height"]:
patient[key] = float(value)
elif key in ["hand"]:
patient[key] = int(value)
else:
warn(f"Invalid patient information {key}")

# Recording ID
meas_id = {}
Expand Down
43 changes: 39 additions & 4 deletions mne/io/edf/tests/test_edf.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#
# License: BSD-3-Clause

import datetime
from contextlib import nullcontext
from functools import partial
from pathlib import Path
Expand Down Expand Up @@ -117,19 +118,53 @@ def _first_chan_temp(*args, **kwargs):
assert raw.get_channel_types()[0] == "temperature"


@testing.requires_testing_data
def test_subject_info(tmp_path):
"""Test exposure of original channel units."""
raw = read_raw_edf(edf_path)
assert raw.info["subject_info"] is None # XXX this is arguably a bug
raw = read_raw_edf(edf_stim_resamp_path, preload=True)

# check subject_info from `info`
assert raw.info["subject_info"] is not None
want = {
"his_id": "X",
"sex": 1,
"birthday": (1967, 10, 9),
"last_name": "X",
}
for key, val in want.items():
assert raw.info["subject_info"][key] == val, key

# check "subject_info" from `_raw_extras`
edf_info = raw._raw_extras[0]
assert edf_info["subject_info"] is not None
want = {"id": "X", "sex": "X", "birthday": "X", "name": "X"}
want = {
"id": "X",
"sex": "M",
"birthday": datetime.datetime(1967, 10, 9, 0, 0),
"name": "X",
}
for key, val in want.items():
assert edf_info["subject_info"][key] == val, key

# add information
raw.info["subject_info"]["hand"] = 0

# save raw to FIF and load it back
fname = tmp_path / "test_raw.fif"
raw.save(fname)
raw = read_raw_fif(fname)
assert raw.info["subject_info"] is None # XXX should eventually round-trip

# check subject_info from `info`
assert raw.info["subject_info"] is not None
want = {
"his_id": "X",
"sex": 1,
"birthday": (1967, 10, 9),
"last_name": "X",
"hand": 0,
}
for key, val in want.items():
assert raw.info["subject_info"][key] == val


def test_bdf_data():
Expand Down
10 changes: 8 additions & 2 deletions mne/io/edf/tests/test_gdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,14 @@ def test_gdf2_birthday(tmp_path):
assert np.fromfile(fid, np.uint64, 1)[0] == d
raw = read_raw_gdf(new_fname, eog=None, misc=None, preload=True)
assert raw._raw_extras[0]["subject_info"]["age"] == 44
# XXX this is a bug, it should be populated...
assert raw.info["subject_info"] is None
assert raw.info["subject_info"] is not None

birthdate = datetime(1, 1, 1, tzinfo=timezone.utc) + offset_44_yr
assert raw.info["subject_info"]["birthday"] == (
birthdate.year,
birthdate.month,
birthdate.day,
)


@testing.requires_testing_data
Expand Down

0 comments on commit f95a9c5

Please sign in to comment.