Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
91c3b12
timefreq: match EEGLAB numerics
innaamogolonova Aug 27, 2026
70dca24
pop_newtimef: honor ERSP/ITC color limits
innaamogolonova Aug 27, 2026
6e6c77f
newtimef: style ERSP/ITC images to match EEGLAB
innaamogolonova Sep 3, 2026
b9e4d57
newtimef: bootstrap significance against the baseline null
innaamogolonova Sep 3, 2026
1414595
newtimef: add ERSP/ITC marginal panels
innaamogolonova Sep 3, 2026
164545d
newtimef: support ITC phase-sign and contour masking
innaamogolonova Sep 3, 2026
20fd72b
newtimef: add scalp-map inset and caption
innaamogolonova Sep 4, 2026
9ff076b
newtimef: refine EEGLAB significance and appearance parity
innaamogolonova Sep 4, 2026
991bf23
newtimef: match EEGLAB marginal value-axis limits and ticks
innaamogolonova Sep 4, 2026
4375017
newtimef: match EEGLAB marginal tick values
innaamogolonova Sep 4, 2026
5dd5802
newtimef: match EEGLAB pcontour outlines and two-sided ITC significance
innaamogolonova Sep 4, 2026
1fa6aff
tests: add newtimef ERSP/ITC/p-value MATLAB parity
innaamogolonova Sep 6, 2026
ec1df3b
docs: document the pop_newtimef time-frequency plot
innaamogolonova Sep 6, 2026
965608e
Merge remote-tracking branch 'origin/develop' into feature/inna-conve…
innaamogolonova Sep 7, 2026
e500d87
newtimef: robust phase-only detection and honest p-value test docstring
innaamogolonova Sep 7, 2026
c590f7d
tests: ground-truth negative-ntimesout timefreq grid against EEGLAB
innaamogolonova Sep 7, 2026
f2b4a6d
newtimef: reject unimplemented boottype instead of silently ignoring it
innaamogolonova Sep 7, 2026
311a917
Merge branch 'develop' into feature/inna-conversion
arnodelorme Sep 7, 2026
6f00f05
newtimef: build ERSP significance null by permuting baseline time lik…
innaamogolonova Sep 8, 2026
037034b
docs: consolidate newtimef changelog entries
innaamogolonova Sep 8, 2026
1524fec
Merge remote-tracking branch 'origin/develop' into feature/inna-conve…
innaamogolonova Sep 8, 2026
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
31 changes: 31 additions & 0 deletions docs/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,37 @@ the `GitHub Releases <https://github.com/sccn/eegprep/releases>`_ page.
Unreleased
==========

