Agent-coordinated protein design for rare-earth-element binding.
Takes templates of known or suspected REE-binding proteins from REE-Miner, generates candidate designs around their metal sites, scores them with REE-Finder, and iterates in a Design-Build-Test-Learn loop to produce a ranked list of designs for wet-lab testing.
REE-Miner templates
│
▼
┌─────────────────────────────────────────────────────┐
│ DBTL loop (deterministic, checkpointed) │
│ │
│ Design ──► Build ──► Test ──► Learn ──┐ │
│ ▲ │ │ │
│ │ ▼ │ │
│ │ REE-Finder │ │
│ │ (Boltz → ESMBind → │ │
│ │ Amber MD → DFT) │ │
│ └────────────────────────────────────┘ │
│ ▲ │
│ LLM advisor (bounded) │
└─────────────────────────────────────────────────────┘
│
▼
ranked designs + wet-lab report
pip install -e ".[dev]" # core + tests
pip install -e ".[advisor]" # + the LLM advisorThe demo runs the whole loop on a laptop — no GPU, no Amber, no scheduler — against a surrogate scorer:
ree-designer check configs/demo.yaml # validate everything, submit nothing
ree-designer run configs/demo.yamlIt designs against human calmodulin EF-hand loop 1 (DKDGDGTITTKE), whose
position-2 Lys is precisely the residue the lanmodulin literature says to
replace with Pro. Outputs land in campaigns/demo/:
| File | Contents |
|---|---|
report.md |
The human document — caveats, recommendations, assay guidance |
recommendations.csv |
The wet-lab shortlist, ordered by evidence strength |
designs.csv |
Every design scored, with all objectives and provenance |
campaign_state.json |
Full state; resume by re-running run |
The surrogate scorer is not a model. Its energies come from coordination heuristics plus hash noise. It exists to exercise the plumbing — including the awkward parts (variable cluster composition, designs that form no site, identical Nd/Ca site predictions) that the real backend produces.
For a real campaign, fill in configs/production.yaml and run the same
commands.
Ingests REE-Miner's cam_dataset_entries.json / training_dataset_esm.json,
or its PDB-mining CSVs. Two numbering hazards are handled at ingest so nothing
downstream has to think about them:
- Miner's
binding_positionsare 0-indexed (re.Match.start()); everything here and in REE-Finder is 1-indexed. - PDB
seqid.numis author numbering and does not index the extracted sequence. Contacts that cannot be reconciled are dropped, not guessed.
All backends implement one interface, so a round can mix a cheap heuristic sweep with an expensive structure generator and rank the output on one scale.
| Backend | What it does |
|---|---|
heuristic |
Chemistry-driven mutagenesis: the EF-hand position-2 proline switch, carboxylate enrichment bounded by charge balance, soft-donor removal, Asp/Glu reach tuned to ionic radius |
knowledge |
Mutations learned from the corpus of known binders — see below |
saturation |
Systematic scanning over a focused alphabet — how the campaign finds effects the rules miss |
combinatorial |
Recombines substitutions the campaign has measured as good; returns nothing without that evidence rather than guessing |
proteinmpnn |
Sequence design on a fixed backbone, coordinating residues held fixed |
rfdiffusion |
Backbone generation around the coordination motif (submitted as a GPU job, chained into ProteinMPNN) |
Every proposal carries a rationale string that flows through to the report, so a bench scientist can see why a mutation was suggested.
The knowledge backend asks the corpus rather than the literature: across
proteins that demonstrably bind lanthanides versus proteins that bind calcium,
which residues at which loop positions actually differ? Every proposal carries
its counts and test statistic into the wet-lab report.
ree-designer corpus configs/demo.yaml # what the corpus supports, and what it can'tGetting a defensible answer took four guards, each of which silently produces a confident wrong recommendation if missed:
Labels can't come from the thing being tested. REE-Miner sets
is_ree_selective = (pos2_aa == "P") and derives metal_code from it. Testing
"is Pro enriched at position 2?" against those labels recovers the rule that
generated them, with an arbitrarily small p-value and no information.
from_miner_loops_csv refuses those columns and labels from protein family
instead — lanmodulin is a characterised Ln binder, calmodulin a characterised
Ca sensor.
Redundancy has to collapse, but not too far. The bundled SaCaM sequence carries loops byte-identical to calmodulin's, and two lanmodulin loops are the same sequence. But the conventional 90%-identity threshold, applied to a 12-residue motif, merges any two loops differing at one position — which is exactly the resolution needed, since the Asp→Pro switch is a single residue. The default collapses exact duplicates only; at 90% the best attainable p-value degrades from 0.006 to 0.048.
Loop positions aren't homologous across families. Every Ln loop carries its structural glycine at loop position 5; every Ca loop at position 6. The loops are anchored at positions 1 and 12, so positions 5–11 sit in different registers and compare different parts of the loop. Raw enrichment there is an alignment artefact — it suggests replacing calmodulin's coordinating Asp25 with LanM's structural glycine. Those positions are detected and excluded.
A tied block is not a winner. All lanthanide loops in the bundled corpus come from one protein, so every position where it differs from the calcium set yields an identical contingency table. Six positions tie at q=0.04 and no test can rank within them. The corpus reports the block as a block.
What survives on the bundled corpus is K22P (the literature answer, recovered from data) and G24D (a novel hypothesis: carboxylate density at loop position 4, which the hand-coded rules never proposed). Both are proposed as separate single mutants — because the DBTL loop can deconfound by measurement what sequence comparison cannot deconfound by counting.
Point the backend at a REE-Miner workspace to grow the corpus:
design:
backends: [heuristic, knowledge, saturation, combinatorial]
params:
knowledge:
loops_csv: ree_miner_data/datasets/cam_efhand_loops.csv
contacts_csv: ree_miner_data/datasets/pdb_contacts_raw.csvcontacts_csv adds donor propensities from residues observed contacting
lanthanides in solved structures — non-circular by construction, and the
fallback for sites that aren't EF-hand loops. Propensities are ranked on a
Wilson lower bound, not the point estimate: Trp is rare enough in the proteome
that one incidental contact otherwise scores as high as seven aspartates.
Adding a second independent Ln-binding protein (Hans-LanM, lanpepsy, an XoxF site) is the single highest-value thing you can do here — it breaks the phylogenetic confounding that currently ties six positions together.
REEFinderBackend generates a driver script and a PBS Parsl config, submits
them, and reassembles the results. Four properties of REE-Finder shaped this
adapter:
Pipeline.run's return value is unusable for attribution — a flatlist[(coords, energy)]with no protein or ion label. Results are harvested fromsims/{design}/{ion}/clusters/instead.- Per-residue probabilities are never persisted. The generated driver dumps them before delegating, since site confidence cannot be recovered otherwise.
src/ree-finderis not an importable package (it contains a hyphen), so the driver puts it onsys.pathand imports its modules flatly.- The FASTA parser strides by line index (
lines[::2]/lines[1::2]), so one wrapped sequence silently pairs every design with the wrong header. Every written batch is re-read the same way and checked before submission.
The part that decides whether the rankings mean anything.
REE-Finder returns energy_hartree: the absolute SCF energy of a carved
cluster. It is not a binding energy and is not comparable between designs whose
clusters differ in composition. The magnitudes make the trap plain — one extra
water shifts the total by about −76 Ha, while the entire chemical effect
being designed for is 0.01–0.1 Ha. Ranking on raw energy ranks designs by
how many waters extract_clusters happened to include.
Two defensible modes, and a refusal to produce a number when neither applies:
strict(default) — compare only identical molecular formulas. Exact. Its cost is coverage: designs with a different water count drop out.linear_reference(opt-in) — least-squares fitE ≈ Σ nₑ εₑand compare residuals. Recovers coverage, loses rigour. For triage, never for a headline.
Selectivity is a double difference, because comparing an Nd cluster against a Ca cluster directly is meaningless (different metals, electron counts, total charge):
ΔΔE = [E(design, Nd) − E(ref, Nd)] − [E(design, Ca) − E(ref, Ca)]
The metal-dependent constants cancel, provided each ion's pair is composition-matched to the reference.
A deterministic state machine, checkpointed atomically before every wait so the orchestrator can be killed and resumed while jobs sit in the PBS queue.
Designs are scored on six objectives, each normalized 0–1, higher always
better: affinity, selectivity, site_confidence, shell_quality,
parsimony, novelty. Missing objectives are dropped and the remaining
weights renormalized, never imputed as zero — otherwise a design REE-Finder
could not evaluate would rank below one it evaluated badly, penalising designs
for the pipeline's coverage gaps.
Output is both a weighted ranking (an ordered list to work down) and a Pareto front (the honest view of where the trade-offs are). Selection reserves slots for Pareto-optimal and diverse designs so the campaign does not collapse onto one lineage by round two.
Consulted at three points, with strictly bounded authority:
adjust_strategy— reweights the next round's budget across backends and nominates saturation positions. Its output is clamped to the campaign's own budget and backend set; unknown backends are dropped.interpret_round— writes the campaign-log paragraph.write_report— writes the wet-lab narrative.
It never selects designs, never changes an objective weight, never touches a score. Ranking stays reproducible. Every call degrades to a no-op without credentials, so a campaign always runs without it.
Stated here as prominently as in every generated report, because these are properties of the modelling stack rather than of any one run:
- Only Nd is modelled. REE-Finder supports
CA,MG,FE,ND. There is no intra-lanthanide selectivity data — nothing distinguishes Nd from Pr or Dy. The chemistry module knows the whole series and uses it for design heuristics (radius-matched carboxylate reach), but no score can confirm it. - Nd and Ca site predictions are identical by construction. ESMBind was
not trained on lanthanides, so
ESMBIND_CHANNEL['ND'] == 'CA'. Both ions are modelled at the same residues; any Nd-over-Ca signal comes from the Amber 12-6-4 parameters and DFT, never from the site predictor. - DFT energies are absolute cluster energies, not binding energies. See above.
- Nothing models fold stability, expression, or solubility. A design can
score well and still be undesigned protein. Mutational load is tracked as
parsimonyprecisely because nothing else guards it.
Every recommendation therefore carries an evidence tier (A–D) so a ranked list is never mistaken for a set of measurements.
pytest # 149 tests, no network, no GPU
ruff check src testssrc/ree_designer/
chem.py REE coordination chemistry (radii, donors, EF-hand motifs)
knowledge.py Known-binder corpus: contrast statistics, donor propensities
types.py Domain types; the contract between every stage
templates.py REE-Miner ingest
config.py Campaign YAML → constructed objects
report.py CSV + Markdown deliverables with evidence tiers
cli.py templates / check / run / report / status
io/ REE-Finder-safe FASTA
runners/ Local and PBS Pro job execution
design/ Generative backends
scoring/ REE-Finder adapter, surrogate, energy comparison
dbtl/ Objectives, selection, state, campaign loop
advisor/ Bounded LLM advisor