Skip to main content

Model manifest

Every published model ships ephysatlas_model.json — the publication contract. It is a discriminated union keyed on model_class that normalises every model family into one shape, which is what lets a single load_pretrained call serve them all.

A model directory without a manifest is not loadable. The manifest is also the only record of settings that cannot be recovered from the weights, such as the spatial encoder’s neighbourhood radius.

ibleatools only ever reads this file: read_manifest parses it, resolve_model fetches the directory, and verify_checksums validates the bytes.

model = load_pretrained(
    "int-brain-lab/ea-decoder-channel-xgboost", revision="2026_W42"
)
print(model.index)   # the parsed manifest

Common blocks

Every manifest, regardless of family, carries these.

key type meaning
model_id str Human-readable release id, e.g. 2026_W42_Cosmos_flimsy-thistle-cat
task str What the model does: region-classification, spatial-encoding, unit-encoding, …
model_class str The dispatch key. Selects the wrapper class
vintage str The release this model belongs to
granularity str channel or unit
method str Model type, e.g. xgboost, transformer, hierarchical_gmm
artifacts dict Role → filename(s), for every published file the loader needs
inputs dict Row identity and required columns
outputs dict What predict returns
config dict Family-specific settings
training dict Seed, dataset sizes, training-set hash
environment dict Package versions at training time

model_class dispatch

load_pretrained maps model_class to a wrapper 1:1. Each family’s trainer writes a different flavour of name — the encoders and the probe transformer record the bare class name, the region classifier the fully-qualified XGBoost class:

model_class wrapper
xgboost.sklearn.XGBClassifier RegionClassifier
ProbeTransformer ProbeTransformerClassifier
NeighborInpaintingModel SpatialEncoder
UnitAutoencoder UnitEncoder

An unrecognised value raises ValueError listing the known ones and suggesting an upgrade, so a model published by a newer ibleatools than yours raises an error instead of loading partially.

environment

The versions the model was trained with. This is the reference to compare against when selftest fails. The region decoder 2026_W42:

{
  "python": "3.12.12",
  "xgboost": "3.4.1",
  "scikit_learn": "1.8.0",
  "numpy": "2.5.4",
  "pandas": "2.3.3",
  "torch": "2.14.1",
  "ephysatlas": "0.9.0",
  "iblatlas": "1.3.0"
}

Region-classification manifests

task: "region-classification", model_class: "xgboost.sklearn.XGBClassifier".

{
  "model_id": "2026_W42_Cosmos_flimsy-thistle-cat",
  "task": "region-classification",
  "model_class": "xgboost.sklearn.XGBClassifier",
  "vintage": "2026_W42",
  "granularity": "channel",
  "method": "xgboost",
  "artifacts": {
    "weights": "model.ubj",
    "folds": ["FOLD00", "FOLD01", "FOLD02", "FOLD03", "FOLD04"],
    "split": "split.json"
  },
  "inputs": {
    "table": "raw_ephys_features_denoised.pqt",
    "index": ["pid", "channel"],
    "features": ["rms_lf", "psd_lfp", "..."],
    "feature_order_sha256": "7493de89a612e7e348ca2f077898dc599603bf1ba6c73803507ffef66c3f23bb"
  },
  "outputs": {
    "kind": "categorical",
    "columns": ["predicted_acronym", "predicted_atlas_id",
                "prediction_probability", "fold_agreement"]
  },
  "config": {
    "classes": [0, 313, 315, 512, 549, 623, 698, 703, 997, 1065, 1089, 1097, 2000],
    "class_acronyms": ["void", "MB", "Isocortex", "CB", "TH", "CNU", "OLF",
                       "CTXsp", "root", "HB", "HPF", "HY", "void_fluid"],
    "region_map": "Cosmos",
    "accuracy": 0.6123792770419426
  }
}
key meaning
artifacts.weights The all-data model, used by estimator="global"
artifacts.folds Fold subdirectories under folds/, averaged by estimator="ensemble"
artifacts.split The insertion split used in training
inputs.table Which published table the features must come from — the denoised one
inputs.features The required columns, in order (48 for 2026_W42)
config.classes Allen structure ids, index-aligned with class_acronyms
config.region_map Region ontology of the classes, e.g. Cosmos
config.accuracy Held-out accuracy at training time

A fold is loadable when its weights file is present. Folds ship weights only and carry no manifest of their own, so the loader keys on the weights file, and every fold that has weights is loaded into the ensemble.

The probe transformer

task: "region-classification-transformer", model_class: "ProbeTransformer". The same classes and region map, with a whole-probe model per seed instead of folds:

