Goodfire

ching-goodfire

MAPS: a covariance probe on ESM-C difference fields, and the published artifacts to build on

MAPS (Mechanistic Atlas of Protein Sequences) scores a missense substitution by embedding the wild-type and the mutant protein with frozen ESM-C 6B, subtracting the two per-residue fields, keeping the 256 positions where they diverge most, and covariance-pooling that field into a two-class probe. On a homology-cluster-disjoint test split of 12,565 ClinVar variants, the six published layer-80 probes reach 0.9442 to 0.9463 AUROC and every 95% interval overlaps every other. The published artifact set contains no paired comparison against AlphaMissense, REVEL, CADD, PolyPhen-2 or SIFT, and neither the mean-pool nor the mutant-only ablation, so those numbers say the probe separates this split well, not that it beats the field's tools. Every number here is read out of a published file and labelled with its source: no probe was retrained, no metric recomputed from per-variant scores, and no embedding shard downloaded.

Scope: same-length single amino-acid substitutions

The probe's input is the aligned difference between the wild-type and the mutant residue field, so the method is defined only where the two sequences have the same length and differ at one position. Indels, frameshifts, stop gains, splice variants and synonymous changes are out of scope, and widening the window does not bring them in: a length change breaks the position-by-position alignment the difference field is built from. The dataset cards state the same restriction.

Labels are ClinVar clinical significance collapsed to pathogenic against benign, so they carry ClinVar's submission and ascertainment biases. The review-status star rating is the only quality signal shipped alongside them, and it is null for 7,940 carried-over rows from the earlier ProteinGym-derived clinical set.

Three design choices, two of them unmeasured

The difference field rather than the mutant embedding. One substitution moves a 6-billion-parameter model's representation slightly. Subtracting the wild-type field at the same layer isolates that move instead of asking the probe to find it inside the absolute embedding. The differences are kept raw: no per-position normalisation, because the magnitude of each mutant-minus-wild-type step is part of the signal and the probe's Newton-Schulz matrix square root handles the conditioning.

Covariance pooling rather than mean pooling. The probe forms two linear views of the field, takes their cross-covariance, and reads a factored linear head off that matrix, so it sees co-occurrence between representation dimensions rather than per-dimension averages. That is the case covariance pooling exists for. It is also the claim with no published support in this artifact set: the mean-pool version of the same probe, on the same fields, is not among the shipped runs.

Top-256 positions by cosine divergence rather than a fixed window. Positions are ranked by 1 - cosine(wild-type, mutant) and the top 256 are kept in ascending position order, so the field is a variable-length slice of the mutation's ripple through the sequence rather than a neighbourhood of the substituted residue. A protein shorter than 256 residues keeps all of them. The alternative, a fixed window, changes what "the field" means, so the choice has to be stated with any result.

Representation and probe contract

Model
biohub/ESMC-6B, frozen, hidden width 2,560
Layer
80 for the published pathogenicity probes, 78 for the annotation battery
Field
mutant minus wild type, top 256 positions by 1 - cosine divergence, raw differences
Probe
goodfire_core.probes.covariance.SequenceCovarianceProbe: d_model 2,560, 2 outputs, d_hidden 64, d_probe 128, 3 Newton-Schulz iterations, eps 0.001; 344,450 trainable parameters
Ensemble
seeds 42 and 43, averaged as pathogenic-class probabilities
Split unit
homology cluster; validation groups carved out of training clusters
Token contract
cls at token 0, residues at tokens 1..L, eos after the last residue; stored tensors keep residues only

Figure 1: The two paths through one frozen forward pass

MAPS pipeline. A wild-type and mutant sequence pair goes through
frozen ESM-C 6B. The prediction path takes layer-80 residue states, forms the mutant minus wild-type
difference field over the top 256 diverged positions, and feeds a covariance probe ensembled over two
seeds. The mechanism path takes layer-78 residue states, runs the 159-channel annotation battery, and
reads per-channel deltas as the disruption profile. Both are scored on the same cluster-disjoint test
split.

One harvest per sequence pair feeds two readouts. The prediction path produces a pathogenicity probability; the mechanism path produces a per-channel disruption profile.

