SC‑WBD

Engineering · 13

Everything needed to admit fMRI already existed

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.

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.