SC‑WBD

Getting fMRI into parcel space

Three measured datasets sat disabled behind blockers that read like missing components. Two of them were missing a caller, not a component — and the module that would have been the caller had been on the main branch the whole time.

Applies to SC-WBD-002-pilot (run 2) and the source-card registry, which is shared across runs. The two follow-up sections report SC-WBD-003 and SC-WBD-004.

The question

Why does a whole-brain model train on EEG alone when there is fMRI on disk?

The honest answer looked like scope: parcel-space state, scanner-space data, no registration between them. Four measured sources are declared, three are switched off. That reads as a roadmap.

What was actually blocking them

sourcewhat it isstated blocker
eegmmidb64-channel EEGenabled
ds002336EEG and fMRI, CC0"no registration between them has been run"
ds0001137 T fMRIa 116 mm slab; parcels outside it unmeasured
sleepedf2 EEG derivationscannot constrain a 64-channel head

Not one of them is disabled by policy. Each waits on an operator. So the question became: how much of that operator has to be written?

None of it, for two of the three

anatomy/registration.py is 513 lines titled "BOLD → parcel registration: bring the atlas to the subject, never the reverse." It contains the EPI ← T1w ← template chain, the label pull onto the EPI grid, and ParcelCoverage — "how many EPI voxels each parcel actually got, for one subject/run", which is precisely the coverage mask the slab problem calls for.

It was imported by nothing except its own test file.

So was everything else it needed: the volumetric Schaefer-400 atlas in the exact template the module requires, the 1 mm MNI reference, the registration solver, and 55 BOLD runs with per-subject anatomy.

The card said the registration "has not been run." That is literally true, and it reads as "none is available." The distance to a second modality was a caller, not a component.

Running it

The missing piece was the consumer that joins registration to the atlas and averages within parcels. Written, then run on all ten subjects:

10/10 runs cached · 10 participants · 89 windows · 0 dropped
~160 s per subject, chain reused across that subject's runs

per-subject coverage
  xp103 400/400   xp106 400/400   xp108 400/400   xp101 399/400
  xp107 395/400   xp105 391/400   xp109 384/400   xp102 379/400
  xp104 378/400   xp110 357/400   <- 43 parcels never in the field of view

Coverage ranges from 0.89 to 1.00. A global mask would be wrong for six of these ten subjects. That is not a tuning detail — it is why the mask has to travel with each run rather than being computed once.

The invariant that keeps this honest

A parcel outside the acquisition has no measurement. It is NaN in the timeseries and False in the mask, and those have to be the same statement.

A BOLD value of 0.0 for a parcel that was never in the field of view is a fabricated observation, and once it is in an array nothing downstream can distinguish it from a real one.

So the likelihood requires the mask and raises without it — a caller who forgets gets no number rather than a plausible one. And the test asserts mask and data agree parcel by parcel, not merely in count: a count check passes when the same number of parcels drift in both directions, which is the easy version of that test and the useless one.

Then a second axis turned out to be missing

The datasets worth adding next carry more than brain recordings. MEG-MASC ships the audio that was played, with phonetic and word annotation. A naturalistic EEG-fMRI set ships eye tracking and ECG. These are not lower-quality observations; they are not observations at all.

The source ontology ranks sources by integrity — measured, then calibration, then prior, then simulator. That axis is real and it cannot express the difference between a stimulus, a measurement, and something the subject produced.

attachmentwhat it is
stimulusthe world driving the subject — audio, video, text
observationa measurement of the carrier, through a declared operator
boundary_outputproduced by the subject, measured outside the skull
contextslow conditioning that neither drives nor is driven

It deliberately does not default from the role. A single card may carry EEG, the audio that was played, and the participant's gaze — so guessing observation because the role is likelihood is exactly how a stimulus gets trained as a measurement of the brain.

And the enable did not take

With the path built, the card could be switched on. It was — and nothing changed, which is only visible if you check rather than assume.

configs/source_cards/             9 cards   read by RELEASE  (licence, manifest, publish)
configs/curriculum/source_cards/  7 cards   read by TRAINING

Both fMRI cards exist only in the release directory. Enabling one there does nothing to training — and in the other direction it would have added that dataset's licence terms to a published model card with no checkpoint having consumed it.

A licence claim ahead of its evidence — the same error as linking a model before its scores exist, in the one place this project is most careful about.

All seven shared cards differ between the directories. Today the licence-bearing fields happen to agree. That is luck, not structure: two directories of one object, read by different halves of the system, agree until someone edits one — and the halves that disagree are what trained and what the artifact claims about what trained.

The pattern, five times in a day

A stage-admission module with its own patch and eleven tests, unapplied. A config block declaring exactly the right things, read by nobody. A registration module with no caller. A volumetric atlas nothing loaded. A card enabled in the directory the trainer does not read.

The work exists, and the half that would use it is pointed somewhere else. Each time, the artifact that would have consumed it describes the capability as absent — so the codebase reads its own silence as a limitation.

Follow-up: fixing it installed the opposite error

The split above is now closed. The release path no longer names a directory; it derives one from the checkpoint, which records what actually trained:

checkpoints/scwbd-002-pilot/last.pt
  config["mixture_cards"] = configs/curriculum/source_cards

