Binder Filter is a reproducible GPU pipeline for prioritizing generated target-binder complexes from PocketXMol and BoltzGen. It validates and normalizes source structures, independently refolds target-binder sequences with Protenix-v2 and full ESMFold2, calculates confidence and geometry metrics, applies a binary pass/fail decision, and selects a diverse final set.
Scientific scope: Binder Filter performs computational prioritization. A pass is not proof of binding, affinity, specificity, stability, expression, solubility, permeability, activity, safety, or synthetic accessibility. Experimental validation remains required. Production molecular dynamics is deliberately outside this pipeline.
Version 0.2.0 implements a mode-specific interaction and filtering contract. All three
binder modes require median ipSAE_min >= 0.60 from both Protenix-v2 and full ESMFold2,
plus ESMFold2 median exact actifpTM >= 0.93. Miniproteins additionally require a
separate apo-fold gate. Peptide cross-model and source-pose RMSDs are limited to 1.25 A
and 1.50 A, respectively. Legacy YAML field names are rejected rather than silently
migrated.
The production run, resume, and report commands are active. They perform strict
configuration validation, source ingestion and normalization, immutable model/MSA fetch
locking, sequential dual-model GPU inference, exact artifact validation, ipSAE and exact
probability-based actifpTM where available, structural/geometry analysis, binary filtering,
ranking/diversity selection, deterministic CSV/mmCIF export, and a verified multi-page PDF.
Mock-worker output remains unmistakably non-scientific.
A clean one-candidate BoltzGen end-to-end acceptance run completed with genuine Protenix-v2 and full ESMFold2 artifacts, then resumed without changing completed-job timestamps or CSV hashes. These checks demonstrate execution and resume integrity, not biological validity.
A 10-candidate BoltzGen cyclic-peptide smoke run also completed on an A100 with 10/10 valid Protenix-v2 jobs, 10/10 valid full-ESMFold2 jobs, and explicit cyclic closure in all 20 prediction records. Its historical one-seed result identified two provisional passers, but it no longer qualifies as a scientific cyclic pass under the required 3-of-5 valid confirmation-seed and terminal-bond geometry contract. This diagnostic run caught an exporter bug that had compared the 64-bin structure distogram with the unrelated 39-bin confidence-head grid.
The completed first BoltzGen PD-L1 run in /workspace/results predates the v0.2.0 cutoff
change. Its predictions remain valid reusable evidence, but its stored pass flags describe
the older scientific hash. Do not compare those flags directly with a new v0.2.0 run.
| Source | Required metadata | Structures | Cyclic topology |
|---|---|---|---|
| PocketXMol | gen_info.csv |
Peptide/miniprotein PDB | Cyclic mode: matching _mol.sdf with closure; other modes: not required |
| BoltzGen | Metrics CSV | Generated complex PDB or mmCIF | Closure in mmCIF or explicit sidecar |
Supported binder modes are exact:
linear_peptide: 5-30 canonical residues, no polymer closure.cyclic_peptide: 5-30 canonical residues, exactly one last-C/first-N amide closure.miniprotein: 31-150 canonical residues in one binder chain.
Mode and source are required in YAML and are never inferred.
Cyclic mode additionally requires five confirmation seeds with at least three valid seeds per predictor. The explicit last-C/first-N connection must measure 1.20-1.45 A in the normalized source, every valid prediction, and the final consensus structure. A connection record with implausible coordinates is rejected.
Structure sources may be directories, direct files, glob patterns, or ZIP archives.
Archive members can be indexed directly from a ZIP central directory; full extraction is
not required. A file inside an archive can be addressed as
path/to/archive.zip::member/path/file.csv.
- Place the canonical target PDB/mmCIF and immutable generator outputs outside the Git checkout. Never copy model weights or a 30,000-structure campaign into this repository.
- For PocketXMol, provide
gen_info.csvand every generated PDB; cyclic mode also requires the matching<PDB stem>_mol.sdf. Non-cyclic PocketXMol validation does not require an SDF. For BoltzGen, provide its metrics CSV and generated complex PDB/mmCIF files; ambiguous cyclic connectivity requires a sidecar. - Copy the matching example from
configs/, set an explicitinput.source,input.mode, target chains, source columns, paths, run name, and output root. Relative paths resolve relative to the YAML file. Unknown or misspelled fields are fatal. - Run
validate, thendoctor, thenfetch. Inspectconfig.resolved.yaml,weights.lock.json, andmsa.lock.jsonbefore expensive inference. - Launch
run. If interrupted, useresume <run-directory>; do not create a second process against the same directory.
For a cyclic BoltzGen structure whose file format cannot carry connectivity, the optional sidecar is a strict YAML/JSON map from source ID (or structure filename/stem) to the source binder chain ID:
schema_version: 1
head_to_tail:
design_0001: A
design_0002.cif: AThe sidecar declares only the last-C/first-N bond. Any other binder polymer connection is rejected.
Cyclic PocketXMol validation requires one coordinate conformer, an exact heavy-atom PDB/SDF mapping within 0.05 A, and the exact expected inter-residue peptide-bond graph. BoltzGen validation matches generated target chains to the configured canonical target assembly by sequence, preserves deterministic mappings for identical target copies, and rejects unintended binder connections. Invalid source rows remain in the manifest with diagnostics.
The target MSA is queried once per unique target-chain sequence and cached by sequence SHA-256. Designed binder sequences are always single-sequence and are never sent to an MSA service.
Keep generated data and model caches outside the Git checkout:
binder-filter/ # this repository
configs/ # documented YAML examples
envs/ # three isolated Conda environments
scripts/ # noninteractive bootstrap/validation entrypoints
src/binder_filter/ # production package
tests/ # immutable small fixtures and tests
/data/binder-filter-inputs/
targets/
pocketxmol/<campaign>/
boltzgen/<campaign>/
/workspace/models/binder-filter/ # immutable snapshots and checksums
/workspace/cache/binder-filter/msa/ # target-only MSA cache
/workspace/results/<job-name>/ # run directories
Source CSV, PDB, CIF, SDF, ZIP, and model files are read-only inputs. Binder Filter does not modify them.
binder-filter/
README.md operator guide and scientific interpretation
pyproject.toml core package and exact direct dependencies
configs/ strict, runnable source-specific YAML examples
envs/ Conda specs and model-environment lock files
scripts/bootstrap_linux.sh noninteractive, idempotent environment bootstrap
scripts/smoke_uploaded_outputs.py
src/binder_filter/
cli.py public command dispatch
config.py strict Pydantic schema and scientific hash
artifacts.py directory/file/glob/ZIP indexing
adapters/ BoltzGen and PocketXMol input boundaries
normalization.py canonical T1...Tn/B1 mmCIF conversion
msa.py target-only MSA cache
fetch.py immutable model/asset locks
jobs.py versioned jobs and atomic completion validation
protenix_worker.py isolated Protenix-v2 inference
esmfold2_worker.py isolated full ESMFold2 inference/probability export
metrics.py ipSAE, exact actifpTM, RMSD, geometry and descriptors
analysis.py promotion, strict binary filters and analysis CSVs
selection.py Pareto ranking and deterministic diversity selection
output.py final CSV/mmCIF/PDF export and PDF verification
runner.py resumable end-to-end orchestration and GPU lock
tests/ deterministic unit/integration regression tests
tests/fixtures/ immutable scientific and cutoff boundary fixtures
The sibling upstream repositories and uploaded campaigns are references or input data; they are intentionally excluded from this package and must not become runtime imports.
The bootstrap command is non-interactive and idempotent. It uses Conda from PATH or,
when Conda is unavailable, provisions the pinned Miniforge release beneath the selected
prefix:
bash scripts/bootstrap_linux.sh --prefix /data/binder-filterThe script creates three environments without downloading the full Protenix sequence databases:
| Environment | Python | Responsibility |
|---|---|---|
bf-core |
3.11 | CLI, ingestion, structures, metrics, filtering, CSV/PDF output |
bf-protenix |
3.11 | Pinned Protenix-v2 inference and remote target MSA client |
bf-esmfold2 |
3.12 | Full ESMFold2 and pinned Biohub Transformers |
The directly pinned core package dependencies from pyproject.toml are:
| Library | Version | Use |
|---|---|---|
| Pydantic | 2.11.7 | strict YAML schema |
| gemmi | 0.7.3 | PDB/mmCIF parsing and writing |
| RDKit | 2025.3.5 | SDF topology and chemistry validation |
| NumPy | 2.3.2 | confidence, RMSD and geometry calculations |
| Hugging Face Hub | 0.36.2 | immutable model snapshots |
| hf-transfer | 0.1.9 | accelerated Hugging Face downloads |
| ReportLab | 4.4.3 | deterministic PDF generation |
| PyYAML | 6.0.2 | YAML parsing and resolved configuration |
| Poppler | Conda-resolved | pdfinfo parsing and full-page rendering checks |
The test extra pins pytest==8.4.1 and pytest-cov==6.2.1; the package is built with
Hatchling (hatchling>=1.27). PyTorch is installed separately into both model
environments as torch==2.7.1, torchvision==0.22.1, and torchaudio==2.7.1.
The complete transitive model-environment packages are in the two requirements lock
files. Each production run also writes the resolved environment, GPU, CUDA, driver,
snapshot revisions, weight checksums, and input checksums into provenance files.
Pinned model and scientific-reference revisions are:
| Asset | Revision |
|---|---|
| Protenix | c3bfc365b3e1341a11935eddfe7bfdc308092147 |
| Biohub ESM | 26b0bc2b771e3e419ea74f445a5f35cc094a1509 |
| Biohub Transformers | ef32577f55da19a4989cd7b22e004dc43a4998cb |
| ESMFold2 | 8fc3ff471022fdce52c77030685eb775de0c00a3 |
| ESMC-6B | 45b0fa5d7fb06faefbd5e3b89bdcef35d564e79a |
| DunbrackLab ipSAE | 6174cf9e71cb1bd660cc805856a18c4871a6dec3 |
| ColabFold actifpTM | 90327af088c51108f4779916c8f1b11ec623b18f |
| Protenix-v2 checkpoint | Hugging Face revision 87c7a633c66f9407095649d43fb08d1a073093c6; SHA-256 8f931f9774a396b67033d0e58628e1834f4a1448165e04254b40a780b0c0d599 |
Resolved model-environment packages are recorded in
envs/bf-protenix.requirements.lock.txt and
envs/bf-esmfold2.requirements.lock.txt.
PyTorch is pinned to 2.7.1: bootstrap selects its CUDA 12.6 wheel for A100/H100 and the
CUDA 12.8 wheel of the same release for Blackwell sm_120 support. On Blackwell,
Protenix uses its documented PyTorch triangle kernels and standard LayerNorm because its
optional fused binaries lack sm_120; scientific inputs and inference settings remain
unchanged.
Model snapshots, target MSAs, uploaded campaigns, and results intentionally live outside the Git checkout. Copying this repository therefore copies the implementation, but not those large assets. On each new machine or workspace:
- Run the idempotent bootstrap from the new checkout.
- Point
models.cache_dir,msa.cache_dir, input paths, andrun.output_dirat durable locations outside the checkout. Prefer a local SSD path for the approximately 25 GB ESMC-6B cache; network/FUSE workspace mounts can stall large Hugging Face shard writes. Predictor scratch trees are always created on node-local temporary storage and only validated final artifacts are committed torun.output_dir. Atomic writes retain rename-and-hash validation on FUSE volumes but omitfsync, which can block forever on some attached pod-volume implementations. - Run the CPU suite with the environment on
PATH, thenvalidate,fetch, anddoctorin that order.fetchreuses checksum-valid snapshots and downloads only missing assets. - Do not copy an old run directory into the new checkout or resume it after source, checkpoint, or scientific-contract changes. Those changes alter the scientific hash; create a new run name and preserve the old run as an archive.
- Before expensive inference, verify the pinned ESMFold2 head contract below. A mismatch is an installation/checkpoint error and must stop the run.
bash scripts/bootstrap_linux.sh --prefix /data/binder-filter
conda run -n bf-core pytest -m 'not gpu'
conda run -n bf-core binder-filter validate run.yaml
conda run -n bf-core binder-filter fetch run.yaml
conda run -n bf-core binder-filter doctor run.yamlThe pinned full ESMFold2 model exposes two distinct distance grids:
| Output/configuration | Bins | Range | Purpose |
|---|---|---|---|
raw["distogram_logits"] / structure_head.distogram_bins |
64 | 2-22 A | exported structure-contact probabilities used by exact actifpTM |
confidence_head.distogram_bins |
39 | 3.25-50.75 A | predicted-coordinate embedding used internally by the confidence head |
Run this static checkpoint/exporter assertion after fetch and before GPU inference:
conda run -n bf-esmfold2 python - <<'PY'
import numpy as np
from transformers.models.esmfold2.configuration_esmfold2 import ESMFold2Config
from binder_filter.esmfold2_worker import _distogram_lower_bounds
model_dir = "/data/binder-filter/models/esmfold2" # match models.cache_dir
config = ESMFold2Config.from_pretrained(model_dir, local_files_only=True)
assert config.structure_head.distogram_bins == 64
assert config.confidence_head.distogram_bins == 39
edges = _distogram_lower_bounds(config, 64)
assert len(edges) == 64 and np.isfinite(edges).all()
assert edges[0] == 0.0 and edges[1] == 2.0 and edges[-1] == 22.0
assert (np.diff(edges) > 0).all()
print("ESMFold2 distogram contract: OK")
PYThe exporter must validate raw distogram logits against the 64-bin structure head and
write lower bounds concat([0], linspace(2, 22, 63)). It must never compare those logits
with the 39-bin confidence grid. PAE is separate and is validated against
confidence_head.num_pae_bins. This assertion is part of fresh-checkout acceptance, not
an optional diagnostic.
Copy and edit configs/pocketxmol_pdl1.yaml. The uploaded
PD-L1 campaign contains 30,000 unique canonical 12-residue head-to-tail cyclic peptides.
Its top-100 cfd_traj cutoff is 0.614142; cfd_traj is generator self-confidence,
not binding confidence or affinity.
The default compute funnel is intentionally economical:
- Validate all source rows and retain invalid rows with diagnostics.
- Remove exact sequence duplicates from prediction.
- Convert the source score to a within-run percentile.
- Select the deterministic top 100 by source percentile, source rank, and candidate ID; record diversity without allowing it to displace a higher-ranked preliminary candidate.
- Run seed 101 with both predictors: at most 200 preliminary jobs.
- Promote at most 30 candidates using conservative confidence, agreement, pose recovery, and diversity. If preliminary failures leave unused capacity, fill it deterministically by source rank so a candidate can still recover under the 3-of-5 confirmation rule.
- Complete seeds 101-105 with both predictors: at most 240 additional jobs.
- Rank passers and select at most 20 for downstream work.
This is at most 440 holo prediction jobs. The selection lists and scored inputs are persisted before GPU execution so resume never silently reruns selection.
Funnel counts must be interpreted by stage: all 100 preliminary candidates receive one seed from each predictor, but only the promoted 30 receive the five-seed confirmation needed for scientific pass/fail. The other 70 are compute-funnel exclusions, not confirmed scientific failures. Consequently, "16 of 30 pass" means 16 of the 30 fully confirmed candidates passed; the final budget of 20 remains unfilled when fewer than 20 qualify.
For multiple PocketXMol generation seeds, preserve a campaign identifier, normalize
cfd_traj within each campaign, globally remove sequence duplicates, pool candidates,
and make one global quality/diversity selection. A random generation seed is provenance,
not evidence of quality.
Copy and edit configs/boltzgen_pdl1.yaml. Binder Filter
preserves every source CSV field inside source_metrics_json. It prefers quality_score,
then design_to_target_iptm, design_iptm, iptm, and finally inverse final_rank for
within-run preselection. Those values are generator signals, not new binding evidence.
The current uploaded BoltzGen fixture contains 100 compute-ready final CIFs and 100 matching metrics rows. Its larger CSV contains 25,371 rows but does not provide all corresponding structures, so it cannot be treated as a complete 30,000-structure input.
Additional PD-L1 cyclic BoltzGen examples are included for the current campaign layout:
configs/pd_l1_cyclicpeptide_anything_smoke10.yaml: 10-candidate diagnostic smoke run.configs/pd_l1_cyclicpeptide_three_metric_smoke1.yaml: one-candidate, five-confirmation-seed metric smoke run.configs/pd_l1_cyclicpeptide_boltzgen_production.yaml: 1,000-candidate preselection, 30-candidate confirmation, and final budget 20.
These examples contain machine-specific /workspace/uploads and model-cache paths; copy
them and replace those paths before use on another machine.
conda run -n bf-core binder-filter validate run.yaml
conda run -n bf-core binder-filter doctor run.yaml
conda run -n bf-core binder-filter fetch run.yaml
conda run -n bf-core binder-filter run run.yaml
conda run -n bf-core binder-filter resume /workspace/results/my_run
conda run -n bf-core binder-filter report /workspace/results/my_run
conda run -n bf-core pytest -m 'not gpu'
conda run -n bf-core pytest -m gpufetch downloads only pinned inference assets and queries the remote MSA service once per
unique uppercase target-chain sequence. Designed binder sequences are never submitted to
an MSA service. Single-sequence fallback is fatal unless explicitly enabled in YAML.
run performs fetch, normalization, the persisted preliminary/confirmation funnel, both
predictor families under one GPU lock, analysis, and final export. resume validates the
scientific configuration hash, implementation-source hash, source inventory, provenance,
and every completed artifact before reuse; interrupted or partial jobs alone are rerun.
Worker metadata cross-checks normalized-input/MSA/parameter/output checksums, while
deterministic model-local failures are terminal and process-level OOM failures receive one
batch-size-one retry. report regenerates and Poppler-verifies the PDF from the existing
scientific results without launching prediction.
All medians use valid confirmation seeds. A hard-filter model median requires at least three valid seeds.
| Rule | Cutoff |
|---|---|
| Valid holo seeds | at least 3/5 from each model |
| Peptide interaction confidence | all three required: Protenix-v2 median ipSAE_min >= 0.60, ESMFold2 median ipSAE_min >= 0.60, and ESMFold2 median exact actifpTM >= 0.93 |
| Miniprotein interaction confidence | all three required: Protenix-v2 median ipSAE_min >= 0.60, ESMFold2 median ipSAE_min >= 0.60, and ESMFold2 median exact actifpTM >= 0.93 |
| Peptide cross-model binder RMSD | <= 1.25 A |
| Peptide source-pose recovery RMSD | <= 1.50 A |
| Miniprotein cross-model/source-pose RMSD | <= 2.5 A for each |
| Severe nonbonded C-alpha distance | < 2.5 A |
| Severe target-binder heavy-atom overlap | < 0.65 * summed vdW radii |
| Ordinary reported overlap | < 0.80 * summed vdW radii |
| Miniprotein apo pTM | >= 0.70 |
| Miniprotein apo binder pLDDT | >= 75 |
| Miniprotein apo-holo RMSD | <= 2.5 A |
ipSAE uses the pinned official equations with a 10 A PAE cutoff and 15 A structure-distance
cutoff, treating all target chains as one partner and B1 as the other. ipSAE_min is
the smaller directional value.
actifpTM is calculated only from the real probability pathway required by the pinned ColabFold reference. Expected PAE alone is insufficient and is never relabelled as actifpTM. If ESMFold2 exact probability inputs are unavailable, the value is null and a interaction gate fails. Every mode also requires ipSAE from both model families. The 0.93 actifpTM cutoff is a campaign-prioritization heuristic chosen after sensitivity analysis of the first BoltzGen PD-L1 run; it is not an affinity threshold and has not been experimentally calibrated across targets.
The stricter gate is intentionally conjunctive. A high score from one predictor cannot
rescue weak or unavailable evidence from the other. Retrospectively, combining
Protenix and ESMFold2 ipSAE_min >= 0.60, exact ESMFold2 actifpTM >= 0.93,
cross-model RMSD <= 1.25 A, source-pose
RMSD <= 1.50 A, and zero severe clashes retained 16 of the 30 confirmed candidates in
the first BoltzGen PD-L1 campaign, compared with 29 under the former permissive rule.
Native ipTM is always recorded and is never a hard gate.
Non-passers cannot enter the final set. Passers are ranked without a weighted composite score using exactly five evidence dimensions:
- Higher Protenix-v2 median
ipSAE_min. - Higher full ESMFold2 median
ipSAE_min. - Lower cross-model binder RMSD.
- Lower source-pose binder RMSD.
- Lower worst-family seed variability: the larger within-model median RMSD from the two predictor families.
A candidate belongs to Pareto front 1 when no other passer is at least as good on every
dimension and better on at least one. Removing that front and repeating produces fronts
2, 3, and so on. Within a front, each dimension receives a dense ordinal rank; candidates
are ordered by their worst of the five ranks, then median rank, then stable candidate ID.
This quality_rank is computational evidence priority, not predicted affinity.
Exact ESMFold2 actifpTM and ipSAE from both model families are mandatory interaction evidence for pass/fail in every mode, but their margins are not mixed numerically. Native ipTM, generator score/source rank, hydrophobic patch, liabilities, delta-SASA, contacts, hydrogen bonds, and salt bridges remain report fields and do not affect default final rank. Generator scores are used only for the preliminary compute funnel.
Final selection also has no 90/10 or other weighted utility. Pareto fronts are processed
in order, so a lower front cannot displace an available higher-front candidate. Within a
front, the strongest weakest-link candidate is selected first. Each subsequent candidate
maximizes its minimum joint novelty to the selected set, where pairwise joint novelty is
the smaller of sequence-identity distance and target-contact Jaccard distance. Cyclic
sequence identity is rotation-aware. Diversity changes selection only; it never changes
pass/fail, Pareto front, or quality rank.
This deliberately follows the hard-filter/refold-consistency emphasis of RFdiffusion, the independent confidence/geometry checks used by RFdiffusion3, and BoltzGen's useful worst-ordinal-rank idea, while removing hand-selected metric weights. The BindCraft study also cautions that ipTM can prioritize likely binding but is not an affinity rank. A learned ranking model should replace this default only after multi-target wet-lab calibration with held-out targets.
run.output_dir names the shared results root and run.name is appended automatically.
For example, output_dir: /workspace/results and name: pdl1_screen write only beneath
/workspace/results/pdl1_screen/. Each job contains resolved configuration and hashes,
provenance, normalized structures, selections, predictions, analysis, and final exports:
<run.name>/
config.resolved.yaml
provenance.json
weights.lock.json
msa.lock.json
normalized/
manifest.csv
structures/<candidate>.cif
source_cache/
work/
state.json
gpu.lock
selection/preliminary.json
selection/confirmation.json
predictions/
protenix_v2/<candidate>/<context>/seed_<seed>/
esmfold2/<candidate>/<context>/seed_<seed>/
report_render/page-*.png
analysis/
per_seed_metrics.csv
failures.csv
diversity_clusters.csv
summary.json
final_validation/
all_designs_metrics.csv
passed_designs_metrics.csv
final_designs_metrics_<budget>.csv
passed_complexes/rank0001_<candidate>.cif
results_overview.pdf
COMPLETED.json
This follows BoltzGen's useful output convention: deterministic rank-prefixed structures,
an all-designs CSV, a budget-specific final CSV, and a PDF overview. Binder Filter does
not reuse BoltzGen's scientific ranking or pass logic. The copied structures are only
Binder Filter passers, and the final-budget CSV marks the diversity-selected subset.
The output convention was reviewed against
HannesStark/boltzgen commit
a3149cf18eeb58648d1abbb27539bd73f746cdda, particularly
src/boltzgen/task/filter/filter.py. Binder Filter has no runtime dependency on that
checkout.
- Peptide modes have genuine dual-model GPU smoke coverage and a completed BoltzGen production campaign. A PocketXMol cyclic production campaign must be rerun under the v0.2.0 scientific hash after the cutoff change.
- Miniprotein support is implemented end to end: schema, ingestion, normalized
structures, the isolated-binder apo confirmation stage (seeds 101-105 for both
predictors), and the
pass_miniprotein_apogate (at least one model with median apo pTM>= 0.70, apo binder pLDDT>= 75, and binder-aligned apo-holo RMSD<= 2.5 A), with unit and synthetic end-to-end coverage. It has not yet passed a genuine GPU acceptance run. Do not describe miniprotein support as accepted production coverage until its GPU acceptance test passes. - Screening relaxation is unavailable and
enabled: trueis rejected. Final structures are unmodified consensus medoids and are not MD-ready systems. - Protenix does not expose the exact probability tensors required by the pinned actifpTM pathway. Every mode therefore requires ipSAE from both predictors plus ESMFold2 exact actifpTM.
- Confidence metrics prioritize candidates; they are not affinity, specificity, or experimental-success probabilities.
Binder Filter source code is provided under the included Apache-2.0 license. Upstream code, model weights, data, and generated campaigns remain subject to their respective licenses and terms. Any independently reimplemented upstream algorithm retains its pinned revision, license notice, provenance, and regression fixtures.