{
  "model_id": "ea-decoder-channel-transformer",
  "task": "region-classification-transformer",
  "model_class": "ProbeTransformer",
  "vintage": "2026_W42",
  "granularity": "channel",
  "artifacts": {
    "weights": "best_model.pt",
    "scaler": "feature_scaler.npz",
    "seeds": ["SEED00", "SEED01", "SEED02", "SEED03", "SEED04"]
  },
  "inputs": {
    "index": ["pid", "channel"],
    "features": ["alpha_mean", "alpha_std", "..."],
    "feature_order_sha256": "49a1fc2d942c7187f1f2b050526e95dc85a91d3e17999d88abbc0b5d86816753",
    "position_column": "axial_um"
  },
  "outputs": {
    "kind": "categorical",
    "columns": ["predicted_acronym", "predicted_atlas_id",
                "prediction_probability", "seed_agreement"]
  },
  "config": {
    "classes": [0, 313, "..."],
    "class_acronyms": ["void", "MB", "..."],
    "region_map": "Cosmos",
    "model_config": {"d_model": 128, "n_heads": 4, "n_transformer_layers": 4,
                     "dropout": 0.3, "n_features": 51, "n_classes": 13}
  }
}

inputs.position_column is the depth the transformer uses as its positional signal, read alongside the 51 features. estimator="ensemble" averages the seeds; "global" uses the first one.

Spatial-encoding manifests

task: "spatial-encoding", model_class: "NeighborInpaintingModel".

{
  "model_id": "2026_W42_encoder",
  "task": "spatial-encoding",
  "model_class": "NeighborInpaintingModel",
  "vintage": "2026_W42",
  "granularity": "channel",
  "method": "transformer",
  "artifacts": {
    "weights": "spatial_encoder.pt",
    "context": ["agea_vol_pca.npy", "merfish_vol_pca.npy"],
    "confidence": "confidence_model.pt",
    "neighbor_bank": "neighbor_bank.npz",
    "stats": "preprocessing/channel_stats.npz",
    "split": "split.json",
    "results": ["results/confidence_validation.json", "results/test_r2.json"]
  },
  "inputs": {
    "table": "channels.pqt",
    "index": ["pid", "channel"],
    "columns": ["x", "y", "z"]
  },
  "outputs": {
    "kind": "continuous",
    "columns": ["rms_lf", "psd_delta", "..."],
    "feature_order_sha256": "06be1cc9256bcb6bd85b0b45e9e1769f35ef417bf6ee9f0dc8d5603f947163c6"
  },
  "config": {
    "architecture": {"f_ctx": 100, "f_ephys": 43, "f_out": 43, "d_model": 128,
                     "nhead": 8, "depth": 2, "drop": 0.15, "use_positions": false},
    "neighbourhood": {"radius_um": 500.0, "m_max": 8,
                      "selection": "nearest", "allow_same_probe": false},
    "context": {"n_cell_pcs": 50, "n_gene_pcs": 50, "voxel_um": 200, "sources": {"...": "..."}}
  }
}

Note the direction: inputs.columns are coordinates and outputs.columns are features — the reverse of the classifier.

The weights alone are not a usable model. It also needs the frozen PCA context volumes and a bank of training-channel features to draw neighbours from, neither of which can be recomputed downstream:

role why it ships
weights The encoder itself
context Frozen AGEA + MERFISH PCA volumes, sampled at the query coordinates
neighbor_bank Real features of training channels, to draw neighbours from
confidence The per-probe confidence model
stats The training statistics, including the per-feature clipping thresholds
split The insertion split used in training
results Validation metrics, for the record

config.prediction, when a release has it, says how the release predicts: trilinear_context and neighbour_draws, the defaults of SpatialEncoder.predict. Without it, a release predicts from the nearest voxel and the nearest neighbours.

config.neighbourhood is not reconstructible from the weights, so the manifest is the only record of it. allow_same_probe: false means neighbours come from other probes only, so a prediction for a probe already in the bank does not use that probe’s own features. architecture.use_positions: false means the model was trained without the position input; an ephysatlas that predates the option cannot load it.

Unit-encoding manifests

task: "unit-encoding", model_class: "UnitAutoencoder".