In the published artifacts the two paths read different layers: pathogenicity at layer 80, annotations at layer 78. That is the combination in use, and no crossed pairing is evaluated anywhere in the release.

Gene identity is the shortcut the split has to break

Pathogenic fraction varies enormously between genes, so a probe that recognises "this looks like this protein" scores well on missense pathogenicity without learning anything about substitutions. A random variant-level split hands it exactly that, because the same protein then appears on both sides. MAPS splits by MMseqs2 sequence-identity cluster instead, so a protein and its homologs land in one split only.

SplitVariantsProteinsClusters
test12,565581494
train_existing49,7191,9221,603
train_new138,62916,54010,484

The test split shares zero proteins and zero clusters with either training pool, checked in both directions from the published tables rather than taken from an upstream report. Its label balance is 6,095 pathogenic against 6,470 benign.

Two things the split does not give. The published metric files record a 95% interval but not the unit it was resampled over, so those intervals cannot be read as gene-level intervals, and variants cluster inside genes, which makes a variant-level interval too narrow. And no per-gene AUROC distribution is published, so the pooled number cannot be checked against the possibility that a few large gene families carry it.

Published pathogenicity results at layer 80

Six probes ship: three training arms crossed with two seeds. They differ in training-pool size as well as seed, from 39,736 to 163,772 variants, so the arms are not a clean seed comparison. Held-out AUROC moves by 0.0021 across all six, inside 95% intervals each about 0.0082 wide, so nothing here separates the arms: the largest training pool does not produce the best held-out number.

RunArmSeedTraining variantsTest AUROC 95% intervalAverage precisionReported epoch
baseline_L80_cov_s42baseline4242,1220.94480.9406 to 0.94880.94372
baseline_L80_cov_s43baseline4339,7360.94470.9408 to 0.94860.94471
scaled_L80_cov_s42scaled42163,7720.94540.9414 to 0.94930.94417
scaled_L80_cov_s43scaled43157,6190.94550.9418 to 0.94930.94563
scaledbal_L80_cov_s42scaledbal4291,1800.94630.9423 to 0.95020.94409
scaledbal_L80_cov_s43scaledbal4384,1220.94420.9402 to 0.94810.93987

All six are evaluated on the same 12,565 variants with 6,095 pathogenic, and each probe has 344,450 trainable parameters. The published model card also re-derives every one of the six AUROCs from the shipped per-variant score files and reproduces the recorded value, so the metric files and the scores agree.

Figure 2: Held-out AUROC for the six published layer-80 probes

Test AUROC with its published 95% interval for each run, on the 12,565-variant cluster-disjoint split. The axis starts at 0.935, below every interval; it is not zero-based, so read the spread rather than the bar-like distances.

The spread across arms and seeds (0.9442 to 0.9463) is smaller than a single interval's width, so the three arms are indistinguishable on this split.

The earlier layer-78 pair recorded in the repository

The MAPS repository records an older pair of covariance probes at layer 78, evaluated on the same 12,565-variant test split. They belong to a different harvest with different training pools, and no run pairs the two layers under one protocol, so these numbers stand on their own rather than as a layer comparison.

Run (layer 78)SeedTraining variants Test AUROC95% intervalAverage precisionReported epoch
esmc_L78_cov_s424242,7810.94080.9366 to 0.94480.94005
esmc_L78_cov_s434339,7140.94050.9366 to 0.94450.94015

Figure 3: Validation and test AUROC across the 12 training epochs of the layer-80 runs

One panel per published run. The circled point is the epoch chosen on validation AUROC, whose test value is the reported number. The y axis starts at 0.925, below every point.

The trainer logs held-out test AUROC every epoch even though selection uses validation AUROC. In four of the six runs the test curve peaks at a different epoch, and selecting on that column instead would have added at most 0.0017 AUROC. That is the size of the trap, and the recorded metric avoids it: for all six runs the reported value equals the test column at the validation-selected epoch.

The annotation battery: 159 channels at layer 78

The mechanism half is 159 per-residue probes at layer 78: 144 binary, 14 regression and 1 multiclass channel, each a logistic or ridge estimator over a standardised residue vector. They cover InterPro domains (115 channels), functional sites (17), structure (11), post-translational modifications (8) and topology (8).

