ssvep.io — manifests and data¶
Part of the API reference, generated from the docstrings in src/ssvep.
Run manifest I/O¶
ssvep.io.manifest ¶
Run-manifest I/O and schema validation.
The manifest (schemas/run_manifest.schema.json) is the toolbox's single source of
truth: the builder emits it, acquisition stamps it into each recording, analysis reads it.
This module loads the JSON Schemas and validates manifests/markers against them, so every
producer (builder now; runtime later) can guarantee a conformant contract.
A manifest describes exactly one run (AGENTS.md §2.1). Manifest v1.3 renamed the
run-identity block experiment → run; :func:canonicalize_manifest maps the old key on
read so every pre-1.3 protocol and recorded sidecar still loads, and :func:run_meta is the
tolerant accessor for that block regardless of which key a given manifest carries. Manifest
v1.4 adds image targets and retires the stimulus shape: "image" that never drew one;
:func:canonicalize_manifest maps that too.
canonicalize_manifest ¶
Return manifest in the current shape, mapping the two legacy spellings a saved one can have.
experiment→run(v1.3, #42). Every sidecar ever written embeds the manifest it recorded under (RunRecorder.sidecar), and the older saved protocols predate the rename.- Stimulus
shape: "image"→"rect"(v1.4, #256). The value was in the schema's enum from v1.0 and nothing ever drew an image for it — the renderer fell through to a rectangle, in whateverpatternthe target had. So"rect"is what such a manifest showed, and mapping it there keeps the manifest describing what was on screen. Only when the stimulus also carries animageblock — a picture that no renderer has yet drawn, but that the manifest does declare — does it becomepattern: "image", since an image is a fill, not an outline. Mapping it topattern: "image"unconditionally would declare a picture that was never there, and would stop the manifest validating: v1.4 requires a picture for an image fill.
Idempotent and non-mutating: a manifest needing neither mapping is returned as the same
object; otherwise a shallow copy is made (and each rewritten stimulus is itself a copy). This
is the single point that keeps every saved manifest loadable, and it runs inside
:func:manifest_fingerprint, so a legacy file and its re-saved current-shape copy fingerprint
identically. The shape mapping does move the fingerprint of a file that still says "image"
relative to builds before it; no saved manifest, protocol or sidecar in this repository says it,
and a pre-1.2 protocol pinning one would report drift — the hard stop those have always had.
Source code in src/ssvep/io/manifest.py
run_meta ¶
The run-identity block (name/task/description), tolerant of pre-1.3 manifests.
Reads run (v1.3+) or experiment (legacy). Use this everywhere a manifest — including one
lifted out of an on-disk sidecar, which may carry either key — is read for its name or task.
Source code in src/ssvep/io/manifest.py
manifest_fingerprint ¶
Short stable hash of a manifest's semantic content — the identity a session protocol pins.
Canonicalized first (§canonicalize_manifest), so the pre-1.3 experiment → run rename does
NOT change a run manifest's fingerprint: a protocol authored against the legacy shape still
resolves against the same file re-saved in v1.3 shape. Key order is irrelevant (sort_keys).
Ties a recording / QC report / protocol pin to the exact design it was built against.
Source code in src/ssvep/io/manifest.py
design_fingerprint ¶
:func:manifest_fingerprint with provenance-only and prose fields ignored.
"Is this the same design?" for the name check (#467): run.description is prose, and
run.builder_version / manifest_version record which build wrote the file, so none of them
make a different design. Without this an identical copy would stop being identical after a
package version bump. Nor does acquisition: hardware is the session's, not the run's (#484),
so two copies of one design that an older build stamped with different hardware are one design.
Never stored or pinned anywhere, so it cannot move any recorded fingerprint;
:func:manifest_fingerprint itself is untouched.
Source code in src/ssvep/io/manifest.py
name_clashes ¶
Saved run manifests in folders that carry this manifest's run.name as a different design.
The save-time duplicate-name check (#467), asked only when a collision would be created
(see DesignWindow._resolve_name_clash). Same name + same design (:func:design_fingerprint)
is not a clash, so re-saving a run, or saving an identical copy, is allowed. exclude is the
path being saved to (a file never clashes with itself). Unreadable or non-manifest *.json
files are skipped: this is a nicety, never a reason a save fails. Folders are scanned
non-recursively and de-duplicated; a missing folder is ignored.
Source code in src/ssvep/io/manifest.py
saved_run_names ¶
Every run.name among the readable run manifests in folders (see :func:name_clashes).
Source code in src/ssvep/io/manifest.py
suggest_free_name ¶
name with the smallest " (2)", " (3)"... suffix not in taken.
paradigm ¶
The manifest's paradigm: 'ssvep' (flickering targets), 'resting' (eyes-open/closed)
or 'fpvs' (fast periodic visual stimulation, #494).
Absent ⇒ 'ssvep', so every pre-1.2 manifest keeps reading correctly.
Source code in src/ssvep/io/manifest.py
feedback_config ¶
{mode, wait_s, show_s} when the run declares participant feedback, else None.
The manifest is authoritative (AGENTS.md section 2.2): the schedule, the renderer, the live
answer's route and the analysis all read this one function and never infer feedback from the
data. Absent block, or mode: "none", is None - a run that showed nothing between trials.
A resting or FPVS run has no decoder to answer from, so it never has feedback.
Source code in src/ssvep/io/manifest.py
cue_mode ¶
'cued' or 'free': who picked each trial's target, as the manifest declares it.
Every consumer of a trial's label asks this one function (never "is target_freq_hz there?"):
a free-choice run has no label by design, and inferring that from a missing field is how a free
run would silently be treated as cued. A resting run has no targets to choose and reads cued.
Source code in src/ssvep/io/manifest.py
is_cued ¶
False only for a free-choice run (design.cue_mode == 'free'): it carries no label.
feedback_mode ¶
The declared feedback mode ('none' when the run showed none).
early_stop_config ¶
The run's live early-stopping rule (design.early_stop, v1.8), or None for a run whose
trials all last stim_s (an absent block: every earlier manifest).
Returns {statistic, method, min_window_s, max_window_s, step_s, confidence_margin,
calibration}. max_window_s is not a manifest field: it is design.timing.stim_s
(one fact, one place), read here so no consumer re-derives it. Every consumer (the runtime, the
renderer, the analysis) asks this one function and never infers early stopping from short
trials (AGENTS.md section 2.2). A resting or FPVS run has no decoder and reads None.
Source code in src/ssvep/io/manifest.py
early_stop_schedule_problem ¶
Why a live early-stopping schedule cannot mean anything, in one wording for the builder and
the loader (the maximum window is design.timing.stim_s); None when it is sound. Beyond
:func:~ssvep.classification.decoding.check_decision_schedule (which the offline replay also
uses, and which allows a single look at the maximum), a live declaration needs its first look
before the end of the stimulation: with only the last look it can never stop early, and is
the fixed rule under the early-stop stamp.
Source code in src/ssvep/io/manifest.py
early_stop_source_problem ¶
Why a noise_default source cannot carry this threshold, or None (#487). For a design
being authored or used for a new recording, never for loading a recorded manifest. Only that
source is judged: study may carry any threshold the schedule check accepts.
Source code in src/ssvep/io/manifest.py
early_stop_offset_problem ¶
Why design.timing.onset_offset_s cannot serve a live early-stopping run, or None.
The live window origin is stim_on plus the lead-in the schedule presents, so the field must
be present and finite: without it the live decoder would wait for a lead-in that is never
presented and could never decide at the maximum window.
Source code in src/ssvep/io/manifest.py
is_resting ¶
resting_params ¶
design.resting for a resting manifest (the authoritative block structure); {} if absent.
is_fpvs ¶
fpvs_params ¶
design.fpvs for an FPVS manifest (the authoritative stream design); {} if absent.
has_decoder ¶
True only for the paradigm a live SSVEP decoder can answer (ssvep).
A resting run has nothing to decode, and an FPVS run's response is read offline from its
spectrum (ssvep.analysis.fpvs), never by classifying windows: neither can have participant
feedback, free choice or live early stopping, and neither is an SSVEP decoding contrast.
Source code in src/ssvep/io/manifest.py
carrier_hash ¶
Short content hash (12 hex) of the wiring a manifest's acquisition block records, or
None when there is no montage to hash.
The wiring is what distinguishes two carriers that share a name: the amplifier channel and the 10-10 site of each montage entry, in order, plus the electrode type. Positions and measured impedance are deliberately left out (positions follow from the site; impedance changes every session). Computed from the block itself, never from a carrier file, so it can be re-checked anywhere the manifest travels (#410).
Source code in src/ssvep/io/manifest.py
carrier_hash_problem ¶
A sentence when acq carries a carrier_hash its own wiring does not reproduce (the
copy was edited by hand after it was written), else None. A block without the field
(every stock carrier, and every manifest from before it existed) is never a problem.
Source code in src/ssvep/io/manifest.py
unknown_carrier_problem ¶
A sentence when acq names a carrier that is neither built in nor copied into the block.
A user-defined carrier is copied whole into acquisition.montage when it is picked, so the
copy is what makes a non-built-in name usable. A name with no copy (a retired built-in, a typo,
a hand-written protocol) has no wiring anywhere this manifest goes, and is refused out loud
rather than set up on a substitute montage. A block without a headset is never a problem.
Source code in src/ssvep/io/manifest.py
carrier_label ¶
The headset name, followed by its wiring hash in brackets when the block carries one.
Source code in src/ssvep/io/manifest.py
validate_manifest ¶
Raise jsonschema.ValidationError if manifest violates the schema; else return.
Canonicalized first, so a legacy experiment-keyed manifest validates against the v1.3 schema.
A picture on a non-image fill gets a message that names the problem
(:func:_explain_image_fill_conflicts) rather than the schema's own.
Source code in src/ssvep/io/manifest.py
load_manifest ¶
Load, canonicalize, and validate a manifest JSON file.
Returns the v1.3 shape (run key), so a legacy experiment-keyed file on disk — a saved
protocol or a recorded sidecar's embedded manifest — is transparently upgraded for every caller.
Source code in src/ssvep/io/manifest.py
save_manifest ¶
Validate then write a manifest to path (pretty-printed). Returns the path.
Source code in src/ssvep/io/manifest.py
BIDS conversion¶
ssvep.io.bids ¶
Convert acquired XDF recordings (sourcedata/) into a BIDS-EEG dataset.
The runtime records each run as an XDF (durable, LabRecorder-compatible) plus a
*.session.json that embeds the full run manifest, provenance, consent and impedance
(see :mod:ssvep.io.xdf and the runtime). That XDF is the raw truth and belongs in
sourcedata/. This module derives a BIDS-compliant view of it: BrainVision
(.vhdr/.vmrk/.eeg) signals at the subject level, with *_eeg.json sidecars,
*_channels.tsv, *_events.tsv (every structured marker, sample-accurate), plus the
dataset-level dataset_description.json, README, CHANGES, participants.tsv/.json
and a task-<task>_events.json describing the event columns.
Design rules honoured here:
* The manifest is the source of truth. Channel names, sampling rate, montage, target
frequencies, and trial structure all come from the embedded manifest/markers — never inferred
from the signal.
* Sample-accurate events. Each marker is mapped to its nearest EEG sample via the recorded
LSL timestamps (not time × nominal_rate), so events stay aligned even when the effective
rate drifts a few tenths of a percent from nominal.
* Compliance. Only sub-XXX codes; no demographics are invented (unknown → n/a).
Everything written lives under the git-ignored BIDS/ tree.
Units: BrainFlow (Cyton / Unicorn) delivers EEG in microvolts; MNE works in volts, so we
scale µV → V on the way into :class:mne.io.RawArray and BrainVision stores true volts.
Conversion is not optional and not a separate errand. Analysis reads the XDF directly, so
nothing downstream needs the BIDS view — which is exactly how it came to be skipped: the GUI's
Analyze tab wrote derivatives/ for a dataset that had no BIDS-standard raw data to be a
derivative of. :func:ensure_converted (with :func:is_converted as the "is it already there?"
check) is that step, callable per selection, so every path that analyses data also derives the
dataset — 01_xdf_to_bids.py remains the way to (re)derive the whole tree at once.
ConversionReport
dataclass
¶
What :func:ensure_converted did — one entry per run, for a log line or a GUI status.
Source code in src/ssvep/io/bids.py
ConversionRefused ¶
NoEEGRun ¶
NotStudy
dataclass
¶
Why a session (or one recording of it) is not study data: :data:NOT_STUDY_PILOT,
:data:NOT_STUDY_DECLARED (group is the declared group) or :data:NOT_STUDY_UNREADABLE.
Source code in src/ssvep/io/bids.py
NotStudySessions
dataclass
¶
Every session that is not study data, and why: the one rule export, the demographics
fill, session_type and every contrast read (#400), built by :func:not_study_sessions.
locked: {sub: {ses}}, sessions any of whose sidecars was locked as a pilot / development
session (:func:is_pilot_session). declared: {sub: {ses: group}}, the group
pilot / test a session's declarations give it after it was recorded (#384).
unreadable: {sub: {ses: why}}, a session whose declarations cannot be read (they may say
pilot). dataset: why no declaration could be read at all, which holds back every session.
seen: every (sub, ses) with a recording or a session folder.
Source code in src/ssvep/io/bids.py
why ¶
Why sub/ses is not study data, or None when it is.
Without sidecar, the session: locked as pilot when any of its sidecars says so
(export, the demographics fill and session_type ask this). With sidecar, one
recording of it: locked as pilot only when that sidecar says so (a contrast asks this, so a
session locked more than once keeps its study recordings, #161). A declared group or an
unreadable declarations file is about the whole session either way.
Source code in src/ssvep/io/bids.py
sessions ¶
{sub: {ses}} for every session held back, for any of the three reasons.
Source code in src/ssvep/io/bids.py
keys ¶
{(sub, ses): why} for every session held back.
load_session ¶
Load a *.session.json (manifest + provenance + data-use stamp + plan + impedance).
markers_to_events ¶
Build a rich BIDS events table from the structured JSON markers.
Every marker becomes a row. Each is snapped to its nearest EEG sample (via the recorded
timestamps), so onset = sample / sfreq lands on that exact sample. stim_on rows get a
duration running to the matching stim_off; the extra columns carry the decoding label
(target_freq_hz) and trial structure.
Source code in src/ssvep/io/bids.py
measurement_date ¶
The run's BIDS meas_date (and so scans.tsv acq_time): its date-shifted start, in
UTC — or None, which BIDS records as n/a.
None in three cases, all on purpose (#286):
- Not shifted. A sidecar written before date-shifting carries the real date. The converter can't shift it (the offset it would need was never kept, by design) and won't copy it, so the BIDS view simply has no date. It never writes a real one.
- No timezone. Those same old sidecars stored local wall-clock time with no offset, and this used to tag it UTC without converting it — the time was then off by the site's UTC offset. A naive time names no instant, so it is left out rather than guessed at. Everything the toolbox writes now is shifted and in UTC.
- Before MNE's floor. MNE stores
meas_dateas signed 32-bit seconds, so a date before 1901-12-13T20:45:52Z would convert and then break every laterraw.save(). The toolbox's own shift window starts in 1902, so only a hand-edited sidecar gets here.
Source code in src/ssvep/io/bids.py
build_raw ¶
Assemble an :class:mne.io.RawArray (volts) with montage, meas date, line freq, annotations.
Returns (raw, events_df). The events table is the authoritative one we later write to
*_events.tsv; annotations on the raw exist so MNE-based tools see the same events.
The mains frequency comes from the site profile, not from a constant: 60 Hz is a fact about
North America, and this toolbox is not only run there (#165). Unconfigured ⇒ None, which
BIDS records as "n/a" — unknown said out loud rather than a plausible wrong number.
Source code in src/ssvep/io/bids.py
task_of ¶
The BIDS task label for a run — from its manifest, never from a caller-wide default.
The run manifest's run.task (legacy experiment.task) is the design's own statement of what
ran ('ssvep', 'rest', …). A single task= argument applied to every run in a dataset silently
relabels any run that isn't the majority paradigm: a resting run once landed in BIDS as
task-ssvep despite its manifest saying rest, which is exactly the "analysis infers design"
failure the manifest spine exists to prevent (AGENTS.md §2).
Source code in src/ssvep/io/bids.py
bids_path_for ¶
Where this run's BIDS view lives — the same path :func:convert_run would write.
Split out so a caller can ask whether a run has been converted without converting it
(:func:is_converted); the entities are derived in exactly one place.
Source code in src/ssvep/io/bids.py
is_converted ¶
Is this run's BrainVision view already on disk, no older than its XDF, and produced by the converter code currently running?
Existence alone is not enough: the BIDS tree is derived, so a source recording that is newer than the copy derived from it means the copy describes data that has since changed. Re-deriving is idempotent and cheap relative to being quietly wrong (AGENTS.md §2.2).
The mtime check alone misses the other direction of that same rule: a converter code change
(a bug fix in the conversion logic) doesn't touch the source XDF at all, so a run converted before
the fix landed would report "up to date" forever even though re-running the fixed converter would
produce different output (#113) — a fix that never reaches what already happened. So conversion
also stamps a *.converter.json marker (:func:_stamp_converter_version) with a fingerprint of
this module's source (:func:_converter_fingerprint), and this check compares it against the
fingerprint of the code running now. A run with no marker at all — converted before this check
existed — is treated as needing reconversion too, which is the one-time cost of adopting it.
Source code in src/ssvep/io/bids.py
no_eeg_reason ¶
Why this recording has no EEG to convert or analyse, or None (#482).
Read from the sidecar the recorder wrote (provenance.eeg_samples == 0, with its
incomplete_reason), never inferred from the XDF: the manifest spine says what the run is. A
run that is merely short still has EEG, and still converts.
Source code in src/ssvep/io/bids.py
active_kind ¶
This machine's site-profile kind, for :func:ssvep.runtime.data_use.data_use_of.
It decides only whether an unstamped recording counts as teaching (#286 Q1) and which purpose a
contrast pools. A profile that cannot be read falls back to generic: never teaching, so
the fallback can only refuse more, never infer a purpose nobody stamped.
Source code in src/ssvep/io/bids.py
conversion_refusal ¶
Why this recording (its *.session.json) may not be converted here, or None.
The one matrix's CONVERT column (:data:ssvep.runtime.data_use.MATRIX), read through the
fail-closed reader: every readable stamp with something saved may be converted (research from
tier 1, pilot, teaching, bench), and tier 0 never has anything to convert, since nothing was
written. A recording with no stamp, a malformed one, or only the older three-flag consent record
is refused: there is no migration (#286 Q2). The one exception is #286 Q1: on a teaching machine
an unstamped recording that does not say it was made on a lab machine counts as teaching, which
may be converted and analysed locally (and is never pushed or exported). kind is the
machine's kind (:func:active_kind when None).
Source code in src/ssvep/io/bids.py
recording_purpose ¶
The purpose a recording's sidecar declares, for labelling (session_type,
SSVEPPurposes), never for granting anything: pilot when it is declared a pilot /
development session (:func:is_pilot_session, which also reads the older
session_position.pilot), else its stamp's purpose as the fail-closed reader reads it
(including the #286 Q1 teaching inference), else :data:PURPOSE_UNSTAMPED.
Source code in src/ssvep/io/bids.py
stim_file_column ¶
events.tsv:stim_file for each row, from {stimulus id: stim_file} (:func:_bids_stimuli).
A row whose cued target is an image target names its picture. An FPVS oddball/item row
(#494) names the item it marks: that item's picture is the image stimulus "item <id>"
(:func:ssvep.stim.fpvs.item_image_stimuli). Everything else is n/a.
Source code in src/ssvep/io/bids.py
ensure_converted ¶
Derive the BIDS view of refs for any run that does not already have an up-to-date one.
This is the 01_xdf_to_bids.py step, scoped to the runs a caller cares about. It exists
because analysis reads the XDF directly, so a pipeline run would otherwise fill
derivatives/ while leaving the dataset without the BIDS-standard raw data it is a derivative
of — a tree that looks analysed but isn't a BIDS dataset.
Failure to convert one run is recorded, not raised: the XDF is the source of truth and the
analysis can still run from it. progress(i, n, stem) is called before each conversion.
A run its data-use stamp does not let this machine convert (:func:conversion_refusal) is
recorded in :attr:ConversionReport.refused with the reason, and is neither converted nor
counted as present, whatever is already on disk.
Source code in src/ssvep/io/bids.py
profile_mismatch ¶
Why this run must not be converted under the active profile, or None (#298).
A run recorded under a configured deployment (its sidecar's site_profile.kind is not
generic) and converted by a process whose active profile is generic would be stamped with
no institution, no governance and no licence — silently, and undetectably afterwards. That is
what happens when the converter runs in a process that could not see SSVEP_SITE_PROFILE
(another user, a scheduled task, a shell opened before it was set). Sidecars from before the
stamp existed carry no site_profile and are not second-guessed.
Source code in src/ssvep/io/bids.py
convert_run ¶
Convert one XDF run into the BIDS tree and return its EEG :class:BIDSPath.
task overrides the manifest's own label; leave it None (the normal path) to use task_of.
A run recorded under a configured deployment is refused under the generic profile
(:func:profile_mismatch), and a run its data-use stamp does not let this machine convert
raises :class:ConversionRefused (:func:conversion_refusal) before anything is written.
Source code in src/ssvep/io/bids.py
discover_runs ¶
Find every *_eeg.xdf under sourcedata/ and pair it with its *.session.json.
sub/ses come from the filename's entities, not the folder it sits in, and the two
are not cross-checked: a recording filed in another participant's folder is that filename's.
Readers keyed by folder (the beh/ demographics fill) agree with it on every correctly filed
dataset only (#372 review).
Source code in src/ssvep/io/bids.py
migrate_toplevel_to_sourcedata ¶
Move raw *.xdf / *.session.json / *_impedance.json sitting at the subject level
(the pre-BIDS layout) down into sourcedata/. Idempotent — returns the files moved.
Source code in src/ssvep/io/bids.py
write_dataset_metadata ¶
(Re)write the dataset-level files with our richer, compliance-aware content.
tasks is every task label present in the dataset — each gets its own task-<x>_events.json,
because a dataset holds more than one paradigm (ssvep + rest). task is the single-label legacy
form.
SSVEPPurposes (:data:PURPOSES_KEY) lists the purposes the dataset's recordings were made
for (:func:dataset_purposes), so a reader of the dataset alone can tell it is not all research.
SSVEPStudy (:data:STUDY_KEY) names its study, when it has one (#396).
Who made the dataset and under what approval comes from the site profile (#165). An
unconfigured deployment produces a dataset that names no lab and cites no ethics file — and
HowToAcknowledge is then omitted rather than invented, while License still defaults to
"not licensed for redistribution", because the restrictive reading is the safe one when nobody
has said (COMPLIANCE R5's principle applied to redistribution).
Source code in src/ssvep/io/bids.py
1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 | |
enrich_participants ¶
Fill species/group and age_range/gender in the mne-bids-generated
participants.tsv, and declare the dataset's pilot / development sessions. Returns notes worth
reporting.
No demographics are invented — unknown fields stay n/a (COMPLIANCE: coarse-only, no
linkage). group is a declaration, never overwritten; see :mod:ssvep.io.participants for
its rule, including the one-time rewrite of the pilot stamp this function wrote on every
conversion before #269. A between-subject factor declared with the recordings
(sourcedata/sub-XXX/ses-YYY/declarations/, #276) is rendered into an undeclared cell; a
disagreement is kept, reported, and excluded from contrasts on that factor
(:func:ssvep.io.participants.apply_declarations). A group declared there binds only its
own session and is never rendered. age_range/gender (#290) are derived from the background
questionnaire's beh/ answers, when any exist —
:func:~ssvep.io.participants.resolved_demographics has the multi-session rule.
This is the one function every conversion path calls — :func:convert_dataset,
:func:ensure_converted, and Analyze's batch (ssvep.analysis.batch) — so they declare pilot
sessions identically (#161). It reads every recording under sourcedata/, not only the ones
just converted: whether every recording of a participant is pilot is a fact about all of them.
Each recording declared pilot (its data-use stamp's purpose, or the legacy
session_position.pilot, read only as a restriction) becomes session_type = pilot in
sub-XXX_sessions.tsv (:func:write_session_types), and group becomes pilot only
when every recording of the participant is pilot and no group is declared
(:func:ssvep.io.participants.apply_pilot_groups). Neither is what keeps pilot data out of a
contrast: the contrast reads each recording's own sidecar.
Every session's session_type is its recordings' purpose (#286 P3d), combined with a group
its declarations give it afterwards (#400, :func:session_types). The demographics fill skips
every session :func:not_study_sessions holds back — locked as pilot, declared pilot / test
afterwards, or with declarations that cannot be read — the same set export holds back.
Source code in src/ssvep/io/bids.py
fpvs_order_notes ¶
One note per FPVS recording whose design asks for a new item order each session
(design.fpvs.realise: "per_session") but which has no per-session realisation record:
a toolbox that could not draw the order recorded it, so the design's sample order was played,
the same as in every other such recording (#530). Reported by every conversion, never passed off
as a per-session order. A sidecar that cannot be read is left to the converter to report.
Source code in src/ssvep/io/bids.py
is_pilot_session ¶
Whether a recording's sidecar (a run's *.session.json or an answer's _beh.json)
declares it a pilot / development session. Declared at lock by the operator, never inferred.
Two declarations count, and either is enough, because this is read only to restrict (a
contrast excludes the recording, the sessions table marks it, the demographics fill skips it,
an export holds it back): its data-use stamp's purpose is pilot (#286 P3, the one source of
truth since), or the older session_position.pilot is true (#161, no longer written). Not
validated beyond that: a malformed stamp that says pilot still excludes, and every gate that
grants something reads the stamp strictly (:func:ssvep.runtime.data_use.data_use_of). Strict
about types: only the string "pilot" and the JSON true count, never "true" or 1.
Source code in src/ssvep/io/bids.py
pilot_runs ¶
{"sub-XXX": {"ses-YYY": [whether each run is a pilot / development session]}}.
A sidecar that cannot be read is left out: it declares nothing (and cannot be converted either).
Source code in src/ssvep/io/bids.py
pilot_sessions ¶
{"sub-XXX": {"ses-YYY", ...}}: the sessions :func:pilot_runs says are pilot /
development sessions (#161) — a session is pilot when any of its recordings declares pilot
(:func:is_pilot_session: the stamp's purpose, or the legacy session_position.pilot), the same test :func:write_session_types uses for session_type.
The one place that turns per-recording declarations into per-session ones, so the sessions table
and the demographics fill (:func:ssvep.io.participants.resolved_demographics, #290 follow-up)
cannot disagree about which sessions are pilot. A session with no readable recording declares
nothing, so it is not in the result.
Source code in src/ssvep/io/bids.py
purpose_runs ¶
{"sub-XXX": {"ses-YYY": [each run's purpose]}} (:func:recording_purpose), keyed like
:func:pilot_runs. A sidecar that cannot be read is left out: it declares nothing.
Source code in src/ssvep/io/bids.py
dataset_study ¶
{STUDY_KEY: id} for a study's root (:func:ssvep.runtime.study.root_study), {} for
the implicit study — so a dataset with no study is rendered byte for byte as before. A reader
of the dataset alone, without its marker or the site profile, can then tell which study it is.
Source code in src/ssvep/io/bids.py
dataset_purposes ¶
Sorted purposes of every recording under sourcedata/ (:func:recording_purpose), with
:data:PURPOSE_UNSTAMPED among them when any recording has no readable stamp: what
:data:PURPOSES_KEY says. Purpose words only: no dates, no codes, no counts.
Source code in src/ssvep/io/bids.py
not_study_sessions ¶
Every session under bids_root that is not study data, and why (:class:NotStudySessions).
The one "pilot / excluded session" rule (#400): export holds these sessions back under
both purposes, the demographics fill skips their answers, session_type labels them, and a
contrast leaves their recordings out. A session is held back when:
- it was locked as pilot: any of its recordings declares pilot (:func:
is_pilot_session: the data-use stamp's purpose, or the oldersession_position.pilot, read only as a restriction). Read two ways, and either counts: each XDF's sidecar, keyed by the recording's file name (:func:pilot_runs), and every sidecar in the session's folder, keyed by the folder: each*.session.json(a rescued one, or one whose XDF is gone, included) and each questionnaire answer's*_beh.json, which carries the same stamp; - it was declared pilot or test afterwards: its declarations file gives it a
group(:mod:ssvep.io.declarations, #384); - its declarations cannot be read, so they may say pilot (fail-closed), including when no declaration in the dataset can be read at all.
refs (from :func:discover_runs) and resolved (from
:func:ssvep.io.declarations.resolve) save reading them again when a caller already has them.
Source code in src/ssvep/io/bids.py
session_types ¶
{sub: {ses: session_type}} for every session in purposes (:func:purpose_runs) and
every session held holds back, recording or not.
The recordings' purpose (any pilot recording makes the session pilot, as it does for
export and the demographics fill; several other purposes are joined by +; a session with no
known purpose is n/a), with _declared_<group> appended when the session's declarations
give it a group afterwards (#400): a declared pilot is labelled, and the purpose it was locked
with is kept beside the label. A held-back session with no recording (a pilot questionnaire
answer saved before run 1, or only a declaration) is pilot or declared_<group>.
Source code in src/ssvep/io/bids.py
write_session_types ¶
Write session_type into each converted participant's sub-XXX_sessions.tsv (#161,
286 P3d, #400).¶
session_type is derived on every conversion from the recordings and their declarations
(:func:session_types): their stamped purpose, pilot for a session with a pilot /
development recording, and _declared_<group> appended for a session declared pilot / test
afterwards. purposes is :func:purpose_runs (None: only runs' pilot flags are
known, so a non-pilot session is n/a) and held is :func:not_study_sessions. Written
for every participant whose sub-XXX/ exists (converted). Every other column and row is
kept; a session missing from the file gets a row. A different value already in the cell is
replaced, and reported, except by n/a or unstamped: neither says anything was derived
(an unstamped recording is not even converted here), so a hand-set value stays. Returns notes.
Source code in src/ssvep/io/bids.py
1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 | |
convert_dataset ¶
End-to-end: migrate legacy layout → convert every sourcedata run → write dataset metadata.
Each run is labelled from its own manifest (:func:task_of); task forces one label onto
every run and exists only for tests/legacy callers. A dataset can hold more than one paradigm, so
a task-<x>_events.json is written for every task actually present.
A run its data-use stamp does not let this machine convert (:func:conversion_refusal) is
skipped, logged with its reason, and named in report.refused when a report is passed
(the conversion scripts print it); the others are converted. Only a profile mismatch refuses
the whole dataset (#298), because that one would stamp every run wrongly.