{
  "model_id": "2026_W42_unit",
  "task": "unit-encoding",
  "model_class": "UnitAutoencoder",
  "vintage": "2026_W42",
  "granularity": "unit",
  "method": "hierarchical_gmm",
  "artifacts": {
    "autoencoder": "autoencoder.pt",
    "config": "config.json",
    "scaler": "shared_latent_scaler.joblib",
    "types": "types_gmm.joblib",
    "subtypes": "subtypes_gmm.joblib",
    "subtype_parent": "subtype_parent.npy",
    "context_transform": "context_transform.joblib",
    "type_weights": "context_weights_types.pt",
    "subtype_weights": "context_weights_subtypes.pt",
    "members": "members.npz",
    "stats": "preprocessing/unit_stats.npz",
    "split": "split.json",
    "context": ["agea_vol_pca.npy", "merfish_vol_pca.npy"],
    "results": ["results/summary.json"]
  },
  "inputs": {
    "index": ["pid", "cluster"],
    "columns": ["x", "y", "z"],
    "modalities": ["waveform", "acg", "stpc"]
  },
  "outputs": {
    "kind": "continuous",
    "columns": ["depolarisation_slope", "recovery_slope", "..."],
    "feature_order_sha256": "8536d8c5c99959110df046905224cf91de680434287153131d3ba85eaed968af",
    "latent_dim": 60
  },
  "config": {
    "architecture": {"modalities": ["waveform", "acg", "stpc"], "modality_latent_dim": 20,
                     "latent_dim": 60, "n_types": 30, "n_subtypes": 300,
                     "max_subtypes_per_type": 10, "min_units_per_subtype": 20},
    "context": {"n_cell_pcs": 50, "n_gene_pcs": 50,
                "source_model": "int-brain-lab/ea-encoder-channel", "data_vintage": "2026_W41"}
  }
}

inputs.columns are again coordinates: predict maps positions to the expected unit phenotype, the 10 outputs.columns. inputs.modalities are what encode takes for a unit. The artifacts split the model into its parts:

  • the autoencoder and its latent scaler;
  • the type and sub-type mixtures, and the parent type of each sub-type;
  • the context networks for the type and sub-type weights;
  • the training members each sub-type is read out through.

config.context.source_model names the spatial-encoder release whose context volumes the unit model shares.

Feature-order digests

Where a manifest carries feature_order_sha256, the loader re-hashes the feature list and compares. A mismatch raises ValueError.

This catches a manifest that has been edited or reordered — which would otherwise feed the model a correctly-named but wrongly-ordered feature matrix and produce plausible, completely wrong numbers.

import ephysatlas.model_registry as mr

digest = mr.feature_order_sha256(model.inputs["features"])
mr.validate_feature_order(
    model.inputs["features"], model.inputs.get("feature_order_sha256")
)

checksums.json

Published alongside the manifest: a per-file digest of everything in the release, verified on load, so a truncated download or a file left out at publish time raises an error.

Supported hashers are blake2b, md5 and sha1.

Files edited on the Hub after publication are excluded, so they never cause a spurious failure: checksums.json itself (it cannot cover itself), README.md (the model card), LICENSE, .gitattributes, and the Hub’s .git/ and .cache/ trees.

from pathlib import Path
import ephysatlas.model_registry as mr

# missing_ok=False is what load_pretrained uses for a local directory
mr.verify_checksums(Path("/data/models/ea-decoder-2026_W42"), missing_ok=False)

example/ — the golden files

A release may ship an example/ directory:

example/
├── features_sample.parquet      a small input the model can run on
└── expected_predictions.parquet the output the publisher got from it

The unit encoder ships positions and synthetic units instead (positions_sample.parquet, units_sample.npz, expected_predictions.parquet, expected_latents.npy), so its self-test covers both predict and encode.

This is what selftest uses, and what makes the tutorial runnable with no data of your own. selftest raises FileNotFoundError when a model ships no example/.

Tolerances differ by family, for a reason:

family tolerance why
RegionClassifier rtol=1e-5 Tree ensembles are near-exactly reproducible
ProbeTransformerClassifier rtol=1e-4, atol=1e-5 float32 network outputs, as below
SpatialEncoder rtol=1e-4, atol=1e-5 float32 transformer outputs drift ~1e-4 from BLAS and library differences. atol guards near-zero outputs, which no relative bound can
UnitEncoder rtol=1e-4, atol=1e-5 float32 network outputs, as above

All are far tighter than real corruption or a wrong model, which drift by orders of magnitude more.

Reading a manifest without loading a model

from pathlib import Path
import ephysatlas.model_registry as mr

manifest = mr.read_manifest(Path("/data/models/ea-decoder-2026_W42"))

# A flattened summary of the common fields
print(mr.meta_from_manifest(manifest))

# Class ids -> acronyms in the manifest's region map
print(mr.class_acronyms(manifest["config"]["classes"],
                        manifest["config"]["region_map"]))

read_manifest returns None when there is no manifest, rather than raising — the wrappers turn that into an explicit error.

See also