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
| source | what it is | stated blocker |
|---|---|---|
eegmmidb | 64-channel EEG | enabled |
ds002336 | EEG and fMRI, CC0 | "no registration between them has been run" |
ds000113 | 7 T fMRI | a 116 mm slab; parcels outside it unmeasured |
sleepedf | 2 EEG derivations | cannot 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.0for 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.
| attachment | what it is |
|---|---|
stimulus | the world driving the subject — audio, video, text |
observation | a measurement of the carrier, through a declared operator |
boundary_output | produced by the subject, measured outside the skull |
context | slow 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.
enabledis 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.