Skip to content

Evoked hypotheses, NBS edge-mask fix, and the 2026-09 audit remediation - #2

Merged
alexedmon1 merged 3 commits into
mainfrom
feat/evoked-hypotheses
Sep 4, 2026
Merged

alexedmon1 merged 3 commits into
mainfrom
feat/evoked-hypotheses

Conversation

@alexedmon1

Copy link
Copy Markdown
Owner

Summary

Three commits on top of main:

  1. Evoked modules get declared hypotheses (f601c5c). roi_evoked / electrode_evoked run the design:/hypotheses: spec through the tabular adapter with the measure as the FDR facet (shared analyses/_evoked_hypotheses.py).
  2. NBS significant-edge mask is filled (5176462). It was allocated and never written.
  3. Audit remediation (faa6610). A repo audit (2026-09-04) compared README / CLAUDE.md against the code and found 24 defects plus a dozen false README claims. Every item was verified against the code before fixing; all are fixed here and the docs are synced.

Behaviour changes (read before re-running a study)

  • Vertex absolute band power is a dB/Hz density, 10*log10(integral / bandwidth), matching the ROI/electrode path. Within-band statistics are unchanged (per-band constant shift); raw values and plots move by 10*log10(bandwidth).
  • Vertex modules honour top-level and per-analysis epoch_sampling (global → vertex: block → analysis block). vertex_cluster now epoch-samples when enabled. n_bootstrap: 0 = full timeseries on the vertex sampler too.
  • --jobs: explicit CLI value wins (including 1); omitted → YAML jobs:; 0/-1 actually auto-parallelize now.
  • --force really removes previous output (published tables/ + figures/ always; working data/ when the process step runs). Deprecated analysis names print/check the canonical output dir.
  • vertex_spatial is retired in Python too: loads nothing, calls no R, writes empty tables + a note.
  • roi_connectivity reads a metrics: list; the R report adapts to the metric columns present instead of requiring coherence.
  • roi_directed hypothesis tables renamed to roi_directed_{global,directed_edges,region}_hypotheses.csv with a dv column covering te / net_te / dtf; DTF-only runs no longer skip R.
  • fcd_comparison finds its two primaries across paradigm dirs (resting + vertex); sensor_dir / source_dir overrides accepted. Metadata carries supplements + requires.

Fixed

  • Evoked R scripts looped config$contrasts (NULL under modern configs) so their tables came out empty; they now derive contrasts from the design spec and accept --hypothesis.
  • vertex_evoked joins the hypothesis contract (--hypothesis, vertex_evoked_hypotheses.csv, list heading).
  • AAC / PPC from roi_cross_freq get an R hypothesis path (R/roi_cross_freq_edges_analysis.R, three tiers).
  • aggregate_to_regions() keeps delta_ref.
  • Package imports without mne (lazy TFR import with a clear ImportError); matplotlib is a core dependency; extras documented; lockfile refreshed.
  • R scripts ship in wheels/sdists (share/source-analytics/R); find_r_script_dir() checks there and honours SOURCE_ANALYTICS_R_DIR.
  • init writes a parseable design:/hypotheses:/paradigms: config for both discovery layouts; --output - streams YAML to stdout.
  • Timeout log messages report the real timeout; aperiodic about text says 12–45 Hz; PSD about describes dB/Hz density.
  • Hard source() in the R modules (no silent tryCatch); nlme dropped from the retired script.
  • Removed orphaned R/network_analysis.R, the dead *_network R lookups, and run_connectivity_network.sh; added scripts/run_study.sh.

Docs

README synced to the code (init, figures off by default, real analytics/results trees with paradigm + profile segments, install extras, R package list, catalog incl. fcd_comparison / electrode_signature / vertex_signature, --jobs / --profile / --paradigm semantics, no signed-STC fallback, no dB column). CLAUDE.md rewritten (it described the original PSD-only package). CHANGELOG filled through v0.6.0 plus this pass.