Which settled the question the two directories left open — and revealed that the literal in publish.py pointed at the set tests/curriculum/test_tiers.py names LEGACY, on line 19, in the same file that names the other one CORRECTED. The test suite had been saying so in a variable name the whole time.

Then the correction produced, in one step, the error this page warned about — pointing the other way. The corrected card is enabled, so ds002336 immediately appeared on the published card under DATASET INPUTS, with its citation and its CC0 term, for a checkpoint whose recorded participant split contains none of its participants.

enabled is a statement about the mixture, not about a checkpoint. A card switched on after a run has finished is enabled and unconsumed at once, and nothing in the card can distinguish those — the field has no tense. Reading it as "was an input" only breaks when the mixture and the artifact sit at different points in time, which is exactly the situation a fix creates.

The checkpoint can tell them apart, because it stores the split it trained on. 109 participants — exactly eegmmidb_real's declared count, with ds002336_real's 10 appearing in no fold. The card now says so.

The check that would have accused the right dataset

The obvious implementation intersects participant IDs. Run directly:

data/ds002336: overlap with split = 0
data/eegmmidb: overlap with split = 0   <- the corpus that trained the model

Zero for both — the corpora label participants differently (S001 against sub-xp101). The check would have flagged the dataset that did train the model as unconsumed, with the same confidence and the same shape of output as a correct answer.

So the shipped check compares counts, which belong to no namespace, and returns nothing when the comparison is not decidable rather than guessing. The standing rule it produced:

A fix that moves which source a consumer reads must be checked in the direction it now over-claims, not only the direction it previously under-claimed. Correcting a pointer changes the error's sign, not its existence.

Follow-up: run 3 answered the question for the other two

The table above records stated blockers, and the page's argument is that each was an operator gap rather than a policy. SC-WBD-003 finished the count.

sleepedf needed about forty lines. Its blocker was "two bipolar derivations cannot constrain a 64-channel observation head", which is true of forcing Sleep-EDF through the eegmmidb head and is not a property of the data. A bipolar channel measures V(anode) − V(cathode), and the forward operator is linear in the source amplitudes, so the correct gain row is exactly L[anode] − L[cathode] — derived from the same monopolar solution every other montage is built from. The operator's rank is 2, which the lead field states on itself so nobody reads 414 parcels out of two channels. It is now enabled at tier 1 with 77,827 windows over 78 participants.1The alternative was zero-padding two real channels into a 64-channel montage. That asserts 62 measured-and-silent electrodes, which is fabricated data, and the model would learn that most of the scalp is quiet.

ds000113 needed none, and its stated blocker was wrong. It is not a 116 mm slab — that is the orientation-decoding sub-study. The localizer runs actually on disk are whole-brain: all 20 parcellated into Schaefer400x7, measured coverage 0.988–1.000 per run. The real blocker turned out to be one nobody had stated, because it is not an operator at all: the snapshot carries no licence. No LICENSE file, a "License" key whose value is the empty string, and no occurrence of "licen", "PDDL", "CC0" or "Creative Commons" in the README. The OpenNeuro draft record says CC0; the draft is mutable and is not the snapshot on disk, and a pointer to where a licence might be found is not a licence.

So the corpus is parcellated, cached and unusable, and the card says so in those terms. That is the third instance on this page of the same shape: the blocker everyone can name is rarely the one holding the door.

Follow-up: run 4 admitted fMRI properly, and it lost

The fourth instance is the largest. Everything above gets a haemodynamic corpus into parcel space; none of it makes the likelihood work. SC-WBD-003's measured BOLD path never integrated the Balloon–Windkessel ODE, so its five haemodynamic parameters were bit-identical to their initialisation across all five stages. The term was inert and the run reported it. SC-WBD-004 fixed the clock: the measured path now rolls the neural state for the duration a BOLD frame covers, 250 steps per frame at TR = 2 s.

The repair worked, and the likelihood then degraded through training. real_bold_nll ran 1.99 to 36,472 over 14,600 steps while the electrical likelihood improved, the total objective stayed flat near 1.0, and the haemodynamic variance channel held steady — so this is one term losing a competition rather than an optimiser failing. Four arms located the cause: freezing the five Balloon parameters changes nothing, and freezing the shared trunk makes the haemodynamic likelihood improve. The head models the data; it cannot track a latent state being reshaped underneath it.

The mechanism is a gradient-share imbalance and it is measured. The sole haemodynamic corpus is 5.39% of the source mixture and is outvoted 17.6 : 1 by the electrical and behavioural sources.2This page first carried 4.13% and 23.2 : 1. Those came from the pre-launch smoke — four steps per stage, where the source sampler has not converged to the configured proportions — and were quoted as the trained run's before the trained run existed to contradict them. The completed run measured 5.39%, and holds between 4.64% and 5.53% across all five measured stages. The correction is not in the argument's favour: a larger BOLD share makes divergence-by-dilution slightly harder to argue, not easier. The trunk converges on what the electrical sources require, and the haemodynamic head reads that same state.

So the shape holds a fourth time, one level deeper. The blocker was never the registration module, and it was never the clock either. Getting a modality in is not the same as giving it a vote, and only a working likelihood could show the difference — run 3's inert term could not have produced this result, and its silence read as absence rather than as ignorance. No fMRI, haemodynamic or neurovascular claim is available from SC-WBD-004, and the model card says so at the top.