Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/eegprep/functions/popfunc/pop_rejcont.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from eegprep.functions.guifunc.inputgui import inputgui
from eegprep.functions.guifunc.spec import ControlSpec, DialogSpec
from eegprep.functions.popfunc._chanutils import chanlocs_as_list
from eegprep.functions.popfunc._pop_utils import format_history_value, parse_key_value_args
from eegprep.functions.popfunc._rejection import copy_eeg, one_based_indices, parse_numeric_sequence
from eegprep.functions.popfunc.pop_select import pop_select
Expand Down Expand Up @@ -245,7 +246,7 @@ def _rejcont_winrej(selected: np.ndarray, channel_count: int) -> np.ndarray:


def _selected_chanlocs(EEG: dict[str, Any], selected_rows: np.ndarray) -> list[dict[str, Any]]:
chanlocs = list(EEG.get("chanlocs", []) or [])
chanlocs = chanlocs_as_list(EEG.get("chanlocs"))
return [chanlocs[index] for index in selected_rows if index < len(chanlocs)]


Expand Down
6 changes: 2 additions & 4 deletions src/eegprep/functions/popfunc/pop_rmbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from eegprep.functions.guifunc.inputgui import inputgui
from eegprep.functions.guifunc.spec import CallbackSpec, ControlSpec, DialogSpec
from eegprep.functions.miscfunc.misc import round_mat
from eegprep.functions.popfunc._chanutils import chanlocs_as_list
from eegprep.functions.popfunc._pop_utils import format_history_value, parse_text_tokens
from eegprep.functions.popfunc.eeg_findboundaries import eeg_findboundaries
from eegprep.functions.sigprocfunc.rmbase import rmbase
Expand Down Expand Up @@ -418,10 +419,7 @@ def _channel_field_values(EEG: dict[str, Any], field: str, *, unique: bool = Fal


def _chanlocs(EEG: dict[str, Any]) -> list[dict[str, Any]]:
chanlocs = EEG.get("chanlocs", [])
if isinstance(chanlocs, np.ndarray):
chanlocs = chanlocs.tolist()
return [chan if isinstance(chan, dict) else {} for chan in list(chanlocs or [])]
return [chan if isinstance(chan, dict) else {} for chan in chanlocs_as_list(EEG.get("chanlocs"))]


def _default_baseline_timerange(EEG: dict[str, Any]) -> str:
Expand Down
5 changes: 4 additions & 1 deletion src/eegprep/functions/popfunc/pop_select.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from eegprep.functions.adminfunc.eeg_checkset import eeg_checkset
from eegprep.functions.guifunc.inputgui import inputgui
from eegprep.functions.guifunc.spec import CallbackSpec, ControlSpec, DialogSpec
from eegprep.functions.popfunc._chanutils import chanlocs_as_list
from eegprep.functions.popfunc._pop_utils import (
format_history_value,
parse_key_value_args,
Expand Down Expand Up @@ -578,7 +579,9 @@ def _clip_time_matrix(mat):

def pop_select_dialog_spec(EEG) -> DialogSpec:
"""Return the EEGLAB-like dialog spec for ``pop_select``."""
chanlocs = list(EEG.get("chanlocs", []) or [])
# eeg_checkset stores EEG['chanlocs'] as a numpy object array, so use the
# shared helper instead of `or []`, which raises on ndarray truth checks.
chanlocs = chanlocs_as_list(EEG.get("chanlocs"))
channel_labels = tuple(str(chan.get("labels", "")) for chan in chanlocs if isinstance(chan, dict))
channel_types = tuple(
value
Expand Down
3 changes: 2 additions & 1 deletion src/eegprep/plugins/ICLabel/_prop_numerics.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
numeric_vector,
parse_plot_options_text,
)
from eegprep.functions.popfunc._chanutils import chanlocs_as_list
from eegprep.functions.popfunc._rejection import component_rejection_flags, one_based_indices
from eegprep.functions.sigprocfunc.spectopo import compute_spectra
from eegprep.plugins.dipfit._utils import normalize_model_list
Expand Down Expand Up @@ -263,7 +264,7 @@ def _channel_dashboard_data(
figure_title=f"Channel {label} - pop_prop_extended()",
topography_title=f"Channel {label}",
topography_values=index,
topography_chanlocs=list(EEG.get("chanlocs", []) or []),
topography_chanlocs=chanlocs_as_list(EEG.get("chanlocs")),
activity=activity,
times_ms=times_ms,
activity_title="Channel Time Series",
Expand Down
8 changes: 2 additions & 6 deletions src/eegprep/plugins/clean_rawdata/vis_artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import numpy as np

from eegprep.functions.popfunc._chanutils import chanlocs_as_list
from eegprep.functions.sigprocfunc.eegplot import eegplot
from eegprep.plugins.clean_rawdata.private.masks import mask_to_intervals

Expand Down Expand Up @@ -142,9 +143,4 @@ def _nbchan(eeg: dict[str, Any]) -> int:


def _chanlocs(eeg: dict[str, Any]) -> list[dict[str, Any]]:
chanlocs = eeg.get("chanlocs", [])
if isinstance(chanlocs, np.ndarray):
chanlocs = chanlocs.tolist()
if isinstance(chanlocs, dict):
chanlocs = [chanlocs]
return [chan if isinstance(chan, dict) else {} for chan in list(chanlocs or [])]
return [chan if isinstance(chan, dict) else {} for chan in chanlocs_as_list(eeg.get("chanlocs"))]
6 changes: 2 additions & 4 deletions src/eegprep/plugins/firfilt/_filtering.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from scipy.signal import filtfilt, firls, firwin, lfilter, minimum_phase, remez
from scipy.signal import windows as signal_windows

from eegprep.functions.popfunc._chanutils import chanlocs_as_list
from eegprep.plugins.firfilt.findboundaries import findboundaries
from eegprep.plugins.firfilt.fir_filterdcpadded import fir_filterdcpadded
from eegprep.plugins.firfilt.firws import firws
Expand Down Expand Up @@ -475,7 +476,4 @@ def _has_values(value: Any) -> bool:


def _chanlocs(EEG: dict[str, Any]) -> list[dict[str, Any]]:
chanlocs = EEG.get("chanlocs", [])
if isinstance(chanlocs, np.ndarray):
chanlocs = chanlocs.tolist()
return [chan if isinstance(chan, dict) else {} for chan in list(chanlocs or [])]
return [chan if isinstance(chan, dict) else {} for chan in chanlocs_as_list(EEG.get("chanlocs"))]
6 changes: 2 additions & 4 deletions src/eegprep/plugins/firfilt/_pop_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import numpy as np

from eegprep.functions.guifunc.spec import CallbackSpec, ControlSpec
from eegprep.functions.popfunc._chanutils import chanlocs_as_list
from eegprep.functions.popfunc._pop_utils import format_history_value, parse_key_value_args


Expand Down Expand Up @@ -168,7 +169,4 @@ def _channel_field_values(EEG: dict[str, Any], field: str, *, unique: bool = Fal


def _chanlocs(EEG: dict[str, Any]) -> list[dict[str, Any]]:
chanlocs = EEG.get("chanlocs", [])
if isinstance(chanlocs, np.ndarray):
chanlocs = chanlocs.tolist()
return [chan if isinstance(chan, dict) else {} for chan in list(chanlocs or [])]
return [chan if isinstance(chan, dict) else {} for chan in chanlocs_as_list(EEG.get("chanlocs"))]
1 change: 1 addition & 0 deletions tests/test_gui_pop_clean_rawdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ def run(self, spec, initial_values=None):

def test_vis_artifacts_diagnostics_summarizes_samples_and_channels(self):
old = _eeg()
old["chanlocs"] = np.asarray(old["chanlocs"], dtype=object)
new = dict(
old,
data=old["data"][:, :30],
Expand Down
29 changes: 29 additions & 0 deletions tests/test_gui_pop_firfilt.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ def test_pop_eegfiltnew_dialog_matches_eeglab_sections(self):
self.assertIn(("text", "Channel type(s)", None), labels)
self.assertIn(("text", "OR channel labels or indices", None), labels)

def test_pop_eegfiltnew_dialog_accepts_numpy_chanlocs(self):
eeg = _eeg()
eeg["chanlocs"] = np.asarray(eeg["chanlocs"], dtype=object)

controls = controls_by_tag(pop_eegfiltnew_dialog_spec(eeg))

self.assertEqual(controls["chantype_button"].callback.params["channels"], ["EEG", "EOG"])
self.assertEqual(controls["channels_button"].callback.params["channels"], ["Cz", "Pz", "EOG"])

def test_pop_eegfiltnew_gui_result_filters_and_returns_history(self):
class Renderer:
def run(self, spec, initial_values=None):
Expand Down Expand Up @@ -100,6 +109,26 @@ def run(self, spec, initial_values=None):
np.testing.assert_allclose(out["data"][2], before[2])
self.assertIn("'channels', {'Cz' 'Pz'}", command)

def test_pop_eegfiltnew_accepts_numpy_chanlocs_for_channel_type_filtering(self):
eeg = _eeg()
eeg["chanlocs"] = np.asarray(eeg["chanlocs"], dtype=object)
before = eeg["data"].copy()

out, command = pop_eegfiltnew(
eeg,
"hicutoff",
30,
"filtorder",
80,
"chantype",
["EOG"],
return_com=True,
)

np.testing.assert_allclose(out["data"][:2], before[:2])
self.assertFalse(np.allclose(out["data"][2], before[2]))
self.assertIn("'chantype', {'EOG'}", command)

def test_legacy_pop_eegfilt_dialog_and_gui_result(self):
spec = pop_eegfilt_dialog_spec(_eeg())

Expand Down
11 changes: 11 additions & 0 deletions tests/test_gui_pop_select.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,17 @@ def test_gui_channel_picker_exposes_labels_and_types(self):
self.assertEqual(controls["chans_button"].callback.params["channels"], ("Fz", "Cz", "HEOG", "VEOG"))
self.assertEqual(controls["chantype_button"].callback.params["channels"], ("EEG", "EOG"))

def test_gui_dialog_spec_accepts_numpy_chanlocs(self):
# eeg_checkset normalises EEG['chanlocs'] to a numpy array of dicts;
# the dialog builder must accept that storage form (regression for #229).
eeg = _eeg()
eeg["chanlocs"] = np.asarray(eeg["chanlocs"], dtype=object)

controls = controls_by_tag(pop_select_dialog_spec(eeg))

self.assertEqual(controls["chans_button"].callback.params["channels"], ("Fz", "Cz", "HEOG", "VEOG"))
self.assertEqual(controls["chantype_button"].callback.params["channels"], ("EEG", "EOG"))

def test_gui_result_runs_selection_and_history(self):
class Renderer:
def run(self, spec, initial_values=None):
Expand Down
10 changes: 10 additions & 0 deletions tests/test_pop_prop_extended.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ def test_classifier_data_parsing_defaults_to_iclabel_and_standard_classes() -> N
assert classifier_names(eeg) == ["Other", "ICLabel"]


def test_channel_property_data_accepts_numpy_chanlocs() -> None:
eeg = _iclabel_eeg()
eeg["chanlocs"] = np.asarray(eeg["chanlocs"], dtype=object)

dashboard = build_extended_property_data(eeg, 1, 1)

assert len(dashboard.topography_chanlocs) == 4
assert dashboard.topography_chanlocs[0]["labels"] == "Ch1"


def test_classifier_name_from_gui_matches_string_values_case_insensitively() -> None:
eeg = _iclabel_eeg()

Expand Down
17 changes: 17 additions & 0 deletions tests/test_pop_rmbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,23 @@ def test_pop_rmbase_dialog_disables_channel_controls_for_multiple_datasets():
assert controls["channels_button"].enabled is False


def test_pop_rmbase_dialog_accepts_numpy_chanlocs():
eeg = create_test_eeg(n_channels=2, n_samples=50, srate=50.0, n_trials=2)
eeg["chanlocs"] = np.asarray(
[
{"labels": "Cz", "type": "EEG"},
{"labels": "EOG", "type": "EOG"},
],
dtype=object,
)

spec = pop_rmbase_dialog_spec(eeg)
controls = {control.tag: control for control in spec.controls if control.tag}

assert controls["chantypes_button"].callback.params["channels"] == ["EEG", "EOG"]
assert controls["channels_button"].callback.params["channels"] == ["Cz", "EOG"]


def test_pop_rmbase_sample_data_zeroes_selected_baseline_channels_without_warnings():
eeg = pop_loadset(SAMPLE_DATASET_PATH)

Expand Down
2 changes: 2 additions & 0 deletions tests/test_rejection_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,12 +308,14 @@ def fake_eegplot(_data, *args, **kwargs):
def test_pop_rejcont_display_accept_removes_continuous_regions(monkeypatch):
accepted = []
eeg = create_test_eeg(n_channels=2, n_samples=120, n_trials=1, srate=100)
eeg["chanlocs"] = np.asarray(eeg["chanlocs"], dtype=object)
time = np.arange(120) / 100
eeg["data"][0] = 100 * np.sin(2 * np.pi * 30 * time)

def fake_eegplot(data, *args, **kwargs):
del args
assert np.asarray(data).shape[0] == 1
assert kwargs["eloc_file"][0]["labels"] == "Ch1"
kwargs["command_callback"](kwargs["winrej"])
return "window"

Expand Down
Loading