The shipped artifacts are final fits on all selected rows. They are inference tools, not held-out checkpoints, so a channel's accuracy comes from separate grouped cross-validation, never from the artifact itself. The manifest is explicit about which evidence each channel has: 18 channels carry protein- and cluster-grouped 5-fold cross-validation, 139 carry a metric from the old-versus-new comparison table, and 2 carry no evaluation entry at all. Every classifier's decision threshold is 0.5, which is scikit-learn's own rule rather than a per-channel tuned value.

ChannelProbeCluster-split AUROC95% interval Average precision
signal peptideP_SIGNAL_PEP0.99570.9918 to 0.99960.9575
transmembraneP_TRANSMEM0.97310.9667 to 0.97950.8267
buried vs exposedP_RSASA_BIN_WT0.93540.9294 to 0.94130.9104
active siteP_ACTIVE_SITE0.90600.8592 to 0.95280.5091
disulfide-bonded cysteineP_DISULFIDE0.88020.8573 to 0.90310.4426
ligand binding siteP_BINDING_SITE0.86850.8397 to 0.89720.3048
metal coordinatingP_METAL_BINDING0.83330.7824 to 0.88410.2231
inside an InterPro domainP_IN_DOMAIN0.80280.7754 to 0.83030.5298
modified residueP_MOD_RES0.78130.7432 to 0.81950.0832

Those are means over 5 homology-grouped folds on 35,609 residue rows at variant positions. Three more channels of other types land in the same table: 3-state secondary structure at 0.7505 macro F1, AlphaFold pLDDT at 0.8576 R-squared, and relative solvent accessibility at 0.7898 R-squared. A rare-positive channel such as modified residue reaches 0.7813 AUROC but only 0.0832 average precision, which is the number that matters when positives are scarce.

Figure 4: Structural channels against the stronger model-free baseline

Ten structural channels trained on all residues of the mapped proteins, each shown twice: the ESM-C layer-78 probe and the stronger of two model-free baselines, one-hot amino acid and physicochemistry. The connector's length is the lift. Spearman correlation, 5-fold homology-grouped cross-validation.

Every one of these channels gains from the embedding, by +0.1998 to +0.7465 Spearman. Contact counts, half-sphere exposure and backbone torsion are things the residue vector knows and amino-acid identity does not.

Six channels decode well and add nothing over chemistry

An earlier generation of the same battery was audited head by head against one-hot amino acid and physicochemistry on a held-out 30%-identity split. Those probes read layer 33 of the earlier, smaller ESM-2 model rather than layer 78 of ESM-C 6B, so the numbers below are not the shipped channels' scores. The pattern they establish is what carries over: raw accuracy and embedding-attributable signal are different quantities, and six channels that decode well add nothing over chemistry.

ChannelMetricProbeOne-hotPhysicochemistry LiftReading
R_COMPLEXITY_WIN10Spearman0.7580.1450.999-0.242reconstructed by physicochemistry
R_CHARGE_WIN10Spearman0.9500.1710.996-0.047reconstructed by physicochemistry
R_KD_WIN10Spearman0.9730.2491.000-0.027reconstructed by physicochemistry
R_FOLDINDEX_WTSpearman0.9730.2491.000-0.027reconstructed by physicochemistry
R_POSITION_FRACSpearman0.9750.0251.000-0.025reconstructed by physicochemistry
R_KD_WIN5Spearman0.9860.3221.000-0.014reconstructed by physicochemistry
P_SIGNAL_PEPAUROC1.0000.6730.995+0.005close to hydrophobicity
P_MOD_RESAUROC0.8930.8780.708+0.015reads residue identity
P_ACTIVE_SITEAUROC0.9450.9190.594+0.027reads residue identity
P_TRANSMEMAUROC0.9900.7020.959+0.031close to hydrophobicity
P_METAL_BINDINGAUROC0.8680.7710.616+0.097partly residue identity
P_BINDING_SITEAUROC0.8840.6510.612+0.234earns the embedding
P_RSASA_BIN_WTAUROC0.9490.6950.695+0.254earns the embedding
P_IN_DOMAINAUROC0.8860.5360.606+0.280earns the embedding
P_DISULFIDEAUROC0.9250.5430.605+0.320earns the embedding
P_SS3_WTmacro F10.7330.3720.346+0.361earns the embedding
R_PLDDT_WTSpearman0.7990.1900.409+0.390earns the embedding
R_RSASA_WTSpearman0.8780.3120.437+0.441earns the embedding