Test plan

  • pytest: 197 passed (new tests/test_audit_fixes.py, tests/test_r_scripts_smoke.py which runs the real R scripts on synthetic CSVs)
  • Every R/*.R parses; Rscript tests/test_hypothesis.R passes
  • uv build: wheel contains the 20 R scripts under share/source-analytics/R
  • Package imports with mne blocked
  • Re-run one real study (scripts/run_study.sh) and diff vertex absolute values against the expected per-band constant shift

🤖 Generated with Claude Code

https://claude.ai/code/session_01UukEcpPCEWnLHJHdT2fTLd

alexedmon1 and others added 3 commits August 27, 2026 12:33
…facet

roi_evoked and electrode_evoked were the last two modules with no path to the
declarative hypothesis layer -- HYPOTHESIS.md section 9 listed both as deferred for
"long-format DV". What ran instead was the R script's whole-brain
lmer(value ~ group * roi + (1|subject)) omnibus, which spends power across every
parcel when an auditory hypothesis names two, and cannot express a declared
contrast, omnibus or equivalence at all.

The deferral was real. Every other emmeans-tabular module exports one column per
DV and hands write_module_hypotheses a dv_cols vector; the evoked modules export
one `value` column with measure_name saying what it holds. One DV, twenty-odd
measures inside it.

The measure is a FACET, not a DV. The Python tabular adapter already carries
facet_cols, which runs an independent FDR family per facet across the spatial
grid -- and facet is exactly what dv_col is on the R side, where each DV gets its
own run_hypothesis() call and so its own family. That family boundary is the only
defensible one here: ITC is 0-1, ERSP is dB, ERP amplitude is signal units and
latency is seconds, so a family spanning them would pool quantities with no
common null. There is also no band axis -- each measure definition fixes its own
band and time window -- so the band coordinate is null rather than dropped.

Wired in analyses/_evoked_hypotheses.py, shared by both modules so they cannot
drift, the way _network_base is shared by the graph/NBS pair. roi_evoked scores
on roi, electrode_evoked on channel. Additive: the descriptive LMM in the R
scripts is untouched and still runs.

X49, found by the wiring and fixed here. Two different FDR families carried
byte-identical fdr_family labels. The label's `dv` coordinate read
facet_map.get("dv", "NA"), so it only picked up a facet literally named `dv` and
every real facet resolved to NA; the member hash could not separate them either,
because the members are the band x spatial cells and those are the same cells for
every facet. roi_network's 8 connectivity x 3 graph metrics produced 24 families
all labelled key=all|<hyp>|NA members=cell[N] hash=<same>. The label exists so
q-values from different families are provably non-comparable (REPORT_PLAN 10b)
and was asserting the opposite. q-values were never wrong -- BH is applied per
facet either way -- so this is label-only and no published number moves.

tests/test_evoked_hypotheses.py, 12 tests on planted synthetic signal: the
planted measure x ROI cell is recovered and neither a null ROI nor the null
measure is, the band coordinate is null, each measure is its own BH family, the
CSV fallback drives --steps statistics alone, a renamed design factor resolves,
and the X49 regression fails on the old code. 175 pass.

No study config is populated -- this is the mechanism only. The auditory
hypotheses themselves are Alex's to specify.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tten (X50)

nbs_permutation_test allocated sig_edges under the comment "Build significant
edge mask" and then returned it untouched, so significant_edges came back
all-False for every call -- including a 99-edge component at p = 0.0, alongside a
correct component_sizes=[99] and n_significant_components=1. The dataclass was
self-contradictory as returned.

Dormant in the shipped pipeline: nothing reads the field. _network_base uses
component_nodes and component_sizes, and hypothesis/edge.py re-derives components
itself, so no published number is affected. But it is a public field of a public
return type, and any new consumer silently gets "no edges" -- which is what
happened when surface-evoked's Phase 7 work read it to ask how many edges NBS
declares significant and got 0 for every detection.

The mask is now filled from each significant component's node set intersected
with the suprathreshold graph. That is exactly a component's edge set: components
are connected components of the suprathreshold graph, so they partition the
nodes and every suprathreshold edge with both endpoints in one component's node
set belongs to it.

test_nbs_significant_edges_mask_is_populated checks the mask is non-empty for a
significant component, contains no sub-threshold edges, and totals the
significant components' sizes. It fails on the old code. 176 pass.

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

Every item in the 2026-09-04 repo audit was verified against the code and
then fixed; the README claims it flagged as false are corrected too.

Behaviour changes
- Vertex `absolute` band power is a dB/Hz density like the ROI path
  (per-band constant shift; within-band statistics unchanged).
- Vertex modules honour top-level and per-analysis `epoch_sampling`
  (global -> vertex block -> analysis block); vertex_cluster epoch-samples;
  `n_bootstrap: 0` means full timeseries on the vertex sampler too.
- `--jobs`: explicit CLI value wins, omitted -> YAML `jobs:`, 0/-1 auto.
- `--force` actually removes previous output (published dirs always,
  data/ when the process step runs); output paths use the canonical name.
- vertex_spatial retired in Python (no loading, no R; empty tables + note).
- roi_connectivity reads a `metrics:` list; R adapts to the metric
  columns present instead of requiring coherence.
- roi_directed hypothesis tables renamed roi_directed_* and cover DTF;
  DTF-only runs no longer skip R.
- fcd_comparison finds its two primaries across paradigm dirs
  (sensor_dir / source_dir overrides); metadata carries supplements/requires.

Fixed
- Evoked R scripts derive contrasts from design:/hypotheses: (tables were
  empty under modern configs); Python forwards --hypothesis.
- vertex_evoked joins the hypothesis contract (SELECTABLE, perm adapter,
  vertex_evoked_hypotheses.csv, `list` heading).
- AAC/PPC get an R hypothesis path (roi_cross_freq_edges_analysis.R).
- aggregate_to_regions keeps delta_ref.
- Package imports without mne (lazy TFR import, clear ImportError);
  matplotlib is a core dependency; R scripts ship in wheels/sdists and
  find_r_script_dir() looks there (plus SOURCE_ANALYTICS_R_DIR).
- `init` writes a parseable design/hypotheses/paradigms config for both
  discovery layouts; `--output -` streams YAML to stdout.
- Timeout log messages report the real timeout; aperiodic `about` text
  says 12-45 Hz; PSD `about` describes dB/Hz density.
- Hard `source()` in the R modules (no silent tryCatch); nlme dropped.
- Removed orphaned R/network_analysis.R, the dead *_network R lookups and
  run_connectivity_network.sh; added scripts/run_study.sh.

Docs: README synced (init, figures off by default, real output trees,
extras, R packages, catalog incl. fcd_comparison/electrode_signature,
--jobs/--profile/--paradigm, no signed-STC fallback, no dB column);
CLAUDE.md rewritten; CHANGELOG through 0.6.0 + this pass.

Tests: 197 passed (new tests/test_audit_fixes.py, tests/test_r_scripts_smoke.py).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UukEcpPCEWnLHJHdT2fTLd
@alexedmon1
alexedmon1 merged commit a4080c5 into main Sep 4, 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