Release notes#

coco-pipe 0.0.3 (2026-10-11)#

Bug fixes#

  • BIDSDataset keeps the event codes of a dict passed as event_id: they are the codes in y and in meta["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.html and section.html of the report templates, so rendering any report raised jinja2.exceptions.TemplateNotFound: 'static/tailwind_config.html'; the templates under partials/ and static/ 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 example lora__r, lora__alpha or trainer__lr on 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_report now 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 in reason.

  • CVConfig.score_by_group scores 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.

  • TrainerConfig has 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) and probe_epochs with probe_lr (train the head alone first, then train as train_mode describes).

Bug fixes#

  • A decoding sweep run without a primary_mask now reads SUCCESS when every unit succeeded; every successful unit counts as primary. It used to read PARTIAL.

  • A decoding unit now writes two files instead of up to sixteen: result.joblib and run_manifest.json. The twelve per-unit CSV tables, config_used.yaml and the _SUCCESS / _FAILED markers are gone: every table is read back from the result with ExperimentResult.load(...).get_*(), and the manifest holds the config, its hash, the status and the summary rows a resumed sweep needs. ExperimentResult.export no longer takes formats.

  • A fitted TopologicalAEReducer can 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 neither groups nor sample_metadata: every permutation re-ran the experiment with an empty metadata table, which run() 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 a status="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_checkpoints embeds 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) are NaN for 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_ratios band output, and the agg_band_corr_ratio_* columns that add_aggregated_band_ratios / build_descriptor_tables added by default (with their prefixes / ratio_prefixes arguments). 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. TrainerConfig gains lr and weight_decay, and the optimizer field of NeuralFineTuneConfig, which was ignored apart from its lr, is removed.

  • Fine-tuning without groups now honours validation_fraction: 0 trains 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 in Experiment) 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 markdown is 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_groups and a cohort_signature of 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 false nan | nan | nan mismatch; paired deltas now come with a paired permutation p-value (new paired_prediction_test, BH-corrected), paired by sample rather than by fold; and run_paired_permutation_assessment refuses 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 a Sweep with 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, the IVIS method) and the ivis extra. ivis installs only from git, which PyPI does not accept as a dependency, and it needs annoy. Use ParametricUMAP for 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.

  • BIDSDataset keeps MNE and mne-bids quiet for the whole load: per-call verbose arguments changed the process-wide log level while other threads were reading, which let warnings through at random.

  • BIDSDataset no longer skips a recording that fails to load without a trace: it logs a warning with the reason, lists the recording under load_failures in the container metadata, and names the first failure in the error raised when nothing could be loaded.

  • BIDSDataset now finds recordings with mne_bids.find_matching_paths and identifies each one by its BIDS entities (e.g. sub-0001_ses-01_task-rest_run-01): this is the new recording_id coordinate, 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 new description option; when several files match one recording an error is raised, where the first match of a pattern used to be read (another desc variant, or run-10 for run-1). mne, mne-bids>=0.20 and filelock>=3.20.4 are now core dependencies.

  • BIDSDataset now matches subject metadata to recordings on normalized subject labels, so 1, 01, 0001 and sub-0001 refer to the same subject in participants.tsv and in subject_metadata_df. A participants.tsv that cannot be read now raises instead of being ignored.

  • BIDSDataset now reads every recording as mne.Epochs (mne_bids.read_epochs_bids for pre-computed epochs, mne_bids.read_raw_bids for raw files) and stacks them with mne.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. The continuous loading mode, evoked files and the drop_short_epochs option are removed, and raw recordings need window_length or event_id.

  • BIDSDataset numbers each epoch by its position in its recording (new epoch coordinate), 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.extract takes a window_index.

  • BIDSDataset stacks recordings that number the same event differently or mark different bad channels, which mne.concatenate_epochs refused (it failed on real clinical recordings). Each event name gets one code across recordings, recorded in meta["event_id"]; each recording’s bad channels are kept in a bad_channels coordinate. 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. New dtype option (default "float32", which halves the returned array) and n_jobs option to read recordings in threads (about 2x faster reading with 8 threads on a cluster file system); both are also in BIDSConfig. A recording with none of the requested event_id events is skipped without a warning and listed in meta["skipped_no_matching_events"]; it used to be reported as a load failure.

  • DataContainer.save now writes plain arrays and dictionaries (the data, each coordinate with its dimension, y, ids and meta) 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.load raises ValueError for a file that does not hold a saved container. Files written by earlier versions are not readable.

  • DataContainer is now a thin layer over one xarray.DataArray (container.data, also returned by to_xarray()); xarray is a new core dependency. Every coordinate, y and ids belongs to one dimension, fixed when the container is built, so slicing can no longer attach a coordinate to the wrong axis. A coordinate, y or ids of the wrong length raises, isel raises on an unknown dimension, and a value that fits no dimension is stored in meta. X, dims, coords, y and ids are read-only views: use container.replace(...) to change them (coords can no longer be edited in place, and dataclasses.replace no longer applies). The unused center, zscore, rms_scale, baseline_correction and combine_coords methods are removed. concat and select run on xarray: select now raises KeyError for a key that is not a coordinate, where it used to warn and return everything, and its fuzzy option is removed. flatten, stack and unstack also run on xarray; stack now expands y, ids and per-observation coordinates whatever the name of the new dimension. aggregate, with_features, obs_table, observation_frame and feature_schema read from the array too; aggregate keeps the coordinates of the other dimensions (e.g. feature_family), which it used to drop.

  • EmbeddingDataset (and load_data on an embedding file) can load a single file; it used to search the file path as a directory and find nothing.

  • EmbeddingDataset, EmbeddingConfig and load_data default dims to ("feature",); the old ("obs", "feature") default, with obs added again in front, made a default load of an (n_obs, n_features) file fail.

  • EmbeddingDataset, EmbeddingConfig and load_data no longer take task, run and processing, which built a sub-*_task-*_run-*_embeddings<processing>.pkl glob by hand; pass that glob as pattern instead.

  • ExperimentResult.get_search_results now reports the rank, mean and standard deviation of each tuning candidate; these columns were always empty.

  • ExperimentResult.get_statistical_assessment now returns the bootstrap confidence interval (CILower, CIUpper) it already computed and stored.

  • SpatialWhitener is removed: nothing used it, and pyriemann, already a dependency, provides whitening.

  • StatisticalAssessmentConfig.unit_of_inference now applies to the assessment run inside Experiment.run; it was only read for paired comparisons, so every permutation and binomial assessment counted samples whatever it said. group_mean and group_majority count CV groups (averaging or voting within each), custom counts custom_unit_column, matched ignoring case. A unit set here takes precedence over run(inferential_unit=...).

  • TrimapReducer runs TriMap’s PyTorch implementation (trimap.TorchTRIMAP, trimap 1.2 or newer) on the CPU by default. The default trimap.TRIMAP finds neighbours with annoy, whose search returns only the query point on Apple silicon, so those embeddings mixed clusters. random_state now reaches TriMap, and device / knn_backend can be passed through.

  • import coco_pipe works with a plain install: statsmodels was imported at load time but declared only in extras. coco-pipe no longer uses statsmodels; FDR correction uses scipy.stats.false_discovery_control, and compare_models(correction=...) accepts "fdr_bh" or "none".

  • iter_analysis_units now 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). Use list(...) where a list is needed. DataContainer.aggregate and aggregate_groups keep float inputs in their own dtype instead of casting to float64, so float32 embeddings stay float32.

  • load_embedding_derivatives reads 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_1005 montages, the new names of standard_1020 / standard_1005 (same channels and positions), so MNE no longer warns that the old names are deprecated. Montage defaults in coco_pipe.viz and the decoding reports are now colin27_*.

Miscellaneous#

  • The foundation-model extras now require braindecode>=1.8.1 (the <1.6 cap is gone); all eight backbones were checked against 1.8.1.