Three specifics worth carrying into any use of the battery. Active site decodes at 0.945 while one-hot amino-acid identity alone reaches 0.919, and at layer 0 of that model, before any contextual computation, it already scores 0.885 against a best-layer 0.891. That is what reading "is this a histidine, cysteine or aspartate" looks like. Transmembrane and signal peptide decode near-perfectly, but a physicochemistry-only baseline reaches 0.959 and 0.995, so most of that is hydrophobicity. And the disulfide channel's headline is inflated by conditioning: its unconditioned one-hot baseline is 0.543, but restricted to cysteines the same baseline sits at 0.500, exactly chance, so the channel has to be re-scored within cysteines to mean anything.

The disruption profile is a lossy explainer, not a predictor

The existential check for the mechanism half is whether the named-channel deltas separate pathogenic from benign on their own. In the recorded benchmark they do not, by a wide margin. The 10 informative channel deltas reach 0.645 AUROC and all 18 reach 0.669, while an ESM-1v zero-shot log-likelihood ratio, which needs no training at all, reaches 0.873 on the same variants under the same grouped protocol. Adding the whole profile on top of the zero-shot ratio moves it by +0.006, to 0.879. The best single channels are relative solvent accessibility at 0.60, domain membership at 0.59 and binding site at 0.59.

The useful contrast comes from the annotation cross-validation table: a probe trained on the mutant-minus-wild-type difference, on ESM-C layer-78 states under the same grouped cross-validation, reaches 0.9089 AUROC (5 folds, 38,960 variants). The raw difference predicts pathogenicity far better than its decomposition into named structural channels, which makes the decomposition lossy: projecting the difference onto solvent accessibility, secondary structure and site membership discards most of what separates the classes.

That does not disqualify the profile as a mechanism lens. It forbids selling it as a predictor, and it points at the missing axes: pathogenic missense are separated mainly by evolutionary constraint and folding stability, and neither conservation nor folding-stability sensitivity is in the profile. One caveat on the whole comparison: it was run on the earlier, shallower embedding generation, and no equivalent benchmark of the shipped layer-78 battery's deltas is published.

Figure 5: Channel deltas against a zero-shot baseline on the same variants

Pathogenic-versus-benign AUROC under grouped cross-validation on protein and homology cluster, for named-channel disruption profiles and for an ESM-1v zero-shot log-likelihood ratio. The reference line is the supervised difference-field probe on ESM-C layer-78 states, from the annotation cross-validation table.

The interpretable decomposition sits about 0.228 AUROC below a zero-shot baseline that needs no training, and about 0.264 below the supervised difference-field probe.

Limitations

Loading the three published artifacts

All three are public Hugging Face repositories joined by variant_id, of the form {protein_id}_{wt_aa}{pos1}{mut_aa}.

Probes: ching-goodfire/MAPS-ESMC-Probes

165 rows in manifest.csv, one per probe: 6 pathogenicity checkpoints at layer 80 and 159 annotation channels at layer 78. Read the manifest first, because the two families take different inputs and carry different kinds of evaluation, and its metric_basis column is what tells them apart. The pathogenicity checkpoints hold weights only, so rebuilding one needs goodfire_core.probes.covariance.SequenceCovarianceProbe; the annotation battery has a safetensors export (161 coefficient rows, the multiclass channel occupying three) that loads with NumPy alone and agrees with scikit-learn's own output to 7.0e-06. The original joblibs were written under scikit-learn 1.7.2 and warn under later versions.

from huggingface_hub import snapshot_download
import sys

local = snapshot_download("ching-goodfire/MAPS-ESMC-Probes")
sys.path.insert(0, local)
from load_probes import AnnotationBattery, build_diff_field, score_variant

battery = AnnotationBattery.open(
    f"{local}/annotations/l78/annotation_probes_l78.safetensors")
