Release notes#
coco-pipe 0.0.3 (2026-10-11)#
Bug fixes#
BIDSDatasetkeeps the event codes of a dict passed asevent_id: they are the codes inyand inmeta["event_id"]. The events were renumbered from 1 in their order of appearance, so trials could carry another code than the one asked for.
coco-pipe 0.0.2 (2026-10-10)#
Bug fixes#
Reports render from a PyPI install again. The 0.0.1 wheel and sdist held only
base.htmlandsection.htmlof the report templates, so rendering any report raisedjinja2.exceptions.TemplateNotFound: 'static/tailwind_config.html'; the templates underpartials/andstatic/are now packaged. Installs from a source checkout were not affected.
coco-pipe 0.0.1 (2026-10-10)#
Features#
A tuning grid can address one field of a dict parameter as
<parameter>__<field>, for examplelora__r,lora__alphaortrainer__lron a fine-tuned model. Fields left out of the grid keep their configured value.When the exact variance participation ratio would need a feature Gram matrix above
participation_ratio_max_gram_bytes,streamed_variance_decomposition_reportnow estimates it (Hutchinson’s estimator, 256 random probes, two passes over the batches) instead of skipping it. Those rows have status"estimated"and give the relative standard error inreason.CVConfig.score_by_groupscores each fold on one prediction per group: the test predictions of a group are averaged before the metrics are computed. Predictions are still stored per sample.TrainerConfighas four new fine-tuning options, all off by default:lr_warmup_epochs(linear warmup, then a cosine decay),accumulate_grad_batches(average the gradients of several batches before each optimizer step),focal_gamma(focal loss instead of cross-entropy for classification) andprobe_epochswithprobe_lr(train the head alone first, then train astrain_modedescribes).
Bug fixes#
A decoding sweep run without a
primary_masknow readsSUCCESSwhen every unit succeeded; every successful unit counts as primary. It used to readPARTIAL.A decoding unit now writes two files instead of up to sixteen:
result.joblibandrun_manifest.json. The twelve per-unit CSV tables,config_used.yamland the_SUCCESS/_FAILEDmarkers are gone: every table is read back from the result withExperimentResult.load(...).get_*(), and the manifest holds the config, its hash, the status and the summary rows a resumed sweep needs.ExperimentResult.exportno longer takesformats.A fitted
TopologicalAEReducercan be saved and loaded: its PyTorch modules were classes defined inside a function, which pickle cannot find. They now live in a module that is imported only when the reducer is fitted, so torch stays optional.A permutation assessment no longer fails when
run()was given neithergroupsnorsample_metadata: every permutation re-ran the experiment with an empty metadata table, whichrun()rejected.A permutation assessment whose inference unit holds more than one class now raises a clear error. Each unit used to take the label of its first sample, which could leave every permutation with a single class and fail later with an unrelated KeyError.
A test fold with a single class no longer stops a classification run unless a requested metric needs both classes (ROC-AUC, average precision and other score- or probability-based metrics); accuracy-type metrics are computed as usual, so permutation tests on small grouped data complete. When a model fails on permuted labels, the assessment raises with that model’s error instead of
KeyError: 'Model', and a model that fails inside a decoding unit gets astatus="failed"record instead of disappearing from the sweep.BENDR embeddings are now the mean of its contextualized time tokens. They used to be the output at the start token, which BENDR’s pretraining never trained, so every window got nearly the same embedding (one effective dimension on real EEG; 14 now).
validate_real_checkpointsembeds several different windows and reports a model whose embeddings barely change as"degenerate".Bootstrap confidence intervals in the decoding statistics are 6 to 22 times faster, with identical results: each resample now takes its rows from one table by position instead of concatenating a frame per sampled unit. At epoch scale this step took most of a unit’s run time. When a sweep has fewer units than workers, each unit now gets an equal share of the job budget instead of one worker.
Corrected band outputs (
corrected_absolute_power,corrected_log_absolute_power,corrected_relative_power) areNaNfor a band with no fitted specparam peak centred inside it. They used to report the tail of a neighbouring peak, or zero (log-15.7) when the fit had no peak at all.Corrected band ratios are removed: the
corrected_ratiosband output, and theagg_band_corr_ratio_*columns thatadd_aggregated_band_ratios/build_descriptor_tablesadded by default (with theirprefixes/ratio_prefixesarguments). A ratio of periodic powers mixes two peaks and is undefined when either band has none; use the corrected band powers and the aperiodic exponent as separate descriptors. Ratios of uncorrected band power are unchanged.Fine-tuning now trains with AdamW. The training loop never set an optimizer, so every fine-tune ran skorch’s default, plain SGD, whatever the config said.
TrainerConfiggainslrandweight_decay, and theoptimizerfield ofNeuralFineTuneConfig, which was ignored apart from itslr, is removed.Fine-tuning without groups now honours
validation_fraction:0trains on all windows and any other value holds out that fraction (stratified for classification). It used to fall back to skorch’s default 5-fold split whatever the fraction. With groups (as inExperiment) nothing changes.Fix HTML report rendering: 2-D Plotly arrays (heatmaps) keep their shape instead of being flattened, plot containers grow to the figure’s own height so faceted grids are no longer clipped, the Plotly modebar no longer stacks over legends, table floats are shown to four significant digits (CSV export keeps full precision), markdown tables are styled, numeric heatmap labels stay numeric, and
markdownis now a dependency so report text is rendered instead of shown as escaped source.Fix robust outlier scoring for columns whose MAD is zero (over half the values equal the median, e.g. quantised counts): every non-median value used to get an infinite z-score and count as an outlier. Such columns now fall back to the mean absolute deviation (modified z-score), so only genuinely extreme values are flagged.
LoRA fine-tuning of REVE works: the default target
"all-linear"was split into single characters, and the adapter wrapper did not pass REVE’s two inputs through.Make decoding comparisons trustworthy: a resumed unit is now checked against a fingerprint of its exact input data (not only its config), so a changed table or row set never reuses a stale fit; every decoding record carries its CV design,
n_samples,n_groupsand acohort_signatureof the samples it was scored on; the head-to-head report treats a group with missing or differing signatures as not comparable instead of computing unchecked deltas or reporting a falsenan | nan | nanmismatch; paired deltas now come with a paired permutation p-value (newpaired_prediction_test, BH-corrected), paired by sample rather than by fold; andrun_paired_permutation_assessmentrefuses runs that were not scored on the same samples instead of silently testing their overlap.Reading saved decoding sweeps is easier and touches fewer files.
read_sweep(root)returns aSweepwith the results table, and loads a unit’s result or predictions only when asked. The sweep report no longer opens every unit’s result to rebuild summaries the sweep table already holds: results are read only for the sections that draw from them. A streaming sweep saves its inventory after every batch and marks the run_PARTIAL, so a job that breaks leaves a readable record. Building the predictions table of a result is about three times faster.Removed the IVIS reducer (
IVISReducer,IVISConfig, theIVISmethod) and theivisextra. ivis installs only from git, which PyPI does not accept as a dependency, and it needs annoy. UseParametricUMAPfor a reducer that transforms new samples.SignalJEPA inputs are resampled to 128 Hz, the rate it was pretrained at according to braindecode and its checkpoint, instead of 200 Hz.
SignalJEPA loads again with braindecode 1.5, for embeddings and for fine-tuning: the model was built without the window length its head needs. The real-checkpoint checks now cover all eight backbones.
Time-resolved permutation assessments with a bootstrap confidence interval no longer fail: scoring accuracy per time point pivoted on the inferential unit, which a bootstrap resample repeats. It is now a mean per time point.
BIDSDatasetkeeps MNE and mne-bids quiet for the whole load: per-callverbosearguments changed the process-wide log level while other threads were reading, which let warnings through at random.BIDSDatasetno longer skips a recording that fails to load without a trace: it logs a warning with the reason, lists the recording underload_failuresin the container metadata, and names the first failure in the error raised when nothing could be loaded.BIDSDatasetnow finds recordings withmne_bids.find_matching_pathsand identifies each one by its BIDS entities (e.g.sub-0001_ses-01_task-rest_run-01): this is the newrecording_idcoordinate, and observation IDs are<recording_id>_epoch-<i>instead of<subject>_<session>_run-<run>_<i>. Pre-computed epochs are matched on exact entities, with a newdescriptionoption; when several files match one recording an error is raised, where the first match of a pattern used to be read (anotherdescvariant, orrun-10forrun-1).mne,mne-bids>=0.20andfilelock>=3.20.4are now core dependencies.BIDSDatasetnow matches subject metadata to recordings on normalized subject labels, so1,01,0001andsub-0001refer to the same subject inparticipants.tsvand insubject_metadata_df. Aparticipants.tsvthat cannot be read now raises instead of being ignored.BIDSDatasetnow reads every recording asmne.Epochs(mne_bids.read_epochs_bidsfor pre-computed epochs,mne_bids.read_raw_bidsfor raw files) and stacks them withmne.concatenate_epochs. Recordings with different channels, sampling rates or epoch lengths now raise an error instead of being stacked, dropped or cropped; the same channels in another order are reordered. Thecontinuousloading mode, evoked files and thedrop_short_epochsoption are removed, and raw recordings needwindow_lengthorevent_id.BIDSDatasetnumbers each epoch by its position in its recording (newepochcoordinate), so an observation ID no longer depends on which events are selected. Embedding observation IDs use the stored window number and no longer include the condition, so they match the IDs of the loader and of descriptors;FoundationEmbeddingExtractor.extracttakes awindow_index.BIDSDatasetstacks recordings that number the same event differently or mark different bad channels, whichmne.concatenate_epochsrefused (it failed on real clinical recordings). Each event name gets one code across recordings, recorded inmeta["event_id"]; each recording’s bad channels are kept in abad_channelscoordinate. Recordings are first opened without their data, to check them and size the output (MNE still checks and stacks events and metadata), then each one’s data is read straight into its rows of the output array, so float64 copies of all recordings are no longer held at once. Newdtypeoption (default"float32", which halves the returned array) andn_jobsoption to read recordings in threads (about 2x faster reading with 8 threads on a cluster file system); both are also inBIDSConfig. A recording with none of the requestedevent_idevents is skipped without a warning and listed inmeta["skipped_no_matching_events"]; it used to be reported as a load failure.DataContainer.savenow writes plain arrays and dictionaries (the data, each coordinate with its dimension,y,idsandmeta) instead of a pickled instance, so a saved container can still be read after the class changes, and every coordinate comes back on the dimension it was on.DataContainer.loadraisesValueErrorfor a file that does not hold a saved container. Files written by earlier versions are not readable.DataContaineris now a thin layer over onexarray.DataArray(container.data, also returned byto_xarray()); xarray is a new core dependency. Every coordinate,yandidsbelongs to one dimension, fixed when the container is built, so slicing can no longer attach a coordinate to the wrong axis. A coordinate,yoridsof the wrong length raises,iselraises on an unknown dimension, and a value that fits no dimension is stored inmeta.X,dims,coords,yandidsare read-only views: usecontainer.replace(...)to change them (coordscan no longer be edited in place, anddataclasses.replaceno longer applies). The unusedcenter,zscore,rms_scale,baseline_correctionandcombine_coordsmethods are removed.concatandselectrun on xarray:selectnow raisesKeyErrorfor a key that is not a coordinate, where it used to warn and return everything, and itsfuzzyoption is removed.flatten,stackandunstackalso run on xarray;stacknow expandsy,idsand per-observation coordinates whatever the name of the new dimension.aggregate,with_features,obs_table,observation_frameandfeature_schemaread from the array too;aggregatekeeps the coordinates of the other dimensions (e.g.feature_family), which it used to drop.EmbeddingDataset(andload_dataon an embedding file) can load a single file; it used to search the file path as a directory and find nothing.EmbeddingDataset,EmbeddingConfigandload_datadefaultdimsto("feature",); the old("obs", "feature")default, withobsadded again in front, made a default load of an(n_obs, n_features)file fail.EmbeddingDataset,EmbeddingConfigandload_datano longer taketask,runandprocessing, which built asub-*_task-*_run-*_embeddings<processing>.pklglob by hand; pass that glob aspatterninstead.ExperimentResult.get_search_resultsnow reports the rank, mean and standard deviation of each tuning candidate; these columns were always empty.ExperimentResult.get_statistical_assessmentnow returns the bootstrap confidence interval (CILower,CIUpper) it already computed and stored.SpatialWhiteneris removed: nothing used it, and pyriemann, already a dependency, provides whitening.StatisticalAssessmentConfig.unit_of_inferencenow applies to the assessment run insideExperiment.run; it was only read for paired comparisons, so every permutation and binomial assessment counted samples whatever it said.group_meanandgroup_majoritycount CV groups (averaging or voting within each),customcountscustom_unit_column, matched ignoring case. A unit set here takes precedence overrun(inferential_unit=...).TrimapReducerruns TriMap’s PyTorch implementation (trimap.TorchTRIMAP, trimap 1.2 or newer) on the CPU by default. The defaulttrimap.TRIMAPfinds neighbours with annoy, whose search returns only the query point on Apple silicon, so those embeddings mixed clusters.random_statenow reaches TriMap, anddevice/knn_backendcan be passed through.import coco_pipeworks with a plain install: statsmodels was imported at load time but declared only in extras. coco-pipe no longer uses statsmodels; FDR correction usesscipy.stats.false_discovery_control, andcompare_models(correction=...)accepts"fdr_bh"or"none".iter_analysis_unitsnow returns an iterator: arguments are still checked when it is called, but each unit’s container is built only when it is reached, so peak memory no longer grows with the number of units (156 MB to 2 MB for 418 units of a 46,000 x 836 table). Uselist(...)where a list is needed.DataContainer.aggregateandaggregate_groupskeep float inputs in their own dtype instead of casting to float64, so float32 embeddings stay float32.load_embedding_derivativesreads each file’s arrays once instead of once per window: 40 files of 60 windows load in 60 ms instead of 365 ms, with identical output.coco-pipe requires MNE 1.13 or newer and uses its
colin27_1020/colin27_1005montages, the new names ofstandard_1020/standard_1005(same channels and positions), so MNE no longer warns that the old names are deprecated. Montage defaults incoco_pipe.vizand the decoding reports are nowcolin27_*.
Miscellaneous#
The foundation-model extras now require
braindecode>=1.8.1(the<1.6cap is gone); all eight backbones were checked against 1.8.1.