From 586eb7ff68d335f3884a0315c790168de3c3ed15 Mon Sep 17 00:00:00 2001 From: myd7349 Date: Wed, 29 Oct 2025 22:42:02 +0800 Subject: [PATCH 1/5] TST: Add test to reproduce #13467 --- doc/changes/dev/13468.bugfix.rst | 1 + mne/datasets/config.py | 4 ++-- mne/io/nihon/tests/test_nihon.py | 35 ++++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 doc/changes/dev/13468.bugfix.rst diff --git a/doc/changes/dev/13468.bugfix.rst b/doc/changes/dev/13468.bugfix.rst new file mode 100644 index 00000000000..162ee90ff6a --- /dev/null +++ b/doc/changes/dev/13468.bugfix.rst @@ -0,0 +1 @@ +Correctly set the calibration factor in Nihon Kohden reader (which affects channel amplitudes), by `Tom Ma`_. diff --git a/mne/datasets/config.py b/mne/datasets/config.py index 53bea629097..62937779692 100644 --- a/mne/datasets/config.py +++ b/mne/datasets/config.py @@ -87,7 +87,7 @@ # update the checksum in the MNE_DATASETS dict below, and change version # here: ↓↓↓↓↓↓↓↓ RELEASES = dict( - testing="0.166", + testing="0.167", misc="0.27", phantom_kit="0.2", ucl_opm_auditory="0.2", @@ -115,7 +115,7 @@ # Testing and misc are at the top as they're updated most often MNE_DATASETS["testing"] = dict( archive_name=f"{TESTING_VERSIONED}.tar.gz", - hash="md5:273c5919cf74198a39146e9cbc146ce0", + hash="md5:d82318a83b436ca2c7ca8420487c05c2", url=( "https://codeload.github.com/mne-tools/mne-testing-data/" f"tar.gz/{RELEASES['testing']}" diff --git a/mne/io/nihon/tests/test_nihon.py b/mne/io/nihon/tests/test_nihon.py index ba3bc204d2c..b39633aaebf 100644 --- a/mne/io/nihon/tests/test_nihon.py +++ b/mne/io/nihon/tests/test_nihon.py @@ -90,3 +90,38 @@ def return_channel_duplicates(fname): ) with pytest.warns(RuntimeWarning, match=msg): read_raw_nihon(fname) + + +@testing.requires_testing_data +def test_nihon_calibration(): + """Test handling of calibration factor and range in Nihon Kohden EEG files.""" + fname = data_path / "NihonKohden" / "DA00100E.EEG" + raw = read_raw_nihon(fname.as_posix(), preload=True, encoding="cp936") + + Fp1_idx = raw.ch_names.index("Fp1") + M1_idx = raw.ch_names.index("M1") + M2_idx = raw.ch_names.index("M2") + + Fp1_info = raw.info["chs"][Fp1_idx] + M1_info = raw.info["chs"][M1_idx] + M2_info = raw.info["chs"][M2_idx] + + # M1, M2 are EEG channels, just like Fp1. + # So they should have the same calibration factor and physical range. + assert_allclose(M1_info["cal"], Fp1_info["cal"]) + assert_allclose(M2_info["cal"], Fp1_info["cal"]) + assert_allclose(M1_info["range"], Fp1_info["range"]) + assert_allclose(M2_info["range"], Fp1_info["range"]) + + fname_edf = data_path / "NihonKohden" / "DA00100E.EDF" + raw_edf = read_raw_edf(fname_edf, preload=True) + raw_edf.drop_channels(["Events/Markers"]) + # a couple of ch names differ in the EDF + edf_ch_names = {"EEG Mark1": "$M1", "EEG Mark2": "$M2"} + raw_edf.rename_channels(edf_ch_names) + + assert raw.ch_names == raw_edf.ch_names + assert raw._data.shape == raw_edf._data.shape + assert raw.info["sfreq"] == raw_edf.info["sfreq"] + + assert_allclose(raw.get_data(), raw_edf.get_data()) From b0d51c7384d63ce27976e402855824fed91316ae Mon Sep 17 00:00:00 2001 From: myd7349 Date: Wed, 29 Oct 2025 23:59:40 +0800 Subject: [PATCH 2/5] FIX: Correctly set the calibration factor in Nihon Kohden reader If the .21E file does not exist, issue a warning. Co-authored-by: Eric Larson --- mne/io/nihon/nihon.py | 20 ++++++++++++++------ mne/io/nihon/tests/test_nihon.py | 4 ++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/mne/io/nihon/nihon.py b/mne/io/nihon/nihon.py index 91db6a083d2..82bbf274261 100644 --- a/mne/io/nihon/nihon.py +++ b/mne/io/nihon/nihon.py @@ -165,6 +165,11 @@ def _read_21e_file(fname): f"Could not decode 21E file as one of {_encodings}; " f"Default channel names are chosen." ) + else: + warn( + f"Could not find the 21E file containing channel definitions; " + f"Default channel names are chosen." + ) return _chan_labels @@ -280,7 +285,7 @@ def _read_nihon_header(fname): "Cannot read NK file with different sfreq in each datablock" ) - return header + return header, _chan_labels def _read_event_log_block(fid, t_block, version): @@ -380,13 +385,13 @@ def _map_ch_to_type(ch_name): return "eeg" -def _map_ch_to_specs(ch_name): +def _map_ch_to_specs(ch_name, chan_labels_upper): unit_mult = 1e-3 phys_min = -12002.9 phys_max = 12002.56 dig_min = -32768 - if ch_name.upper() in _default_chan_labels: - idx = _default_chan_labels.index(ch_name.upper()) + if ch_name.upper() in chan_labels_upper: + idx = chan_labels_upper.index(ch_name.upper()) if (idx < 42 or idx > 73) and idx not in [76, 77]: unit_mult = 1e-6 phys_min = -3200 @@ -432,7 +437,9 @@ def __init__(self, fname, preload=False, *, encoding="utf-8", verbose=None): data_name = fname.name logger.info(f"Loading {data_name}") - header = _read_nihon_header(fname) + # chan_labels are electrode codes defined in the .21E file. + # It is not the same as header["ch_names"]. + header, chan_labels = _read_nihon_header(fname) metadata = _read_nihon_metadata(fname) # n_chan = len(header['ch_names']) + 1 @@ -447,8 +454,9 @@ def __init__(self, fname, preload=False, *, encoding="utf-8", verbose=None): if "meas_date" in metadata: with info._unlock(): info["meas_date"] = metadata["meas_date"] - chs = {x: _map_ch_to_specs(x) for x in info["ch_names"]} + chan_labels_upper = [x.upper() for x in chan_labels] + chs = {x: _map_ch_to_specs(x, chan_labels_upper) for x in info["ch_names"]} cal = np.array([chs[x]["cal"] for x in info["ch_names"]], float)[:, np.newaxis] offsets = np.array([chs[x]["offset"] for x in info["ch_names"]], float)[ :, np.newaxis diff --git a/mne/io/nihon/tests/test_nihon.py b/mne/io/nihon/tests/test_nihon.py index b39633aaebf..96ad1a0b73b 100644 --- a/mne/io/nihon/tests/test_nihon.py +++ b/mne/io/nihon/tests/test_nihon.py @@ -30,7 +30,7 @@ def test_nihon_eeg(): raw_edf.drop_channels(["Events/Markers"]) assert raw._data.shape == raw_edf._data.shape - assert raw.info["sfreq"] == raw.info["sfreq"] + assert raw.info["sfreq"] == raw_edf.info["sfreq"] # a couple of ch names differ in the EDF edf_ch_names = {"EEG Mark1": "$A2", "EEG Mark2": "$A1"} raw_edf.rename_channels(edf_ch_names) @@ -48,7 +48,7 @@ def test_nihon_eeg(): raw = read_raw_nihon(fname_edf, preload=True) with pytest.raises(ValueError, match="Not a valid Nihon Kohden EEG file"): - raw = _read_nihon_header(fname_edf) + header, _ = _read_nihon_header(fname_edf) bad_fname = data_path / "eximia" / "text_eximia.nxe" From b5acb91f9681709729f596d60167e61346652c9a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 16:02:34 +0000 Subject: [PATCH 3/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mne/io/nihon/nihon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mne/io/nihon/nihon.py b/mne/io/nihon/nihon.py index 82bbf274261..cfeefb4fcc8 100644 --- a/mne/io/nihon/nihon.py +++ b/mne/io/nihon/nihon.py @@ -167,8 +167,8 @@ def _read_21e_file(fname): ) else: warn( - f"Could not find the 21E file containing channel definitions; " - f"Default channel names are chosen." + "Could not find the 21E file containing channel definitions; " + "Default channel names are chosen." ) return _chan_labels From 404577824b46cf3a53a71809e54d541c5fa7f93d Mon Sep 17 00:00:00 2001 From: myd7349 Date: Thu, 30 Oct 2025 00:29:24 +0800 Subject: [PATCH 4/5] Update mne/io/nihon/tests/test_nihon.py Co-authored-by: Eric Larson --- mne/io/nihon/tests/test_nihon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/io/nihon/tests/test_nihon.py b/mne/io/nihon/tests/test_nihon.py index 96ad1a0b73b..28996cfb49a 100644 --- a/mne/io/nihon/tests/test_nihon.py +++ b/mne/io/nihon/tests/test_nihon.py @@ -96,7 +96,7 @@ def return_channel_duplicates(fname): def test_nihon_calibration(): """Test handling of calibration factor and range in Nihon Kohden EEG files.""" fname = data_path / "NihonKohden" / "DA00100E.EEG" - raw = read_raw_nihon(fname.as_posix(), preload=True, encoding="cp936") + raw = read_raw_nihon(fname, preload=True, encoding="cp936") Fp1_idx = raw.ch_names.index("Fp1") M1_idx = raw.ch_names.index("M1") From 8c87abe8817d86a3467749270f805641536d37c1 Mon Sep 17 00:00:00 2001 From: myd7349 Date: Thu, 30 Oct 2025 00:34:53 +0800 Subject: [PATCH 5/5] Update nihon.py --- mne/io/nihon/nihon.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mne/io/nihon/nihon.py b/mne/io/nihon/nihon.py index cfeefb4fcc8..0c474a4b250 100644 --- a/mne/io/nihon/nihon.py +++ b/mne/io/nihon/nihon.py @@ -162,13 +162,13 @@ def _read_21e_file(fname): break else: warn( - f"Could not decode 21E file as one of {_encodings}; " + f"Could not decode {e_fname} as one of {_encodings}; " f"Default channel names are chosen." ) else: warn( - "Could not find the 21E file containing channel definitions; " - "Default channel names are chosen." + f"Could not find {e_fname} containing channel definitions; " + f"Default channel names are chosen." ) return _chan_labels