tracks = battery.score(embeddings_l78)          # per channel, one value per residue

field, mask = build_diff_field(wt_l80, mut_l80)  # top 256 by 1 - cosine
probability = score_variant(field, [
    f"{local}/pathogenicity/l80/scaled_L80_cov_s42.pt",
    f"{local}/pathogenicity/l80/scaled_L80_cov_s43.pt"])

Difference fields: ching-goodfire/MAPS-ClinVar-VKS-Embeddings-L80

Every stored layer-80 difference field for all 200,913 variants: the whole 12,565-variant test split and the whole 188,348-variant training pool, 262 parquet shards (17 test, 245 train), 154.0 GB on disk from 260.8 GB of raw fp16, row groups of 64 rows. Each row is self-describing: the field as a raw fp16 blob plus its shape, the kept positions, the mean difference vector, and the variant's identity, label, cluster and split. The training pool is 27.1% pathogenic, its source pools' own rate rather than a balanced draw. An earlier revision shipped a 20,000-variant sample of that pool; that sample now survives as a membership list, so anything computed on it stays reproducible.

import numpy as np
from datasets import load_dataset

ds = load_dataset("ching-goodfire/MAPS-ClinVar-VKS-Embeddings-L80", split="test", streaming=True)
row = next(iter(ds))
X = np.frombuffer(row["X"], dtype=np.float16).reshape(row["X_rows"], row["X_cols"])
idx = np.asarray(row["idx"], dtype=np.int32)

Inputs: ching-goodfire/MAPS-ClinVar-VKS-Inputs

200,913 variant rows and 17,231 wild-type sequences, with identity, position, label, split, homology cluster and ClinVar identifiers. Mutant sequences are not stored because every variant is a single substitution: splice the mutant residue into the wild-type sequence at pos1 - 1, a rule verified against the stored mutant sequences on all 200,913 rows before that column was dropped. Protein lengths run 32 to 35,991 residues, median 490, and 2,595 proteins exceed 1,022 residues, covering 91,221 variants.

EVEE, the genomic sibling

MAPS is the protein-space counterpart of EVEE (Evo Variant Effect Explorer, Goodfire with Mayo Clinic, bioRxiv 2026.04.10.717844), and the shape is the same: a covariance probe on frozen foundation-model embeddings for the prediction, supervised annotation-probe deltas for the mechanism, homology-aware evaluation for the verdict. Two differences matter for anyone porting work between them. EVEE's substrate is a genomic model (Evo 2) over a DNA window, so its scope includes variant classes MAPS cannot represent. And EVEE adds a step MAPS's published set does not have: a frontier model writes the explanation from the top disruptions, and that explanation is scored against a metadata-only baseline instead of shipped unevaluated. The figures in that work (0.997 AUROC on 839K ClinVar variants, explanations rated 3.8 against 2.8 out of 5 on a 154-variant held-out set) are that paper's, on genomic variants, and are not comparable to the protein-space numbers on this page. When a MAPS deliverable includes a written explanation, that scored-explanation step is the piece to borrow.

Three things to do next

Replicate, then run the missing ablation. The training pool, the test split and the difference fields are all public, and the probe config is fully recorded, so retraining the covariance probe from the published train split and checking the held-out number is a closed loop that needs no harvest. The same script gives the mean-pool ablation on identical inputs, which is the one measurement that would turn "covariance pooling is the right choice here" into a result. Keep the inputs raw in both arms, or the comparison is not clean.

Extend where the artifacts are silent. Three gaps are ready to close: a paired comparison against AlphaMissense and REVEL on these 12,565 variants, using their precomputed tables joined by variant; per-gene AUROC with a gene-level bootstrap, which the pooled number cannot substitute for; and the two channels the disruption profile lacks, per-position conservation and folding-stability sensitivity, which is where the analysis of the negative result points.

Apply it, within scope. Scoring your own variants needs paired wild-type and mutant layer-80 embeddings, the difference-field rule, and the two seed checkpoints of one arm averaged as probabilities. Two guards before trusting the output: restrict the input to same-length single substitutions, and check your proteins against the published homology clusters, because a protein sitting in a training cluster is not a held-out case for these probes.