- ``pop_newtimef`` / ``newtimef`` now render EEGLAB's single-condition ERSP/ITC time-frequency
figure. The ERSP and ITC images use a symmetric color axis, a stimulus-onset (time 0) marker,
right-hand colorbars titled with the power unit, and the ``turbo`` colormap (EEGPrep's house
colormap, consistent with ``topoplot`` and ``erpimage``); ITC is colored by its phase sign by
default (``plotphasesign``), with ``plotphaseonly`` showing the phase angle in degrees. The
figure adds EEGLAB's marginal panels -- the ERSP min/max envelope and ERP trace below the
images, and the rotated baseline power spectrum and mean ITC to their left -- using EEGLAB's
value-axis limits and two ticks per panel. When channel locations are available it also draws
the channel or component scalp-map inset and caption (the channel label or ``IC n``), marking
the selected electrode for channels. The ``erspmax`` / ``itcmax`` dialog fields set the ERSP
and ITC color limits, and the default frequency range stops at 50 Hz (EEGLAB's ``maxfreq``,
capped at Nyquist).
- ``pop_newtimef`` / ``newtimef`` bootstrap significance now matches EEGLAB. With a significance
level (``alpha``), each time-frequency point is ranked against a per-frequency baseline null
built by permuting the baseline time course (EEGLAB's ``shuffle`` permutation, averaged over
trials), and the p-value folds to a two-sided tail (EEGLAB's ``compute_pvals``) for both ERSP
and ITC -- so the scattered baseline false positives EEGLAB shows are reproduced and genuine
event-related effects survive masking. Non-significant regions are shown as the colormap
midpoint, or outlined with contours instead when ``pcontour`` is set; the FDR (``mcorrect``)
path is supported. A non-default ``boottype`` (``'rand'`` / ``'randall'``) is now rejected with
a clear error rather than silently treated as ``'shuffle'``.
- ``timefreq`` -- and the ``newtimef`` / ``pop_newtimef`` plots built on it -- now match EEGLAB's
decomposition numerics. Requested output frequencies are no longer de-duplicated, so
``freqs``/``nfreqs`` requests that snap several values onto the same FFT bin return one output
frequency per request (as in EEGLAB) -- this changes the ``pac`` output shape (for example
``(5, 3, 8)`` becomes ``(5, 5, 8)``); ``detrend`` is now a no-op on the FFT (``cycles=0``)
path; output time windows are centered with EEGLAB's
``eeg_lat2point`` rounding and the negative-``ntimesout`` subsample grid no longer includes a
spurious trailing time point; ``subitc`` returns the pre-subtraction inter-trial coherence; and
exact-zero spectral bins are guarded as in EEGLAB. Some outputs change shape or value versus
earlier EEGPrep releases.
- Deleting datasets (``pop_delset``, Edit > Delete dataset(s) from memory) now empties
the slot in place like EEGLAB instead of shifting later datasets down, so dataset
numbers in the Datasets menu, ``CURRENTSET``, and the history stay valid. ``eeg_store``
Expand Down
59 changes: 59 additions & 0 deletions docs/source/user_guide/preprocessing_pipeline.rst
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,65 @@ wrappers for the common EEGLAB plotting workflows:
cross, com_crossf = pop_newcrossf(EEG, typeproc=1, num1=1, num2=2, return_com=True)
spectra, com_spectopo = pop_spectopo(EEG, 1, timerange=[], return_com=True)

``pop_newtimef`` plots one channel or component at a time. Set ``typeproc=1`` for
a channel and ``typeproc=0`` for an ICA component; ``num`` is the 1-based channel
or component index. ``tlimits`` is the epoch window in milliseconds and ``cycles``
selects the decomposition: ``[0]`` uses a short-time FFT, while ``[n]`` (optionally
``[n factor]``) uses an ``n``-cycle Morlet wavelet that narrows toward higher
frequencies. Called with no index, ``pop_newtimef`` opens a dialog to collect
these values; the ``Plot`` menu exposes the same dialog under *Channel
time-frequency* and *Component time-frequency*.

Reading the figure
------------------

The figure stacks two images that share a time axis:

- **ERSP** (top) -- event-related spectral perturbation, the trial-averaged power
change from the baseline window, in dB.
- **ITC** (bottom) -- inter-trial coherence, from 0 (phase varies across trials)
to 1 (perfect phase locking).

Each image carries marginal panels: a rotated plot to its left (the baseline
power spectrum beside ERSP; the frequency-averaged coherence beside ITC) and a
horizontal plot below it (the ERSP minimum/maximum envelope; the ERP trace). A
dashed line marks time zero, colorbars sit on the right, and -- when channel
locations are available -- a scalp-map inset in the upper left shows the
channel's head position or the component's interpolated map, captioned with the
channel label or ``IC n``.

Baseline and significance
-------------------------

Pass a ``baseline`` window in milliseconds to normalize power, and an ``alpha``
to test each time-frequency point against a bootstrap null drawn from the
baseline:

.. code-block:: python

result = pop_newtimef(EEG, 1, 1, [-1000, 1000], [3, 0.5],
baseline=[-1000, 0], alpha=0.05)

Non-significant points are drawn at the colormap's green midpoint so the
surviving effects stand out; add ``mcorrect="fdr"`` to control the false
discovery rate across the image. Because this is a permutation test, roughly
``alpha`` of the effect-free baseline points survive by chance -- expected
scatter, not a real effect.

Display options
---------------

- ``pcontour="on"`` keeps every point at its measured value and outlines the
significant regions with a contour instead of masking them.
- The ITC image is colored by the sign of the coherence phase by default; pass
``plotphase="off"`` for magnitude only, or ``plotphaseonly="on"`` to show the
phase angle in degrees.
- ``erspmax`` sets a symmetric ERSP color scale (``[-erspmax, erspmax]``) and
``itcmax`` the ITC image maximum; leave them unset to auto-scale.

Two-condition comparisons (contrasting two datasets in a single figure) are not
yet part of the standalone wrapper.

Legacy ``pop_timef`` and ``pop_crossf`` calls route through the standalone
``pop_newtimef`` and ``pop_newcrossf`` implementations. ERP-image workflows use
``pop_erpimage`` for channel or component images:
Expand Down
49 changes: 48 additions & 1 deletion src/eegprep/functions/popfunc/pop_newtimef.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@

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.plot_utils import (
channel_labels,
component_activations,
component_map_data,
data_time_slice,
history_command,
numeric_vector,
Expand Down Expand Up @@ -59,7 +61,10 @@ def pop_newtimef(
if cycles is None:
cycles = [3, 0.8]
data, times = _selected_signal(EEG, typeproc, num, tlimits)
result = newtimef(data, data.shape[0], [times[0], times[-1]], float(EEG.get("srate", 1) or 1), cycles, **options)
plot_options = {"title": "", **_topo_options(EEG, typeproc, num), **options}
result = newtimef(
data, data.shape[0], [times[0], times[-1]], float(EEG.get("srate", 1) or 1), cycles, **plot_options
)
command = history_command("pop_newtimef", typeproc, _first_index(num), tlimits, cycles, **options)
show_figures(result.figure, plot=plot)
return (result, command) if return_com else result
Expand Down Expand Up @@ -195,6 +200,12 @@ def _run_gui(EEG: dict[str, Any], *, typeproc: int, renderer: Any | None = None)
alpha = numeric_vector(result.get("alpha", []))
if alpha.size:
options["alpha"] = float(alpha[0])
erspmax = numeric_vector(result.get("erspmax", []))
if erspmax.size:
options["erspmax"] = erspmax.tolist() if erspmax.size > 1 else float(erspmax[0])
itcmax = numeric_vector(result.get("itcmax", []))
if itcmax.size:
options["itcmax"] = itcmax.tolist() if itcmax.size > 1 else float(itcmax[0])
if bool(result.get("fdr", False)):
options["mcorrect"] = "fdr"
if bool(result.get("freqscale", False)):
Expand All @@ -205,6 +216,8 @@ def _run_gui(EEG: dict[str, Any], *, typeproc: int, renderer: Any | None = None)
options["plotersp"] = "off"
if not bool(result.get("plotitc", True)):
options["plotitc"] = "off"
if not bool(result.get("plotphase", False)):
options["plotphase"] = "off"
if bool(result.get("plotcurve", False)):
options["plottype"] = "curve"
_add_popup_options(
Expand Down Expand Up @@ -244,6 +257,40 @@ def _selected_signal(EEG: dict[str, Any], typeproc: int, num: Any, tlimits: Any)
return acts[index, :, :], full_times


def _topo_options(EEG: dict[str, Any], typeproc: int, num: Any) -> dict[str, Any]:
"""Scalp-map inset options (topovec/elocs/caption) when channel locations exist.

Mirrors EEGLAB ``pop_newtimef``: a channel passes its 1-based index (marked on a
blank head) and label; a component passes its ``icawinv`` map column and ``IC n``.
"""
chanlocs = chanlocs_as_list(EEG.get("chanlocs", []))
if not chanlocs or not _has_theta(chanlocs[0]):
return {}
index = int(_first_index(num)) - 1
if typeproc == 1:
if not (0 <= index < len(chanlocs)) or not _has_theta(chanlocs[index]):
return {}
labels = channel_labels(EEG)
caption = labels[index] if index < len(labels) else f"Channel {index + 1}"
return {"topovec": index + 1, "elocs": chanlocs, "caption": caption}
if EEG.get("icawinv") is None:
return {}
maps, map_chanlocs = component_map_data(EEG)
if not (0 <= index < maps.shape[1]):
return {}
return {"topovec": maps[:, index], "elocs": map_chanlocs, "caption": f"IC {index + 1}"}


def _has_theta(chanloc: dict[str, Any]) -> bool:
theta = chanloc.get("theta")
if theta is None:
return False
try:
return bool(np.isfinite(float(theta)))
except (TypeError, ValueError):
return False


def _add_popup_options(options: dict[str, Any], ntimesout: int, nfreqs: int, basenorm: int, freqs: list[float]) -> None:
ntimes_values = {1: 50, 2: 100, 3: 150, 4: 200, 5: 300, 6: 400}
if ntimesout in ntimes_values:
Expand Down
Loading
Loading