Skip to content

Evoked measure set for auditory paradigms: ERP, induced power, debiased ITC - #1

Merged
alexedmon1 merged 5 commits into
mainfrom
feat/evoked-surface
Aug 26, 2026
Merged

Evoked measure set for auditory paradigms: ERP, induced power, debiased ITC#1
alexedmon1 merged 5 commits into
mainfrom
feat/evoked-surface

Conversation

@alexedmon1

Copy link
Copy Markdown
Owner

5 commits, 9 files, +3,118/−40. 163 tests pass.

What this is for

Ports the 22-column evoked measure set used for the auditory paradigms (40 Hz
ASSR, 80 Hz ASSR, chirp), so roi_evoked, vertex_evoked and
electrode_evoked all speak the same measure vocabulary and a source arm can be
compared against the sensor array on identical definitions.

The measure set is ported verbatim from its MATLAB lineage, with defects
flagged in place rather than repaired
— a deliberate decision, so that results
stay comparable to the prior work while the known problems are visible. Two are
documented at their definitions: a chirp offset window that falls outside the
epoch and therefore yields NaN, and gamma band edges that differ between the
source scripts.

What's in it

  • ERP measures, induced power, cycle ramps and tiled extraction (30c172d)
  • Wired into all three evoked modules (5c3e884) so roi / vertex /
    electrode share definitions
  • Debiased ITC, exposed as a measure type (f7251b9) — ITC is biased upward
    at low trial counts, which matters when paradigms differ in trials retained
  • Electrode arm gains ERP and induced power (d4a9126), which is what makes
    it a fair comparator rather than a spectral-only one

Defects fixed

d0c7909 fixes two that travelled together and were found the hard way:

  1. roi_evoked called self._needs_induced(...), a method that was never
    written
    . Every subject of a 26-subject chirp run failed on AttributeError.
  2. That run still exited 0. Per-subject exceptions are caught and logged so
    one bad recording cannot sink a run, but with every subject failing,
    aggregate wrote nothing, logged one warning, and the CLI printed "Done".

The second is the more serious of the two. A run where every subject fails now
raises, on both the serial and parallel paths; partial failure is still
tolerated. Both carry regression tests.

Usage note

These measures are consumed by a sibling method-development workspace that
compares source spaces on auditory evoked responses. Nothing here depends on
that workspace, but the measure definitions are what make its arms comparable,
so changing a definition silently invalidates cross-arm results.

🤖 Generated with Claude Code

alexedmon1 and others added 5 commits August 18, 2026 14:30
Four pieces of the evoked work, all measurement primitives with tests
rather than wiring, so the modules can adopt them next.

There was no ERP amplitude or latency measure anywhere — not in this
package and not in the lab's EEGLAB scripts. Everything on both sides was
ITC, ERSP or trial-averaged power, all of which discard sign, which is
presumably why the absence went unnoticed. Sign is worth having now: under
a fixed-orientation inverse a source's time course is its projection onto
the cortical normal, so an N1 that inverts between regions means something.
The measure takes a polarity argument because a named component is a signed
extremum, not the largest excursion.

n_cycles now accepts [lo, hi] as a linear ramp alongside a scalar and
"adaptive". A fixed 7 cycles cannot resolve the 92-308 ms onset window at
low frequencies because the wavelet is longer than the window. The ramp
mirrors the lab's EEGLAB cycles [1 30]; EEGLAB's second element is a growth
factor rather than an endpoint, so this is an interpretation and is
documented as one. D24 already settled that we match conclusions, not
values.

Measures can now span a union of frequency-by-time tiles. A rectangle
cannot express a response that sweeps, so a chirp measured as one box mixes
response with background; the test shows the diagonal recovering 1.0 where
the enclosing rectangle reads under 0.35.

Induced power gets its own function, with the caveat in the docstring that
subtracting an average estimated from the same trials removes a 1/n share
of the induced power too.

Also fixes the R script that has never been able to read its own input:
roi_evoked_analysis.R looked for evoked_measures.csv while Python has
written roi_evoked_measures.csv since the roi_ rename, which is why
autifony's roi_evoked results directories are empty. Six references
corrected. No autifony re-run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
roi, electrode and vertex now share one config contract: n_cycles accepts a
[lo, hi] ramp, a measure can span a union of tiles instead of one
rectangle, and unknown types still warn and skip rather than failing.

roi_evoked additionally gains the two measures that need the trials rather
than the TF map: ERP amplitude and latency off the averaged waveform, and
induced power from trials with the phase-locked average removed. Induced is
computed only when a measure asks for it, since it doubles the TFR cost.

Existing configs are unaffected — a scalar n_cycles and a band plus
time_window behave exactly as before, so autifony's current measure set
produces the same numbers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ITC is biased upward at low trial counts: pure noise gives roughly
1/sqrt(n). Measured here, noise-only ITC runs 0.199 at 20 trials against
0.044 at 400, so subjects with different trial counts are not on the same
scale and a group difference can be a trial-count difference. Autifony's
counts vary by subject and paradigm, and Sentinel is repeated-measures
across four timepoints where they will vary by session too.

debias_itc works on the squared quantity where the bias is exactly 1/n, and
clips at zero rather than going imaginary, which is the honest answer for
an ITC at or below chance. Debiased noise reads 0.0000 at every trial count
tested.

Offered as itc_debiased alongside raw itc rather than replacing it, so
existing measure sets are untouched.

Also reproduces the lab's Rayleigh rcrit for comparability, with a
docstring making clear it answers a different question: it is a
significance threshold, not a bias correction, and it is zeroed for mouse
MEA in their pipeline in any case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…empty run as success

roi_evoked gated induced power on self._needs_induced(measures). The call site
shipped in 5c3e884; the method did not. Every subject of a 26-subject chirp run
raised AttributeError.

The second half is worse than the first. Per-subject exceptions are caught and
logged so one bad recording cannot sink a run, which is right. But with every
subject failing, aggregate found nothing to write, logged a single warning, and
the CLI printed "Done. Output: ..." and exited 0. The failure was visible only
in the log body, and the absent CSV was the first real signal.

So a run where every subject failed now raises, on the serial and the parallel
path alike. Partial failure stays tolerated — that distinction is the whole
point: some subjects failing is data, all of them failing is code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The electrode module implemented ITC, ERSP and STP and warned-and-skipped
'erp' and 'induced', so a config that meant thirteen measures to roi_evoked
meant nine to electrode_evoked. Comparing the two arms then compared whatever
happened to overlap.

Both are ports of the roi implementation against the per-channel trials rather
than the per-ROI ones: induced power from trials with the phase-locked average
removed, computed only when a measure asks for it, and ERP amplitude and
latency off the trial-averaged waveform.

_needs_induced moves to BaseAnalysis. It was on ROIEvokedAnalysis alone, which
is how the two modules came to read the same config differently in the first
place; one copy is one chance to disagree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@alexedmon1
alexedmon1 merged commit 9e2a202 into main Aug 26, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant