Skip to content
Merged
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
35 changes: 21 additions & 14 deletions src/eegprep/functions/popfunc/pop_envtopo.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.miscfunc.misc import finite_matmul
from eegprep.functions.popfunc._chanutils import chanlocs_as_list
from eegprep.functions.popfunc.plot_utils import (
component_channel_indices,
Expand Down Expand Up @@ -63,31 +64,36 @@ def pop_envtopo(
raise ValueError("pop_envtopo requires ICA weights")
icachansind = component_channel_indices(dataset, data.shape[0])
data = data[icachansind, :, :]
weights = icaweights @ icasphere
weights = finite_matmul(icaweights, icasphere)
if weights.shape[1] != data.shape[0]:
raise ValueError("ICA weights do not match EEG.icachansind channel count")
maps = component_maps(dataset)
chanlocs = _component_chanlocs(dataset, maps, icachansind)
components = kwargs.pop("compnums", kwargs.pop("components", None))
max_components = _first_int(kwargs.pop("compsplot", None), default=7)
title = str(kwargs.pop("title", dataset.get("setname") or "Largest ERP components"))
label_times = times if times.size else eeg_times_ms(dataset)
topoplot_options = parse_plot_options_text(kwargs.pop("options", ""))
figure = envtopo(
compnums = kwargs.pop("compnums", None)
if compnums is None: # GUI supplies "components"; the console uses "compnums"
compnums = kwargs.pop("components", None)
result = envtopo(
np.nanmean(data, axis=2),
weights,
times=times if times.size else eeg_times_ms(dataset),
chanlocs=chanlocs,
icawinv=maps,
components=components,
max_components=max_components,
rank_window=kwargs.pop("limcontrib", None),
exclude_components=kwargs.pop("subcomps", None),
timerange=[float(label_times[0]), float(label_times[-1])],
limcontrib=kwargs.pop("limcontrib", None),
compnums=compnums,
compsplot=_first_int(kwargs.pop("compsplot", None), default=7),
subcomps=kwargs.pop("subcomps", 0),
sortvar=str(kwargs.pop("sortvar", topoplot_options.pop("sortvar", "mp"))),
envmode=str(kwargs.pop("envmode", topoplot_options.pop("envmode", "avg"))),
sumenv=str(kwargs.pop("sumenv", topoplot_options.pop("sumenv", "fill"))),
vert=kwargs.pop("vert", None),
title=str(kwargs.pop("title", dataset.get("setname") or "Largest ERP components")),
topoplot_options=topoplot_options,
title=title,
)
command = history_command("pop_envtopo", timerange, **command_kwargs)
show_figures(figure, plot=plot)
return (figure, command) if return_com else figure
show_figures(result.figure, plot=plot)
return (result.figure, command) if return_com else result.figure


def pop_envtopo_dialog_spec(EEG: dict[str, Any]) -> DialogSpec:
Expand Down Expand Up @@ -136,7 +142,8 @@ def _run_gui(EEG: dict[str, Any], *, renderer: Any | None = None) -> dict[str, A
"components": numeric_vector(result.get("components", []), dtype=int).tolist(),
"limcontrib": numeric_vector(result.get("limcontrib", [])).tolist(),
"compsplot": numeric_vector(result.get("compsplot", []), dtype=int).tolist(),
"subcomps": numeric_vector(result.get("subcomps", []), dtype=int).tolist(),
# Blank field -> 0 ("remove none"), matching EEGLAB; [] would mean "remove all but compnums".
"subcomps": numeric_vector(result.get("subcomps", []), dtype=int).tolist() or 0,
"title": str(result.get("title", "") or ""),
"options": str(result.get("options", "") or ""),
},
Expand Down
18 changes: 12 additions & 6 deletions src/eegprep/functions/popfunc/pop_saveset.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,14 +306,20 @@ def _chanlocs_to_struct_array(chanlocs_list):
if not retain:
return np.array([])

dtype = np.dtype([(f, t) for f, t in field_spec if f in retain])
# A numeric field that is empty for some channels (e.g. no-location EOG or
# reference channels) cannot share a homogeneous numeric column, so store it
# as an object column holding MATLAB empty [] for those channels. EEGLAB
# records a missing location as [], not 0; writing 0 would place the channel
# at the head center when the .set is reloaded (here or in EEGLAB).
resolved = [
(f, object if np.issubdtype(t, np.number) and any(d[f] is None for d in d_list) else t)
for f, t in field_spec
if f in retain
]
dtype = np.dtype(resolved)
arr = np.array(
[
tuple(
d[f] if d[f] is not None else (0 if np.issubdtype(t, np.number) else '')
for f, t in field_spec
if f in retain
)
tuple(d[f] if d[f] is not None else (np.array([]) if t is object else '') for f, t in resolved)
for d in d_list
],
dtype=dtype,
Expand Down
40 changes: 40 additions & 0 deletions src/eegprep/functions/sigprocfunc/axcopy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Pop the clicked figure axes out into an enlarged window, as EEGLAB ``axcopy``."""

from __future__ import annotations

from typing import Any, Callable

import matplotlib.pyplot as plt
from matplotlib.axes import Axes
from matplotlib.figure import Figure

from eegprep.functions.popfunc.plot_utils import backend_can_display


def axcopy(figure: Figure, redraws: dict[Axes, Callable[[Axes], Any]]) -> None:
"""Enlarge the left-clicked axes into a pop-up window, like EEGLAB ``axcopy``.

``redraws`` maps each interactive axes to a closure that redraws its content,
full size, into a fresh axes. matplotlib cannot copy artists between figures
the way EEGLAB copies graphic objects, so each axes carries a redraw closure
instead. This is a no-op on non-interactive backends, which deliver no click
events; tests exercise it by dispatching a synthetic click.
"""

def _on_click(event: Any) -> None:
if event.button != 1 or event.inaxes is None:
return
redraw = redraws.get(event.inaxes)
if redraw is None:
return
popup = plt.figure(figsize=(5, 5))
redraw(popup.add_axes([0.13, 0.10, 0.80, 0.80]))
# The GUI/console run with interactive mode off, so display the pop-up
# explicitly (as show_figures does); a no-op on file-output backends.
if backend_can_display():
popup.show()

figure.canvas.mpl_connect("button_press_event", _on_click)


__all__ = ["axcopy"]
Loading
Loading