tcren package#
The package is laid out in layers: what a structure is (parsing, annotation, contacts), what can be measured on it (topology, energetics, mechanics, docking geometry), the catalogue that names every measurement, and the scores built on top. Each layer only reaches downwards.
Three layers are documented in prose elsewhere and are not repeated here: Assessing a modelled complex for
tcren.score, Reliability: scoring one modelled structure for tcren.reliability, and Contact-map Potts model for
tcren.potts.
Note
Nine top-level modules are deprecated locations, kept so existing imports keep working:
tcren.ddg, tcren.dynamics, tcren.footprint, tcren.interface_graph,
tcren.pose, tcren.rotamers, tcren.scoring, tcren.stability and tcren.surface,
as is the whole tcren.orient package. Each re-exports its new home, and the new home is what
is documented below. Import the canonical name in new code.
Structure I/O#
tcren.structure.model module#
Lightweight structure data model.
These dataclasses wrap the parsed contents of a PDB/mmCIF file in the shape the rest of
the pipeline needs: per-chain residue lists carrying both a 0-based sequential index
(matching the legacy mir residue.index) and the original author numbering, plus
heavy-atom coordinates for contact computation.
- class tcren.structure.model.Atom(name, element, coord)[source]#
Bases:
objectA single (heavy) atom.
- Parameters:
name (str)
element (str)
coord (ndarray)
- name: str#
- element: str#
- coord: ndarray#
- class tcren.structure.model.Residue(seq_index, pdb_index, insertion_code, aa, resname, atoms)[source]#
Bases:
objectA polymer residue.
- Variables:
seq_index (int) – 0-based sequential index over the chain’s resolved polymer residues (the legacy
residue.index); independent of author numbering gaps.pdb_index (int) – Author residue number (
residue.index.pdb).insertion_code (str) – Author insertion code (
''when absent).aa (str) – One-letter amino-acid code (
'X'for unknown).resname (str) – Three-letter residue name (
HIS,MSE…).atoms (tuple[tcren.structure.model.Atom, ...]) – Heavy atoms of the residue.
- Parameters:
seq_index (int)
pdb_index (int)
insertion_code (str)
aa (str)
resname (str)
atoms (tuple[Atom, ...])
- seq_index: int#
- pdb_index: int#
- insertion_code: str#
- aa: str#
- resname: str#
- property ca: ndarray | None#
Cα coordinate, or
Noneif the residue has no Cα atom.
- property cb: ndarray | None#
Cβ coordinate, or
Noneif the residue has no Cβ atom (e.g. glycine).
- property cb_or_ca: ndarray | None#
Cβ coordinate, falling back to Cα (glycine / missing Cβ);
Noneif neither.
- class tcren.structure.model.Chain(chain_id, residues, chain_type=None, chain_supertype=None, allele_info=None, regions=<factory>)[source]#
Bases:
objectA polymer chain and its annotations.
- Parameters:
chain_id (str)
residues (list[Residue])
chain_type (str | None)
chain_supertype (str | None)
allele_info (str | None)
regions (list[RegionMarkup])
- chain_id: str#
- chain_type: str | None#
- chain_supertype: str | None#
- allele_info: str | None#
- regions: list[RegionMarkup]#
- class tcren.structure.model.RegionMarkup(region_type, start_seq_index, end_seq_index, sequence, residues)[source]#
Bases:
objectAn annotated region (CDR/FR for TCR, groove regions for MHC).
- Parameters:
region_type (str)
start_seq_index (int)
end_seq_index (int)
sequence (str)
residues (list[Residue])
- region_type: str#
- start_seq_index: int#
- end_seq_index: int#
- sequence: str#
- class tcren.structure.model.Structure(pdb_id, chains, complex_species=None, cell_type=None, mhc_calls=None)[source]#
Bases:
objectA parsed complex: a set of annotated chains.
- Parameters:
pdb_id (str)
chains (list[Chain])
complex_species (str | None)
cell_type (str | None)
mhc_calls (list | None)
- pdb_id: str#
- complex_species: str | None#
- cell_type: str | None#
- mhc_calls: list | None#
MhcCall list, cached by annotate_mhc / annotate_mhc_batch so the batched path does not have to be repeated per structure downstream (CLAUDE.md 0-mmseqs). None until annotated.
tcren.structure.io module#
Parse PDB / mmCIF files into the tcren.structure.model data model.
Accepts plain .pdb/.ent/.cif/.mmcif files, their gzip-compressed forms
(.pdb.gz/.cif.gz …), and — for batches — directories or .tar/.tar.gz archives
of any of those (see iter_structures()). Structure identifiers are resolved from the file
name by structure_id_from_path().
- tcren.structure.io.is_structure_file(name)[source]#
True if
nameis a (optionally gzipped) PDB/mmCIF structure file.Rejects macOS AppleDouble sidecars (
._4x5w.pdb), which carry the extension of the file they shadow but hold a binary resource fork – tarring a structure set on HFS+ writes one beside every member, and feeding one to a parser raises a decode error several frames from the cause.- Parameters:
name (str | Path)
- Return type:
bool
- tcren.structure.io.structure_stem(path)[source]#
The file name with a trailing
.gzand the structure extension removed.- Parameters:
path (str | Path)
- Return type:
str
- tcren.structure.io.structure_id_from_path(path)[source]#
Resolve a structure identifier from a file name.
Strips a trailing
.gzand the structure extension, then takes the part before the first_(so4x5w_renumbered.cifand1ao7.pdb.gzand6uk4_TCRpMHCmodels.pdball resolve to their PDB id).Warning
Lossy by design, and not unique for cohorts whose file names encode metadata after an underscore –
VDJdb_Model_603_min.pdbandVDJdb_Model_604_min.pdbboth giveVDJdb. Preferiter_structures(), which detects that case for a whole set and falls back to the full stem rather than silently collapsing rows.- Parameters:
path (str | Path)
- Return type:
str
- tcren.structure.io.resolve_structure_ids(paths)[source]#
Map each path to an id: the PDB-id prefix when that is unique over the set, else the stem.
The prefix rule is what makes
4x5w_renumbered.cifcome back as4x5w, and it is right for RCSB-derived files. It is wrong – silently, and in a way that destroys rows downstream – for any set whose names carry metadata after the first underscore. Deciding per SET rather than per file keeps the convenience where it is unambiguous and refuses it where it is not.- Return type:
dict[str, str]
- tcren.structure.io.parse_structure(path, pdb_id=None, model=0, keep_hydrogens=True)[source]#
Parse a structure file into a
Structure.Residues are taken in author order; only amino-acid residues (standard or modified, via the extended three→one table) are kept — waters, ions and ligands are dropped. Each kept residue receives a 0-based sequential
seq_indexper chain, matching the legacymirresidue.index.- Parameters:
path (str | Path) – Path to a
.pdb/.entor.cif/.mmciffile.pdb_id (str | None) – Structure identifier; defaults to the file stem.
model (int) – Model index to read (default 0 — the first model).
keep_hydrogens (bool) – Keep hydrogen atoms (default
True— the legacy mir contact definition counts hydrogens when a structure provides them).
- Returns:
The parsed
Structure.- Return type:
- tcren.structure.io.mean_bfactor(path, chain=None)[source]#
Mean B-factor over a structure file, or over one chain of it.
In a model written by AlphaFold or TCRmodel2 the B-factor column IS the per-residue pLDDT, so this is how a generated structure’s own confidence is read back off disk. In a crystal it is the crystallographic B-factor and means something entirely different; the caller has to know which kind of file it is holding.
This is supplied data — the generator’s read-out, not a quantity tcren computes — which is why it is a file reader here rather than a descriptor in
tcren.recognition.DESCRIPTORS.parse_structure()deliberately drops B-factors, since they are not part of the geometry the rest of the package reasons about; this exists so that reading them does not require a second PDB parser.- Parameters:
path (str | Path) – a
.pdb/.pdb.gzfile. mmCIF is not supported.chain (str | None) – restrict to one author chain id, or
Nonefor every atom in the file.
- Returns:
The mean, or
nanif the file holds no atom line matchingchain.- Return type:
float
- tcren.structure.io.import_structure(path, pdb_id=None, model=0, keep_hydrogens=True, trim_c_gene=True, keep_c_gene=False, min_constant_score=80.0)[source]#
Parse a structure and prepare it for interface analysis.
Wraps
parse_structure(), records the αβ/γδ cell type from the TCR constant region, and — by default — trims that constant region so downstream analysis works on the variable domains and the interface.- Parameters:
path (str | Path) – as in
parse_structure().pdb_id (str | None) – as in
parse_structure().model (int) – as in
parse_structure().keep_hydrogens (bool) – as in
parse_structure().trim_c_gene (bool) – Trim the TCR constant domain (default
True).keep_c_gene (bool) – Retain the constant domain even if
trim_c_geneis set. Use this for molecular-dynamics / FlexPepDock and any workflow that needs the full chain — those depend on the presence of the C-gene.min_constant_score (float) – Minimum constant-region alignment score to trim on.
- Returns:
The imported
Structurewithcell_typeset.- Return type:
- tcren.structure.io.structure_paths(src)[source]#
List structure files for
src, sorted.srcmay be a single structure file, a directory (scanned for structure files), a glob pattern (models/*.pdb.gz), or a manifest: a.txt/.list/.lstfile holding one path per line,#comments and blank lines ignored, relative paths resolved against the manifest’s own directory. Recognises plain and gzipped PDB/mmCIF (.pdb,.cif.gz, …). For archives or streaming, useiter_structures().- Parameters:
src (str | Path)
- Return type:
list[Path]
- tcren.structure.io.resolve_sources(sources)[source]#
Split a CLI
-sspec (or several) into individual sources foriter_structures().Accepts one spec or an iterable of them, and splits each on commas, so
-s a.pdb.gz,b.pdb.gz, a repeated-s, and a shell glob all mean the same thing. Directories, tar archives, globs and manifests are passed through untouched — they are expanded downstream, where the tar streaming lives.- Parameters:
sources (str | Path | Iterable[str | Path])
- Return type:
list[str]
- tcren.structure.io.iter_structures(src, importer=<function import_structure>, on_error='raise', **kwargs)[source]#
Yield
(pdb_id, Structure)for a file, directory, or.tar/.tar.gzarchive.Handles plain and gzipped PDB/mmCIF (
.pdb/.cif/.pdb.gz/.cif.gz…); a directory is scanned for those; a tar archive is streamed member-by-member (each member materialised to a temp file so the path-basedimporterworks unchanged). The identifier is resolved per file bystructure_id_from_path().- Parameters:
src (str | Path) – structure file, directory, or tar archive.
importer (Callable[[...], Structure]) – per-file parser —
import_structure()(default, trims the C-gene) orparse_structure()(parity-pure). Extrakwargsare forwarded to it.on_error (str) –
"raise"(default) or"skip"to ignore files that fail to parse.
- Return type:
Iterator[tuple[str, Structure]]
- tcren.structure.io.pdb_lines(structure, transform=None, keep_hydrogens=True)[source]#
ATOM/TER/END record lines for
structure(optionally coordinate-transformed).One conformer per atom name per residue (drops duplicate altlocs).
transformis an optionalcoord -> coordcallable (e.g. for an oriented frame); identity ifNone. Author residue numbers + insertion codes are preserved.- Parameters:
structure (Structure)
keep_hydrogens (bool)
- Return type:
list[str]
- tcren.structure.io.cif_lines(structure, transform=None, keep_hydrogens=True)[source]#
Minimal mmCIF
atom_siteloop forstructure(optionally transformed).Same atom selection as
pdb_lines()(one conformer per atom name per residue). Only the_atom_sitecategory is written — enough to round-trip coordinates + chain/residue identity through the Biopython MMCIF parser, which is all tcren consumes.- Parameters:
structure (Structure)
keep_hydrogens (bool)
- Return type:
list[str]
- tcren.structure.io.write_pdb(structure, path, transform=None, keep_hydrogens=True)[source]#
Write
structureto a PDB file; return the path.A
.gzsuffix (foo.pdb.gz) transparently gzip-compresses the output.- Parameters:
structure (Structure)
path (str | Path)
keep_hydrogens (bool)
- Return type:
Path
- tcren.structure.io.structure_output_path(directory, pdb_id, mmcif=False, compress=False)[source]#
Build an output path
<directory>/<pdb_id>.<ext>from format flags..pdbby default,.cififmmcif, with a trailing.gzifcompress.- Parameters:
directory (str | Path)
pdb_id (str)
mmcif (bool)
compress (bool)
- Return type:
Path
Annotation#
tcren.annotation.arda_adapter module#
TCR chain annotation via the arda library.
Extracts each chain’s amino-acid sequence, runs arda’s AIRR annotation, and projects the
returned region coordinates (1-based, end-inclusive into the input sequence) back onto
structure residues as RegionMarkup. Region names are
mapped to the legacy mir vocabulary (FR1/CDR1/…/FR4).
- tcren.annotation.arda_adapter.annotate_chain(chain, organism)[source]#
Annotate one chain with arda; return the AIRR record if it is a TCR chain.
Mutates
chainin place when arda recognises it as TRA/TRB: setschain_type,chain_supertype("TRAB"),allele_infoandregions. Returns the arda record (for any locus) orNoneif arda produced no locus.- Parameters:
organism (str)
- Return type:
dict | None
- tcren.annotation.arda_adapter.apply_records(chains, by_id)[source]#
Project a cached
{chain_id: record}map onto chains in place (no arda call).- Parameters:
by_id (dict[str, dict])
- Return type:
None
- tcren.annotation.arda_adapter.score_records(chains, by_id)[source]#
(receptor_ids, summed mmseqs2_score)from already-computed records.- Parameters:
by_id (dict[str, dict])
- Return type:
tuple[list[str], float]
- tcren.annotation.arda_adapter.annotate_chains(chains, organism)[source]#
Annotate a batch of chains in a single arda call; apply records in place.
One mmseqs invocation for all chains (the per-call process/DB overhead dominates the actual alignment, so batching is ~hundreds× faster than per-chain calls). Returns a
{chain_id: record}map for chains that had a sequence.- Parameters:
organism (str)
- Return type:
dict[str, dict]
- tcren.annotation.arda_adapter.annotate_tcr_chains(structure, organism='human')[source]#
Annotate all chains; return ids recognised as antigen-receptor (TCR/BCR) chains.
- Parameters:
organism (str)
- Return type:
list[str]
- tcren.annotation.arda_adapter.annotate_tcr_chains_scored(structure, organism='human')[source]#
Annotate all chains; return
(receptor_ids, summed mmseqs2_score).The summed mmseqs2 alignment score over the receptor chains measures how well the structure’s TCR/BCR chains match this organism’s germline reference — the signal used to pick the correct species when annotating against human vs mouse.
- Parameters:
organism (str)
- Return type:
tuple[list[str], float]
tcren.annotation.chains module#
Chain classification: receptor chains (via arda), PEPTIDE, and (provisional) MHC.
classify_chains tags MHC chains with the generic type "MHC" so the TCR↔peptide
scoring path is complete. Precise MHC sub-typing (MHCa/MHCb/B2M, class I/II, allele) is a
separate step in tcren.mhc (annotate_mhc / mhc.mapper).
- tcren.annotation.chains.classify_chains(structure, organism='human', peptide_max_len=30, autodetect_species=True, precomputed_records=None)[source]#
Classify every chain of
structurein place.Receptor chains are assigned from arda’s locus call (TCR TRA/TRB and, for BCR inputs, IGH/IGK/IGL); the shortest remaining chains (length ≤
peptide_max_len) become PEPTIDE; longer remaining chains are tagged"MHC".- Parameters:
structure (Structure) – Structure to annotate (mutated in place).
organism (str) – arda organism (
"human"/"mouse").peptide_max_len (int) – Maximum residue count for a chain to be called PEPTIDE.
autodetect_species (bool) – Annotate against both supported species (human and mouse) and keep whichever gives the higher total mmseqs alignment score over the receptor chains. TCR/BCR germlines are organism-specific, so the wrong species scores measurably lower (e.g. mouse BM3.3 scores ~435 vs ~197 under human); this avoids mis-typing a chain under the wrong reference. Ties keep the requested
organism. Disable to forceorganismverbatim.precomputed_records (dict[str, dict[str, dict]] | None) – Optional
{organism: {chain_id: record}}of arda records for this structure’s chains, to reuse instead of calling arda (the batch path inannotate_structure_set()annotates the whole dataset in one mmseqs call per organism and injects the per-structure slices).
- Return type:
None
tcren.annotation.cgene module#
αβ vs γδ T-cell classification from the TCR constant region (C-gene).
arda annotates the variable V(D)J region but not the constant domain. When a structure
includes an ordered constant domain, aligning each chain to the TCR constant references
(TRAC/TRBC1/TRBC2 → αβ; TRGC/TRDC → γδ) identifies the chain (α/β/γ/δ) unambiguously and
therefore the cell type. This is authoritative for αβ-vs-γδ and independent of the
(occasionally ambiguous, e.g. TRAV/DV) V-gene call. Variable-domain-only chains carry no
constant region and yield no call (cell type "unknown").
- tcren.annotation.cgene.MIN_CONSTANT_SCORE = 80.0#
Minimum local-alignment score to accept a constant-domain match. The V domain alone scores ~30-45 against any constant; a real constant domain scores in the hundreds.
- class tcren.annotation.cgene.ConstantCall(chain_id, gene, chain_class, cell_type, score)[source]#
Bases:
objectA TCR constant-region identification for one chain.
- Parameters:
chain_id (str)
gene (str)
chain_class (str)
cell_type (str)
score (float)
- chain_id: str#
- gene: str#
- chain_class: str#
- cell_type: str#
- score: float#
- tcren.annotation.cgene.classify_chain_constant(sequence, min_score=80.0)[source]#
Identify the constant region of a single chain sequence, if one is present.
- Parameters:
sequence (str)
min_score (float)
- Return type:
ConstantCall | None
- tcren.annotation.cgene.constant_span(sequence, min_score=80.0)[source]#
Return the
(start, end)query span aligning to the best TCR constant region.start/endare 0-based half-open indices intosequence. ReturnsNoneif no constant domain is present (score belowmin_score). The constant region is C-terminal, so callers trim residues withseq_index >= start.- Parameters:
sequence (str)
min_score (float)
- Return type:
tuple[int, int] | None
- tcren.annotation.cgene.classify_constants(structure, min_score=80.0)[source]#
Identify the constant region of every chain that has one.
- Parameters:
min_score (float)
- Return type:
list[ConstantCall]
- tcren.annotation.cgene.cell_type(structure, min_score=80.0)[source]#
Return
"ab","gd"or"unknown"from the constant regions present.γδ wins if any γ/δ constant is found; otherwise αβ if any α/β constant is found; otherwise
"unknown"(no ordered constant domain — e.g. variable-only chains).- Parameters:
min_score (float)
- Return type:
str
tcren.annotation.batch module#
Annotating a whole structure set in one pass, instead of one call per structure.
classify_chains on its own spawns one arda / mmseqs easy-search per structure, each
building a temporary database from a handful of query sequences. Process startup dominates, and the
cost over a set the size of Native2026 is roughly an order of magnitude. Everything here sends every
chain of every structure in a single call per organism and slices the records back out
afterwards, which is why any path resolving to more than one structure goes through it.
This used to live in tcren.paper.helpers, which is the paper’s table-building module and sits at
the top of the stack. Five modules that are nowhere near the top – footprint, shuffle,
recent, orient.superimpose and the descriptor dispatch – were reaching up into it for the
batching, each with a function-local import to keep the cycle legal. The batching is infrastructure,
not paper code, so it lives here now and those imports point downwards. tcren.paper.helpers
re-exports every name, so callers written against the old location keep working.
- tcren.annotation.batch.iter_annotated_set(struct_dir, on_error='skip')[source]#
Parse and chain-type every structure in a folder, yielding annotated structures.
Annotation is batched: one arda call per organism covers every chain of every structure, because the per-call process startup dominates and per-structure annotation is an order of magnitude slower over a set the size of
Native2026. Global chain ids ("<struct_idx>|<chain_id>") keep chains distinct across structures, and the records are sliced back per structure forclassify_chains.- Parameters:
struct_dir (str | Path) – Folder of PDB/mmCIF structures.
on_error (str) –
"skip"(default) drops a structure that fails to parse or annotate;"raise"propagates.
- Yields:
Chain-typed
Structureobjects.
- tcren.annotation.batch.annotate_structure_set(struct_dir, on_error='skip', count_atoms=False)[source]#
Run the tcren pipeline over a folder of PDBs →
(contacts, markup)tables.Replaces the legacy mir batch annotation.
contactsis the stacked TCR↔peptidecontact_table();markupis one row per structure with the CDR3α/CDR3β/peptide sequences + species (the inputs to non-redundancy clustering and the benchmarks). Species is auto-detected per structure by alignment score (human vs mouse). All chains across the whole folder are annotated in a single mmseqs call per organism (the per-call process overhead dominates, so dataset-level batching is far faster than per-structure annotation).When
count_atomsis set, each contact row carries ann_atom_contactsheavy-atom-pair count (needed for atomic-weighted scoring).- Parameters:
struct_dir (str | Path)
on_error (str)
count_atoms (bool)
- Return type:
tuple[DataFrame, DataFrame]
- tcren.annotation.batch.mhc_annotation(struct_dir, ids=None, organism='human', on_error='skip')[source]#
Per-structure MHC allele + class for a folder (tcren mapper) — fully batched.
Replaces the legacy
PDB_MHC_annotationtable.idsrestricts to those PDB ids. Every chain is TCR-typed in one batched arda call (so MHC candidates can be found), then every candidate MHC chain across the whole folder is searched against the MHC reference in a single mmseqseasy_search(mmseqs parallelises internally — no Python process/thread pool, which would either deadlock on fork or re-pay the fixed mmseqs startup cost per structure). Returnspdb.id,mhc.class,mhc.allele,status.- Parameters:
organism (str)
on_error (str)
- Return type:
DataFrame
- tcren.annotation.batch.annotate_batch(structures, arda=None, organisms=('human', 'mouse'), threads=0)[source]#
Annotate every chain of every structure with one mmseqs call per organism.
Public since 2.3.0. Batching matters: arda/mmseqs costs seconds per call, so annotating a 1,000-structure cohort one at a time is minutes of pure index rebuild. Every benchmark that scores a cohort needs this, which is why four downstream scripts were reaching into the private name.
threadscaps the mmseqs thread count for THIS call. It matters whenever batches are annotated concurrently:arda.annotate_sequencesdoes not forward a thread count, soannotate_recordsfalls back to itsthreads=0default, which mmseqs reads as all cores. Twelve concurrent batches on a 16-core machine then ask for 192 threads and spend their time context-switching instead of searching. Passmax(1, cpu_count // concurrency).0keeps the all-cores default, which is right for a single non-concurrent call.Returns
records[struct_idx][organism][chain_id]— the per-structure slices fed toclassify_chains()asprecomputed_records.ardadefaults to the lazily imported backend; pass an instance to reuse one mmseqs handle across a large batch.- Parameters:
threads (int)
- Return type:
list[dict[str, dict[str, dict]]]
- tcren.annotation.batch.iter_typed(structures, organism='human')[source]#
Yield chain-typed structures, annotating a whole set in one batch.
classify_chainsper structure spawns onemmseqs easy-searchper structure, each building a temporary database from a handful of query sequences; the process startup dominates and the cost is roughly an order of magnitude.iter_annotated_set()sends every chain of every structure in a single call per organism, which is why anything resolving to more than one structure – a directory, a glob, a manifest – goes through it. A single file has nothing to batch and takes the direct path.- Parameters:
structures (Path)
organism (str)
MHC mapping#
tcren.mhc.imgt module#
Download and parse MHC allele references (IMGT/HLA + UniProt mouse H-2 + B2M).
Produces MhcAllele records labelled with species, MHC class and chain role
(MHCa = class-I heavy or class-II alpha; MHCb = class-II beta; B2M). Human
alleles come from IMGT/HLA (hla_prot.fasta); mouse H-2 and beta-2-microglobulin come
from reviewed UniProt entries.
- class tcren.mhc.imgt.MhcAllele(allele, locus, mhc_class, chain_role, species, sequence)[source]#
Bases:
objectA reference MHC allele sequence with its functional labels.
- Parameters:
allele (str)
locus (str)
mhc_class (str)
chain_role (str)
species (str)
sequence (str)
- allele: str#
- locus: str#
- mhc_class: str#
- chain_role: str#
- species: str#
- sequence: str#
- tcren.mhc.imgt.download_human(cache_dir, force=False)[source]#
Download IMGT/HLA
hla_prot.fastaintocache_dir.- Parameters:
cache_dir (Path)
force (bool)
- Return type:
Path
- tcren.mhc.imgt.download_mouse(cache_dir, force=False)[source]#
Download mouse H-2 / B2m and human B2M into
cache_dir.- Parameters:
cache_dir (Path)
force (bool)
- Return type:
tuple[Path, Path]
tcren.mhc.reference module#
Build and load the curated MHC reference under database/mhc/.
The reference is a single FASTA (alleles.aa.fasta) whose headers encode the
metadata (allele|locus|mhc_class|chain_role|species) plus a metadata.tsv mirror. It is
built on demand from IMGT by tcren build-mhc-ref and written under
tcren.paths.tcren_home(), not bundled into the wheel.
The mmseqs search index is built on demand into a gitignored cache (mirroring arda’s
commit-FASTA / build-index-on-demand split).
- tcren.mhc.reference.build(species=('human', 'mouse'), cache_dir=PosixPath('/home/runner/work/tcren/tcren/data/mhc_cache'), out_dir=PosixPath('/home/runner/work/tcren/tcren/database/mhc'), force_download=False)[source]#
Download, curate and write the committed MHC reference.
- Parameters:
species (tuple[str, ...]) – Which species to include.
cache_dir (Path) – Where raw downloads are cached (gitignored).
out_dir (Path) – Where the curated
alleles.aa.fasta+metadata.tsvare written.force_download (bool) – Re-download even if cached files exist.
- Returns:
Path to the written
alleles.aa.fasta.- Return type:
Path
- tcren.mhc.reference.reference_fasta(out_dir=PosixPath('/home/runner/work/tcren/tcren/database/mhc'))[source]#
Path to the committed reference FASTA (raise if the reference is not built).
- Parameters:
out_dir (Path)
- Return type:
Path
- tcren.mhc.reference.reference_db(cache_dir=PosixPath('/home/runner/work/tcren/tcren/data/mhc_cache'))[source]#
Path to a compiled, pre-indexed mmseqs DB of the allele reference (built once, cached).
mmseqs easy-search otherwise rebuilds the target DB and its k-mer prefilter index from the ~28k-allele FASTA on every call. Caching createdb saves little; the dominant cost is the prefilter index, so we also run createindex once. Reusing this DB cuts a single-structure MHC search from ~4.5 s to ~0.9 s. Built into the gitignored
data/mhc_cachewhen missing or older than the FASTA.The build is serialized through
arda._locking.build_lock(): tcren is routinely run concurrently against the same cache (one process per SLURM-array task / Nextflow sample), and an unguardedcreatedb+createindexinto the shared path would let every other process search a half-written index. Thecreateindexmarker is written last, so its freshness gates completeness.- Parameters:
cache_dir (Path)
- Return type:
Path
tcren.mhc.mapper module#
Map a structure’s MHC chains to allele / class / role via mmseqs.
Searches each not-yet-typed chain against the curated MHC reference and assigns the best hit’s class (MHCI/MHCII), chain role (MHCa/MHCb/B2M), locus and allele. Class is reconciled across the complex (B2M ⇒ class I; a class-II beta chain ⇒ class II).
- class tcren.mhc.mapper.MhcCall(chain_id, chain_role, mhc_class, allele, locus, species, identity, bits, qstart, qend, tstart, tend, cigar)[source]#
Bases:
objectResult of mapping one chain to the MHC reference.
- Parameters:
chain_id (str)
chain_role (str)
mhc_class (str)
allele (str)
locus (str)
species (str)
identity (float)
bits (float)
qstart (int)
qend (int)
tstart (int)
tend (int)
cigar (str)
- chain_id: str#
- chain_role: str#
- mhc_class: str#
- allele: str#
- locus: str#
- species: str#
- identity: float#
- bits: float#
- qstart: int#
- qend: int#
- tstart: int#
- tend: int#
- cigar: str#
- tcren.mhc.mapper.map_mhc(structure, sensitivity=5.7)[source]#
Map the structure’s MHC chains against the curated reference.
- tcren.mhc.mapper.calls_from_hits(candidates, best, key=None)[source]#
Build reconciled
MhcCall`s for ``candidates`from precomputed mmseqs hits.key(chain) -> strmaps a candidate chain to its key inbest(default the chain id; a batched search uses"<struct_idx>|<chain_id>"). Lets one mmseqs search over many structures’ chains be sliced back per structure — no per-structure mmseqs call.- Parameters:
best (dict[str, dict])
- Return type:
list[MhcCall]
tcren.mhc.domains module#
Canonical MHC groove region definitions.
Loads the bundled mhc_canonical.json: for each "<class>|<role>" key it holds a
canonical mature chain sequence and the 0-based positions of each groove region
(HELIX_A1/HELIX_A2 for class I, HELIX_A1/HELIX_B1 for class II, and
GROOVE_FLOOR). Region boundaries follow established mature-numbering ranges for the
α1/α2 (class I) and α1/β1 (class II) groove domains. Regions are projected onto query
chains in tcren.mhc.regions.
tcren.mhc.regions module#
Project canonical MHC groove regions onto a structure’s MHC chains.
Each MHC chain is aligned (global, BLOSUM62) to the canonical chain for its class/role;
the canonical region positions are then mapped through the alignment onto the chain’s
residues, producing RegionMarkup entries (HELIX_A1,
HELIX_A2/HELIX_B1, GROOVE_FLOOR) in the same schema as the TCR region markup.
- tcren.mhc.regions.partition_chain(chain, mhc_class, chain_role)[source]#
Return groove RegionMarkups for one MHC chain (empty for B2M / unknown roles).
- Parameters:
chain (Chain)
mhc_class (str)
chain_role (str)
- Return type:
list[RegionMarkup]
- tcren.mhc.regions.partition_mhc(structure, calls)[source]#
Assign groove regions to every mapped MHC chain in the structure (in place).
- tcren.mhc.regions.annotate_mhc(structure)[source]#
Map and partition the MHC chains of an (already chain-typed) structure.
Returns the
MhcCalllist and, in place, sets each MHC chain’s type (MHCa/MHCb/B2M), class supertype, allele and groove regions.
- tcren.mhc.regions.annotate_mhc_batch(structures, sensitivity=5.7, threads=1)[source]#
MHC-annotate many (chain-typed) structures with a SINGLE mmseqs search.
Gathers every candidate MHC chain across all structures, runs one
easy_search(mmseqs parallelises internally — no Python threads, no per-structure call), then slices the hits back and applies the calls + groove partitioning to each structure in place. This is the batched equivalent of callingannotate_mhc()per structure, for dataset-scale work.- Parameters:
structures (list[Structure])
sensitivity (float)
threads (int)
- Return type:
None
tcren.mhc.linker module#
Detect and split covalently linked (single-chain) peptides via MHC alignment.
Engineered single-chain pMHC constructs fuse the peptide to an MHC chain through a flexible (usually Gly/Ser-rich) linker, so the peptide is not a separate chain. Aligning each chain to the MHC reference reveals this: the MHC domain aligns, leaving an unaligned terminal segment that — after stripping the linker — is the peptide. This module provides the alignment check and a splitter that lifts such peptides into their own chain.
No covalently linked peptides occur in the bundled TCR3D / PDB datasets (all conventional, separate-chain complexes); this is robustness for engineered and predicted structures.
- class tcren.mhc.linker.MhcAlignmentCheck(chain_id, best_ref, score, query_start, query_end, n_term_extra, c_term_extra)[source]#
Bases:
objectResult of aligning a chain onto the MHC reference (a chain-identity check).
- Parameters:
chain_id (str)
best_ref (str)
score (float)
query_start (int)
query_end (int)
n_term_extra (int)
c_term_extra (int)
- chain_id: str#
- best_ref: str#
- score: float#
- query_start: int#
- query_end: int#
- n_term_extra: int#
- c_term_extra: int#
- property is_mhc: bool#
- tcren.mhc.linker.check_against_mhc(chain)[source]#
Align a chain onto the MHC reference and report coverage / terminal extensions.
- Parameters:
chain (Chain)
- Return type:
- tcren.mhc.linker.detect_linked_peptide(chain, min_len=7, max_len=25)[source]#
Return the residues of a peptide fused to an MHC chain, or
None.Looks for a peptide-length segment (after stripping an adjacent Gly/Ser linker) at the N- or C-terminus of a chain whose remainder aligns to the MHC reference.
- Parameters:
chain (Chain)
min_len (int)
max_len (int)
- Return type:
list | None
- tcren.mhc.linker.split_linked_peptides(structure, peptide_chain_id='p')[source]#
Split covalently linked peptides off their MHC chains, in place.
For each chain carrying a fused peptide, the peptide residues are removed and added as a new PEPTIDE chain. Returns the list of chain ids that were split (empty if none).
- Parameters:
structure (Structure)
peptide_chain_id (str)
- Return type:
list[str]
tcren.mhc.pseudo module#
MHC pseudosequence (MPS) annotation.
NetMHCpan defines a 34-residue “pseudosequence” per allele — the polymorphic groove positions
that contact the peptide (class I: α1/α2 of MHCa; class II: α1 of MHCa + β1 of MHCb). The
committed mhci_pseudo.fa / mhcii_pseudo.fa (see scripts/build_pseudo_fasta.py) hold
the unique pseudosequences.
annotate_pseudo() adds an MPS region to a chain-typed + MHC-annotated structure, on
demand. The 34 pseudo positions are scattered along the chain (not a contiguous motif), so an
mmseqs/local search can’t find them — there is no shared k-mer to seed on. Instead we thread each
candidate 34-mer through the chain with a fitting alignment (gaps in the chain are free, the
pseudosequence may not gap), which recovers the positions because NetMHCpan lists them N→C. The
best-scoring pseudosequence is chosen (one hit), and its identically-matched residues are marked —
across MHCa only for class I, split across MHCa+MHCb for class II, never β2m. Scoring all ~5.4k
pseudosequences this way is ~0.1 s, so no prebuilt index is needed.
- tcren.mhc.pseudo.annotate_pseudo(structure)[source]#
Add an
MPSregion to each groove chain from the best-matching pseudosequence.structuremust already be chain-typed + MHC-annotated. Returns the chosen pseudosequence id (orNoneif there is no MHC). The best hit is selected once over the class groove sequence (MHCa for class I; MHCa+MHCb for class II) and its residues marked per chain.- Parameters:
structure (Structure)
- Return type:
str | None
Contacts and per-residue geometry#
tcren.contacts.geometry module#
Atom-level contact and Cα-distance computation.
Ports the legacy mir compute-pdb-contacts / compute-pdb-geom steps using a
scipy.spatial.cKDTree for the all-atom neighbour search.
- tcren.contacts.geometry.BACKBONE_ATOMS = frozenset({'C', 'CA', 'N', 'O', 'OXT'})#
Main-chain heavy atoms. Everything else (CB onward) belongs to the side chain, which is what a residue’s one-letter identity stands for – and what a residue-level potential prices.
- tcren.contacts.geometry.all_atom_contacts(structure, cutoff=5.0, count_atoms=False, scope='inter', atom_pairs=False, sidechain=False)[source]#
Closest atom contact for each residue pair within
cutoffÅ.For every pair of residues that have at least one heavy-atom pair within
cutoff(inclusive, matching the legacydist <= 5), the row with the minimum atom–atom distance is kept.- Parameters:
structure (Structure) – The (parsed) structure.
cutoff (float) – Contact distance threshold (Å, inclusive).
count_atoms (bool) – When
Trueadd an extran_atom_contactscolumn = the count of heavy-atom pairs withincutofffor that residue pair (always>= 1for any kept row, and>=the single closest-atom row this function keeps). DefaultFalsekeeps the schema and every value byte-identical to the legacy output.scope (str) – Which residue pairs to keep.
"inter"(default) keeps only pairs on different chains, which is what every interface score in the package is built on and what the legacy output contained."intra"keeps only pairs within one chain, and"all"keeps both. Intra-chain pairs include sequence neighbours, which are in contact by covalent geometry rather than by folding — filter on sequence separation before interpreting them (peptide_internal_contacts()does this).sidechain (bool) – When
Trueaddsc.from,sc.toandn_sc_pairs: whether each side puts a side-chain heavy atom withincutoffof the other residue, and how many of the residue pair’s atom pairs are side-chain to side-chain. A 5 Å residue pair whose only atoms in range are the two backbones is not an interaction between those two residue identities, and a residue-level potential that prices it is charging for chemistry that is not happening. The flags are computed over all atom pairs, so they survive the collapse to the closest one.atom_pairs (bool) – When
Truereturn every heavy-atom pair withincutoffinstead of collapsing each residue pair to its closest one. Chemical typing needs this: a salt bridge whose nearest atom pair happens to be Arg CG–Asp OD1 is invisible in the collapsed table, because the charged NH1–OD2 pair it also makes was discarded. Changes the row count, not the schema.
- Returns:
chain.id.from,residue.index.from,chain.id.to,residue.index.to,residue.aa.from,residue.aa.to,atom.from,atom.to,dist(plusn_atom_contactswhencount_atomsis set). Each unordered residue pair appears once, in(chain.id, residue.index)lexicographic order.- Return type:
Columns
- tcren.contacts.geometry.peptide_internal_contacts(structure, cutoff=5.0, min_seq_sep=3, count_atoms=True)[source]#
Contacts within the peptide chain — the term every interface score omits.
Every energy in this package sums over inter-chain contacts only, so a peptide that is held in a particular conformation by its own side chains scores the same as one that is not. This returns those omitted pairs, so that assumption can be tested rather than inherited.
Sequence neighbours are in contact because they are bonded, not because the peptide folded that way, so pairs closer than
min_seq_sepin sequence are dropped; the default of 3 keepsi/i+3and beyond, which is the shortest separation that can carry a side-chain-to-side-chain interaction across a turn. The 5.0 Å default is the same contact definition the rest of the package uses, so an internal contact and an interface contact mean the same thing.- Parameters:
structure (Structure) – The (parsed, annotated) structure; the chain typed
PEPTIDEis used.cutoff (float) – Contact distance threshold (Å, inclusive).
min_seq_sep (int) – Minimum
|i - j|in residue index for a pair to be kept.count_atoms (bool) – Carry the
n_atom_contactsheavy-atom-pair count (defaultTruehere, since the count is the quantity of interest for an internal contact).
- Returns:
The
all_atom_contacts()schema, restricted to the peptide chain. Empty (with schema) when the structure has noPEPTIDEchain.- Return type:
DataFrame
- tcren.contacts.geometry.representative_atom_contacts(structure, kind='ca', cutoff=12.0)[source]#
Inter-chain residue contacts by a single representative atom per residue.
kind="ca"uses Cα (default cutoff 12 Å);kind="cb"uses Cβ with a glycine/ missing-Cβ fallback to Cα (default cutoff 8 Å). Mirrorsall_atom_contacts()’ residue-pair schema (atom.from/atom.tocarry the representative atom kind).- Parameters:
structure (Structure)
kind (str)
cutoff (float)
- Return type:
DataFrame
- tcren.contacts.geometry.ca_distance_matrix(structure)[source]#
Pairwise Cα–Cα distance matrix over all residues with a Cα atom.
- Returns:
(matrix, keys)wherematrix[a, b]is the Cα distance andkeys[a]is the(chain_id, seq_index)of row/columna.- Parameters:
structure (Structure)
- Return type:
tuple[ndarray, list[tuple[str, int]]]
tcren.contacts.definitions module#
Flexible multi-threshold contact definition.
Beyond the legacy single 5 Å all-atom contact (the TCRen parity default, d1), this adds
two coarser residue-level layers: d2 over Cβ atoms (Cα for glycine) and d3 over Cα
atoms. The layers nest from tight side-chain proximity to backbone neighbourhood, giving the
2D maps and scoring a tunable contact model without changing the 5 Å default.
- class tcren.contacts.definitions.ContactDefinition(d1=5.0, d2=8.0, d3=12.0)[source]#
Bases:
objectThree nested contact thresholds (Å).
- Variables:
d1 (float) – closest heavy-atom distance (all-atom contact).
d2 (float) – closest Cβ distance (Cα for glycine / missing Cβ).
d3 (float) – closest Cα distance.
- Parameters:
d1 (float)
d2 (float)
d3 (float)
- d1: float#
- d2: float#
- d3: float#
- tcren.contacts.definitions.multi_contacts(structure, definition=ContactDefinition(d1=5.0, d2=8.0, d3=12.0))[source]#
Stacked inter-chain residue contacts across the three layers.
Returns the union of the
d1/d2/d3residue-pair tables with alayercolumn ("d1"/"d2"/"d3") and the layer’s distance. A residue pair can appear in several layers; callers filter bylayeras needed.- Parameters:
structure (Structure)
definition (ContactDefinition)
- Return type:
DataFrame
tcren.contacts.table module#
Annotate and symmetrise the residue contact table.
Joins per-residue annotations (chain type, region type, region start, amino acid) onto
the raw contacts and mirrors the R rbind(contacts, swapped) symmetrisation, yielding
the fully annotated, bidirectional contact table the contact map is built from.
- tcren.contacts.table.residue_annotation(structure)[source]#
Per-residue annotation table for joining onto contacts.
Columns:
chain.id,residue.index,chain.type,chain.supertype,region.type,region.start,residue.aa.region.type/region.startare null for residues without a region annotation.- Parameters:
structure (Structure)
- Return type:
DataFrame
- tcren.contacts.table.symmetrize(contacts)[source]#
Return contacts plus their from/to-swapped mirror (R
rbindsemantics).- Parameters:
contacts (DataFrame)
- Return type:
DataFrame
- tcren.contacts.table.tidy_contacts(structure, cutoff=5.0, count_atoms=False, atom_pairs=False, sidechain=False)[source]#
Symmetrised, fully annotated contact table for a structure.
Each inter-chain residue contact appears in both directions, with chain type, region type, region start and amino acid attached on both the
fromandtosides — the input totcren.contactmap.ContactMap.When
count_atomsis set, then_atom_contactsper-residue-pair heavy-atom count is carried through (it is symmetric, so it survives the from/to swap unchanged). DefaultFalsekeeps the table byte-identical to the legacy output.atom_pairspasses through toall_atom_contacts(): every heavy-atom pair instead of each residue pair’s closest one. Chemical typing needs it. So doessidechain, which adds thesc.from/sc.to/n_sc_pairsside-chain-participation flags; the twoscflags are swapped along with the rest when the table is symmetrised.- Parameters:
structure (Structure)
cutoff (float)
count_atoms (bool)
atom_pairs (bool)
sidechain (bool)
- Return type:
DataFrame
tcren.contactmap module#
Residue-level contact map and interface partitioning.
A ContactMap wraps the annotated, symmetrised contact table and exposes the
three biological interfaces (TCR↔peptide, TCR↔MHC, peptide↔MHC). The TCR↔peptide
interface is the central object for scoring and reproduces the schema of
data/contact_maps_PDB.csv once chains and regions are annotated.
- tcren.contactmap.TCR_REGIONS: dict[str, set[str] | None] = {'all': None, 'cdr': {'CDR1', 'CDR2', 'CDR3'}, 'cdr+fr': {'CDR1', 'CDR2', 'CDR3', 'FR1', 'FR2', 'FR3'}}#
TCR region sets selectable on the
from(TCR) side of an interface."all"(no filter) is the default and reproduces the legacy behaviour byte-for-byte;"cdr"keeps only the three CDRs;"cdr+fr"adds the FR1–FR3 framework regions (FR4 excluded).
- class tcren.contactmap.ContactMap(pdb_id, contacts, peptide_length=None, peptide_internal=None)[source]#
Bases:
objectAnnotated, symmetrised residue contacts for one structure.
- Parameters:
pdb_id (str)
contacts (DataFrame)
peptide_length (int | None)
peptide_internal (DataFrame | None)
- pdb_id: str#
- contacts: DataFrame#
- peptide_length: int | None#
- peptide_internal: DataFrame | None#
Annotated contacts the peptide makes with itself (
peptide_internal_contacts()), withpos.from/pos.toadded — populated only when built withpeptide_internal=True, and deliberately kept out ofcontactsso every interface selection and every score built on it is unchanged.None= not requested.
- classmethod from_structure(structure, cutoff=5.0, count_atoms=False, peptide_internal=False, atom_pairs=False, sidechain=False)[source]#
Build a contact map from an (annotated) structure.
When
count_atomsis set, the annotated table carries ann_atom_contactsper-residue-pair heavy-atom count column (needed for atomic-weighted scoring). DefaultFalsekeeps the contacts table byte-identical to the legacy output.When
peptide_internalis set, the peptide’s contacts with itself are collected intopeptide_internal(at the 5 Å / 3-residue-separation defaults oftcren.peptide_internal_contacts()— call that directly to vary them). They are needed by the intra-peptide energy term (intra_weightontcren.score_peptides()andtcren.pipeline.run()) and are stored apart fromcontacts, which is unaffected either way.When
sidechainis set,contactscarriessc.from,sc.toandn_sc_pairs: which of the two residues put a side-chain heavy atom within the cutoff, and how many of the pair’s atom pairs are side chain to side chain. A pair whose only atoms in range are the two backbones is not an interaction between those two residue identities.When
atom_pairsis set,contactsholds every heavy-atom pair rather than each residue pair’s closest one. Every interface selection works unchanged (it only filters and adds positions), so this is the maptcren.contact_typestypes from; energies must not be summed over it, since each residue pair then appears many times.- Parameters:
structure (Structure)
cutoff (float)
count_atoms (bool)
peptide_internal (bool)
atom_pairs (bool)
sidechain (bool)
- Return type:
- interface(which, tcr_regions='all')[source]#
Return the contacts of one interface with within-region positions.
- Parameters:
which (Literal['tcr_peptide', 'tcr_mhc', 'peptide_mhc']) –
"tcr_peptide","tcr_mhc"or"peptide_mhc".tcr_regions (str) – which TCR regions to keep on the
from(TCR) side —"all"(default, no filter; legacy behaviour),"cdr"(CDR1–CDR3 only), or"cdr+fr"(CDR1–CDR3 plus FR1–FR3). Has no effect on"peptide_mhc"(no TCR side).
- Returns:
Filtered contacts with added
pos.from/pos.tocolumns.- Return type:
DataFrame
- class tcren.contactmap.ModeCentroid(apex_x, y_alpha, y_beta, sigma_sum, footprint_width_alpha, footprint_width_beta, n_contacts_alpha, n_contacts_beta)[source]#
Bases:
objectLength-invariant binding-mode centroid in graphon coordinates (see
binding_mode()).- Parameters:
apex_x (float)
y_alpha (float)
y_beta (float)
sigma_sum (float)
footprint_width_alpha (float)
footprint_width_beta (float)
n_contacts_alpha (int)
n_contacts_beta (int)
- apex_x: float#
contact-weighted mean loop position, pooled over both loops (~0.47)
- y_alpha: float#
contact-weighted mean peptide position read by CDR3alpha (~0.41)
- y_beta: float#
contact-weighted mean peptide position read by CDR3beta (~0.62)
- sigma_sum: float#
y_alpha + y_beta— the sigma involution reads ~1.0
- footprint_width_alpha: float#
- footprint_width_beta: float#
- n_contacts_alpha: int#
- n_contacts_beta: int#
- tcren.contactmap.registered_map(structure, *, grid=8, target='peptide', metric='distance', cutoff=5.0)[source]#
CDR3×``target`` Cα map resampled onto a fixed
grid``×``gridgraphon grid.Bilinearly resamples the variable-length CDR3α/β × peptide Cα distance map onto normalised coordinates
(i/(L+1), j/(M+1))so loops of different length are directly comparable, averageable and linear-model-ready. Featurisation, not a binder score — the discriminative signal is epitope-identity provenance and collapses to chance under epitope matching (algem monograph E13).- Parameters:
structure – a chain-typed structure (
classify_chainsrun).grid (int) – output grid size
Gper axis.target (str) –
"peptide"(only target currently supported).metric (str) –
"distance"(Cα distances) or"contact"(binary atcutoffÅ).cutoff (float) – contact threshold in Å when
metric="contact".
- Returns:
(2, G, G)stacking the α then β blocks when both loops resolve;(G, G)when only one does;Nonewhen neither loop or the peptide is available.
- tcren.contactmap.binding_mode(structure, *, contact=8.0)[source]#
Length-invariant binding-mode centroid of the CDR3α/β × peptide contact map.
Reduces each loop’s contact map to graphon-coordinate centroids:
apex_x = i/(L+1)(where on the loop the contacts sit, ~0.47 = the apex) andy = j/(M+1)(where on the peptide each loop reads). The σ involution shows up asy_alpha + y_beta ≈ 1(α reads the N-terminal half, β the C-terminal). The two loops are never pooled fory— the split is the point — butapex_xis contact-weighted over both.Characterises the mode, does not discriminate specificity — the gross mode is universal across HLA-A*02:01 9-mers, so do not use it as a same-epitope classifier (algem monograph E19).
- Parameters:
structure – a chain-typed structure.
contact (float) – Cα–Cα contact threshold in Å (the reference 8 Å Cα proxy). NOT comparable to the closest-heavy-atom 5 Å cutoff used by
contacts/score: at 5 Å between Cα atoms a genuine interface makes almost no contacts and this returnsNone.
- Returns:
A
ModeCentroid, orNoneif neither loop makes>= 3contacts with the peptide.- Return type:
ModeCentroid | None
tcren.contact_types module#
Chemical typing of interface contacts — a DSSP-style annotation layer for TCR:pMHC contact maps.
Types each heavy-atom contact in a ContactMap interface from geometry and
atom identity alone: no hydrogens (models and many crystals lack them), no external DSSP binary.
Two schemes ship, and the difference matters:
"v2"(default)salt_bridge,hydrogen_bond,cation_pi,stacking,aromatic,hydrophobic,polar,vdw,other— whereothernow means only “too far to be anything”, never “unrecognised”. Apolarity is decided per atom (a carbon with no bonded N/O) rather than per residue, donors and acceptors are typed, and a contact may carry more than one type — theis_<type>booleans are independent andcontact.typeis only the highest-priority label."v1"The original five-type, residue-level, winner-takes-all scheme, kept byte-for-byte because the frozen recognition models in
tcren.recognitionwere trained on itsct_*counts.
Why v2 exists. Measured on tests/assets/pdb/{1ao7,1bd2,2ckb,5m01,6bj3}, v1 typed 72.3% of
TCR:peptide contacts as other — not because those contacts are featureless but because of four
specific gaps. 59% of the other rows were mixed C–O or C–N pairs, for which v1 had no class at
all. Every C–C other row failed a residue-level apolarity test that excludes Tyr — the most
common TCR interface residue — along with the aliphatic CB/CG/CD of Arg, Lys and Gln. A further 29
N–O and O–O pairs sat between 3.5 Å and 5 Å, outside a heavy-atom H-bond cutoff that is strict when
hydrogens are absent. And ring stacking was measured in tcren.stacking but never joined here.
Seeing more than one atom pair. all_atom_contacts() collapses each
residue pair to its closest atom pair, which hides a salt bridge whose nearest contact happens to be
between two carbons. Pass atom_pairs=True there, or use residue_pair_types(), to type from
every atom pair a residue pair makes.
- tcren.contact_types.STACK_CENTROID_MAX = 5.5#
centroids close enough to interact, and either near-parallel (a face-to-face or parallel-displaced stack) or near-perpendicular (edge-to-face / T-shaped).
- Type:
Ring-pair geometry accepted as a stack (see
tcren.stacking.ring_stacking())
- tcren.contact_types.TYPES_V2 = ('salt_bridge', 'hydrogen_bond', 'cation_pi', 'stacking', 'aromatic', 'hydrophobic', 'polar', 'vdw', 'other')#
v2 types, in the priority order
classify_contacts()uses for thecontact.typelabel.
- tcren.contact_types.TYPES_V1 = ('salt_bridge', 'hydrogen_bond', 'aromatic', 'hydrophobic', 'other')#
v1 types, frozen — the recognition models’
ct_*features are counts over these.
- tcren.contact_types.stacked_pairs(structure)[source]#
Residue pairs whose aromatic rings are arranged as a stack, keyed like a contact table.
A contact potential scores a residue pair by identity, so it cannot tell two rings face to face at 3.5 Å from the same two residues brushing past edge-on.
tcren.stacking.ring_stacking()measures that geometry; this turns it into the predicateclassify_contacts()needs.- Returns:
A set of
(chain.id.from, residue.index.from, chain.id.to, residue.index.to)tuples, canonically ordered to matchall_atom_contacts().- Return type:
set[tuple]
- tcren.contact_types.classify_contacts(interface_df, scheme='v2', stacking=None)[source]#
Add chemical typing to an interface frame.
- Parameters:
interface_df (DataFrame) – an interface frame from
ContactMap.interface(), carryingresidue.aa.from/to,atom.from/toanddist.scheme (str) –
"v2"(default) or the frozen"v1"— see the module docstring.stacking (set[tuple] | None) – residue pairs to mark as
stacking, fromstacked_pairs(). Ring geometry needs coordinates, which a contact frame does not carry, so it is passed in.
- Returns:
The frame with a
contact.typecolumn (the highest-priority label) and, for"v2", anis_<type>boolean per type — a contact can be both a salt bridge and a hydrogen bond, and collapsing that to one label loses a real interaction.- Raises:
ValueError – for an unknown
scheme.- Return type:
DataFrame
- tcren.contact_types.residue_pair_types(structure, interface='tcr_peptide', tcr_regions='all', cutoff=5.0)[source]#
Per residue pair, the union of the types its atom pairs make.
This is the form the collapsed contact table cannot give: a residue pair is credited with a salt bridge if any of its atom pairs makes one, not only if its closest pair happens to.
- Parameters:
structure – a chain-typed, annotated
Structure.interface (str) – interface name (
"tcr_peptide","tcr_mhc","peptide_mhc").tcr_regions (str) – passed through to
ContactMap.interface().cutoff (float) – contact distance threshold (Å).
- Returns:
One row per residue pair with
dist(the closest atom pair’s),contact.typeand theis_<type>booleans.- Return type:
DataFrame
- tcren.contact_types.contact_type_counts(cm, interface='tcr_peptide', tcr_regions='all', scheme='v1', structure=None, cutoff=5.0)[source]#
Per-type contact counts + distinct residue-pair counts for one interface.
- Parameters:
cm – a
ContactMap(used by"v1";"v2"needsstructure).interface (str) – interface name (
"tcr_peptide","tcr_mhc","peptide_mhc").tcr_regions (str) – passed through to
ContactMap.interface().scheme (str) – defaults to the frozen
"v1"here, unlikeclassify_contacts(), because these counts feed the trained recognition models. Pass"v2"for the current typing.structure – the source structure — required for
"v2", which needs every atom pair and the ring geometry, neither of which a collapsed contact map carries.cutoff (float) – contact distance threshold for the
"v2"rebuild (Å).
- Returns:
Mapping with
n_<type>(contacts of each type) andpairs_<type>(distinct residue-residue pairs with >=1 contact of that type), e.g.pairs_hydrogen_bondis the documentedn_hbondfeature. Under"v2"a contact is counted under every type it satisfies, so then_*values do not sum to the contact count.- Raises:
ValueError – for
scheme="v2"without astructure.- Return type:
dict[str, int]
- tcren.contact_types.UNTYPED = ('vdw', 'other')#
Types that carry no chemistry beyond proximity — what
type_weights()drops.
- tcren.contact_types.type_weights(typed, drop=('vdw', 'other'))[source]#
0/1 per-contact weights that keep only chemically-typed contacts.
The review’s fallback, and the cheap half of a type-aware potential: rather than re-derive the matrix conditioned on the contact type, use the type to discard pairs that are within 5 Å but make no interaction — a contact map built on proximity alone counts them the same as a salt bridge. Feed the result to
score_peptides(..., weights=...).- Parameters:
typed (DataFrame) – a frame carrying the
is_<type>booleans, fromclassify_contacts()withscheme="v2"or fromresidue_pair_types().drop (tuple[str, ...]) – types to zero out. The default drops only the two that mean “nothing but proximity”.
- Returns:
One float (0.0 or 1.0) per row of
typed, in its row order.- Raises:
ValueError – if the frame carries no
is_<type>columns (it was typed under"v1").- Return type:
ndarray
tcren.stacking module#
Ring-stacking geometry between residue side chains.
A contact potential scores a pair of residues by their identities and nothing else, so it treats two rings lying face to face at 3.5 Å exactly like the same two residues brushing past edge-on. Stacking is a directional interaction and that difference is the whole of it. This module measures it from coordinates instead: how far apart two ring centroids are, how nearly parallel the ring planes are, and how far the rings are displaced sideways.
Proline is included among the rings although it is not aromatic. Its pyrrolidine ring packs face-on against aromatic side chains through CH–pi contacts, and leaving it out would miss exactly the interaction this module exists to measure.
The readout is deliberately geometric and carries no energy. Nothing here says a stack is worth some number of kT; it says the rings are or are not arranged the way a stack is.
- tcren.stacking.RING_ATOMS: dict[str, tuple[str, ...]] = {'HIS': ('CG', 'ND1', 'CD2', 'CE1', 'NE2'), 'PHE': ('CG', 'CD1', 'CD2', 'CE1', 'CE2', 'CZ'), 'PRO': ('N', 'CA', 'CB', 'CG', 'CD'), 'TRP': ('CD2', 'CE2', 'CE3', 'CZ2', 'CZ3', 'CH2'), 'TYR': ('CG', 'CD1', 'CD2', 'CE1', 'CE2', 'CZ')}#
Ring atoms per residue. Six-membered rings for the aromatics, the imidazole for His, the pyrrolidine for Pro. Trp is represented by its six-membered ring, which is the face that stacks.
- class tcren.stacking.Ring(chain_id, seq_index, resname, centroid, normal)[source]#
Bases:
objectOne side-chain ring: where it is and which way it faces.
- Parameters:
chain_id (str)
seq_index (int)
resname (str)
centroid (ndarray)
normal (ndarray)
- chain_id: str#
- seq_index: int#
- resname: str#
- centroid: ndarray#
- normal: ndarray#
- class tcren.stacking.RingPair(a, b, centroid_distance, interplanar_angle, vertical, lateral)[source]#
Bases:
objectGeometry of two rings, in the terms that distinguish a stack from a brush-past.
- Variables:
centroid_distance (float) – Between ring centres (Å).
interplanar_angle (float) – Between the ring planes (degrees, 0–90). Near 0 is face-to-face, near 90 is edge-to-face.
vertical (float) – Centroid separation along the first ring’s normal (Å) — the gap between the planes.
lateral (float) – Centroid separation within that plane (Å) — how far the rings slide past each other. A parallel-displaced stack has a small vertical and a lateral of a couple of Å; a perfectly stacked pair has both small.
- Parameters:
- centroid_distance: float#
- interplanar_angle: float#
- vertical: float#
- lateral: float#
- tcren.stacking.ring_of(residue, chain_id)[source]#
The ring of one residue, or
Noneif it has none or is missing ring atoms.
- tcren.stacking.rings(source)[source]#
Every ring in a structure or a single chain, in residue order.
- tcren.stacking.ring_stacking(source, cutoff=7.5, min_seq_sep=1)[source]#
All ring pairs whose centroids fall within
cutoff.- Parameters:
source (Structure | Chain) – A parsed structure, or one chain of it.
cutoff (float) – Maximum centroid separation (Å). The default is generous: a stack sits near 5 Å, and pairs beyond that are worth seeing in order to say they are not stacks.
min_seq_sep (int) – Minimum
|i - j|for two rings on the same chain, so that sequence neighbours held together by the backbone are not reported as stacks.
- Returns:
chain and residue identifiers on both sides, then
centroid_distance,interplanar_angle,vertical,lateral.- Return type:
One row per pair, sorted by centroid distance
tcren.torsions module#
Backbone torsion angles (φ, ψ, ω) per residue, and the CDR3 loops in particular.
The contact map says which residues touch; the torsions say whether the backbone that puts them there is one a real protein would adopt. That distinction is what makes torsions useful against generated structures: a predictor can seat a side chain in a plausible contact while placing its backbone in a region of the Ramachandran map that crystals essentially never visit.
Angles follow the IUPAC convention and are returned in degrees in (-180, 180]:
φ= C(i−1) – N(i) – Cα(i) – C(i)ψ= N(i) – Cα(i) – C(i) – N(i+1)ω= Cα(i−1) – C(i−1) – N(i) – Cα(i) (≈ 180° trans, ≈ 0° cis-proline)
φ is undefined for the first residue of a chain and ψ for the last, so those come back as nan
rather than being silently dropped — a caller counting residues must see the gap.
Example
>>> import tcren
>>> s = tcren.parse_structure("1ao7.pdb.gz", pdb_id="1ao7")
>>> tcren.annotation.classify_chains(s)
>>> df = cdr3_torsions(s)
>>> sorted(df.columns)[:3]
['aa', 'chain.id', 'chain.type']
- tcren.torsions.dihedral(p0, p1, p2, p3)[source]#
Signed dihedral about the
p1–p2axis, in degrees in(-180, 180].Returns
nanif any coordinate is missing, so a residue with an unresolved backbone atom produces a gap rather than a fabricated angle.- Return type:
float
- tcren.torsions.residue_torsions(prev, cur, nxt)[source]#
(phi, psi, omega)in degrees for one residue given its sequence neighbours.
- tcren.torsions.chain_torsions(chain, structure_id='')[source]#
Per-residue
phi/psi/omegafor one chain.Residues are taken in
seq_indexorder, so a residue whose neighbour is unresolved gets ananangle: the peptide bond across a chain break does not exist and must not be invented.- Parameters:
chain (Chain)
structure_id (str)
- Return type:
DataFrame
- tcren.torsions.cdr3_torsions(structure, region='CDR3', drop_incomplete=True)[source]#
Torsions of the CDR3 residues of every TCR chain in
structure.The structure must already be chain-typed (
tcren.annotation.classify_chains()), since the CDR3 span comes from the region markup.- Parameters:
structure (Structure) – a chain-typed structure.
region (str) – region markup type to select (
"CDR3";"CDR1"/"CDR2"also work).drop_incomplete (bool) – drop residues whose φ or ψ is
nan(chain termini, breaks). SetFalseto see the gaps.
- Returns:
One row per selected residue with
pdb.id, chain.id, chain.type, loop, seq_index, aa, phi, psi, omega. Empty (with the right schema) if the structure has no typed TCR chain.- Return type:
DataFrame
tcren.geometry module#
Closed-form CDR3 Ω-loop reachability — the feasibility filter.
The CDR3 loop is a chain of n = L + 1 virtual Cα–Cα bonds of length BOND_CA, pinned at
its neck separation R (the cdr3{a,b}_ext frame descriptor). Pure geometry then bounds how far
the loop’s apex can reach from the neck axis, independent of sequence or energy:
reach_max()— the maximum apex reach for a loop of lengthLpinned atR.reachability_floor()— the shortest loop that can span a target at distanced.span_saturation()— the fraction of maximal reach a structure actually uses,reach / reach_max;> 1is geometrically impossible and real repertoires sit well below 1 (~0.3–0.5 at CDR3 length 10–20), i.e. with large conformational slack.
These are feasibility / mode descriptors, not binder classifiers — backbone Cα geometry does not
discriminate binders (every apparent signal was a provenance / epitope confound). Bill them as
structure -> geometry featurisation only.
Ported from model/src/Feasibility.jl of the 2026-tcren-algem monograph.
- tcren.geometry.BOND_CA = 3.8#
Virtual Cα–Cα bond length (Å) used for the loop chain.
- tcren.geometry.reach_max(length, neck, bond=3.8)[source]#
Maximum apex reach
sqrt(((L+1)b/2)^2 - (R/2)^2)of a length-lengthloop pinned atneck.- Parameters:
length (int) – CDR3 loop length
L(number of residues).neck (float) – neck separation
Rin Å (theextframe descriptor).bond (float) – virtual Cα–Cα bond length (default
BOND_CA).
- Returns:
Maximum reach in Å, or
0.0if the loop is too short to span the neck.- Return type:
float
- tcren.geometry.reachability_floor(distance, neck, bond=3.8)[source]#
Shortest loop length that can reach a target at
distancefrom a neck of separationneck.Inverts
reach_max(): the minimumLwithreach_max(L, neck) >= distance.- Parameters:
distance (float)
neck (float)
bond (float)
- Return type:
int
- tcren.geometry.span_saturation(structure)[source]#
Per-loop
reach / reach_maxforcdr3aandcdr3bof a chain-typed structure.Composes the existing CDR3 frame descriptors:
reach(loop-centroid distance from the groove origin) andext(neck separationR), withLthe resolved CDR3 length.> 1flags an infeasible pose (the loop cannot reach that far); real structures sit ~0.3–0.5. ReturnsNaNfor a loop whose frame or CDR3 span is undefined.Note
The structure must be chain-typed (
classify_chains) so its CDR3 regions are populated.- Return type:
dict[str, float]
- tcren.geometry.cdr3_internal_coords(structure, loop='cdr3b')[source]#
Virtual-bond internal coordinates (θ, τ, neck R) of a CDR3 loop’s Cα trace.
A thin
structure -> coordinatesextractor: the loop’s model (torsion sampling, closure, ensemble entropy) lives in the algem monograph’s Julia code and does not migrate — tcren only hands off the intrinsic internal coordinates of the observed loop. Reuses the CDR3 region markup (classify_chains) for the span and the shared_dihedralprimitive for torsions.- Parameters:
structure – a chain-typed structure.
loop (str) –
"cdr3a"(TRA) or"cdr3b"(TRB).
- Returns:
A
LoopInternalCoords, orNoneif the loop has fewer than 4 resolved Cα (a torsion needs 4 points).- Return type:
LoopInternalCoords | None
- class tcren.geometry.LoopInternalCoords(bonds, angles, torsions, neck, n_ca)[source]#
Bases:
objectVirtual-bond internal coordinates of a CDR3 Cα trace (the input to the Julia loop model).
A Cα chain of
Npoints hasN-1bonds,N-2bond angles (interior vertices) andN-3pseudo-torsions — the inverse of a discrete-Frenet reconstruction.neckis the end-to-end span|p_last - p_first|(theextframe descriptor). Angles and torsions are in radians.- Parameters:
bonds (object)
angles (object)
torsions (object)
neck (float)
n_ca (int)
- bonds: object#
(N-1,) virtual Cα–Cα bond lengths, Å
- angles: object#
(N-2,) Cα–Cα–Cα pseudo bond angles θ, radians
- torsions: object#
(N-3,) Cα pseudo-dihedrals τ, radians in (−π, π]
- neck: float#
end-to-end span
R = |p_last − p_first|, Å
- n_ca: int#
tcren.clashes module#
Steric-clash detection at the peptide interface.
interface_clashes() counts heavy-atom van der Waals overlaps between the peptide chain and its
TCR/MHC partners — the signature of a forced or wrong-register pose, e.g. an AlphaFold / TCRmodel
peptide swap that seats the peptide non-physically. A clash is a non-bonded heavy-atom pair whose
separation is shorter than the sum of their Bondi vdW radii by more than a tolerance (Molprobity uses
0.4 Å for a “bad” clash; 0.6 Å here marks a severe one).
This is a structure-quality check: a generated complex with a heavy clash burden is geometrically
non-physical, so its contact energy is read off a distorted interface (see
tcren.refine.register for the register-specific diagnostic and correction). The pairwise
overlap scan is a native _geom kernel; the numpy implementation behind it
(_clash_pairs_numpy()) is kept as the reference and as a fallback where the extension is
unavailable.
- tcren.clashes.BONDI_RADII: dict[str, float] = {'BR': 1.85, 'C': 1.7, 'CL': 1.75, 'F': 1.47, 'H': 1.2, 'I': 1.98, 'N': 1.55, 'O': 1.52, 'P': 1.8, 'S': 1.8, 'SE': 1.9}#
Bondi van der Waals radii (Å) by element symbol;
_DEFAULT_RADIUScovers anything unlisted.
- class tcren.clashes.ClashPair(peptide_residue, partner_residue, partner_chain_type, overlap)[source]#
Bases:
objectA single peptide↔partner clashing residue pair.
- Parameters:
peptide_residue (str)
partner_residue (str)
partner_chain_type (str)
overlap (float)
- peptide_residue: str#
- partner_residue: str#
- partner_chain_type: str#
- overlap: float#
- class tcren.clashes.ClashReport(n_clashes, n_severe, max_overlap, clash_score, by_partner, worst, n_peptide_atoms)[source]#
Bases:
objectInterface steric-clash summary for a chain-typed complex.
- Variables:
n_clashes (int) – Heavy-atom pairs overlapping by more than
tolerance.n_severe (int) – Subset overlapping by more than
severe.max_overlap (float) – Largest single overlap (Å);
0.0if clash-free.clash_score (float) – Sum of all overlaps >
tolerance(Å) — a total-burden measure.by_partner (dict[str, int]) – Clash count per partner
chain_type(TRA/TRB/MHCa/…).worst (tuple[tcren.clashes.ClashPair, ...]) – Up to
topworst clashing residue pairs, largest overlap first.n_peptide_atoms (int) – Heavy atoms on the peptide chain (denominator context).
- Parameters:
n_clashes (int)
n_severe (int)
max_overlap (float)
clash_score (float)
by_partner (dict[str, int])
worst (tuple[ClashPair, ...])
n_peptide_atoms (int)
- n_clashes: int#
- n_severe: int#
- max_overlap: float#
- clash_score: float#
- by_partner: dict[str, int]#
- n_peptide_atoms: int#
- property clashing: bool#
True if any pair overlaps by more than the tolerance.
- tcren.clashes.interface_clashes(structure, *, tolerance=0.4, severe=0.6, top=8)[source]#
Detect steric clashes between the peptide chain and its partners.
Two heavy atoms on different chains clash when their separation is shorter than the sum of their Bondi vdW radii by more than
toleranceÅ. Only peptide↔partner pairs are examined (partners = every non-peptide chain); intra-chain and peptide-internal pairs are ignored, so no bonded pair is ever counted.- Parameters:
structure (Structure) – A chain-typed complex with a peptide chain (
chain_type == 'PEPTIDE').tolerance (float) – vdW overlap (Å) above which a pair counts as a clash (Molprobity
0.4).severe (float) – overlap (Å) above which a clash is also counted as severe.
top (int) – How many worst residue pairs to return.
- Returns:
A
ClashReport.- Raises:
ValueError – If the structure has no peptide chain.
- Return type:
The descriptor catalogue#
tcren.descriptors package#
Descriptors: the catalogue, the computation, and the batch dispatch, in three layers.
cataloguewhat every column is – names, families, invariance classes, units, definitions and known defects. Pure data, no arithmetic.
computestructure -> values for the interface block, and the calls out to the energetics, topology, potts and kinetics modules that own the rest.
tablea whole structure set -> one row each, with the single batched annotation pass and the process pool.
tcren.recognition re-exports all three under the name every caller already uses.
tcren.recognition module#
Structure -> descriptors. The public name; the implementation is tcren.descriptors.
Split into three layers on 2026-09-01 because this module had grown to 1,151 lines doing three different jobs – holding the catalogue, computing the interface block, and dispatching a batch – and a caller that only wanted to ask what a column means was importing all of it. The names are unchanged and every existing import keeps working:
tcren.descriptors.catalogue–DESCRIPTORS,INVARIANCE,DETAIL,STATUS,FAMILIES,descriptors(). Data and selection, no arithmetic.tcren.descriptors.compute–recognition_features()and the interface terms this package computes itself; it callstcren.pipeline,tcren.footprint,tcren.pottsandtcren.mechanicsfor the other families rather than reimplementing them.tcren.descriptors.table–recognition_table(), the batched set-level path.
New code may import from the three directly; nothing is deprecated here.
tcren.descriptors.catalogue module#
The descriptor catalogue: what every emitted column is, and how to select a subset of them.
Data and selection only. Nothing here computes a descriptor, and nothing here imports a module
that does – the one exception is the two tuples of names it splats into DESCRIPTORS, which
are strings, not arithmetic. That separation is the point: a caller that only wants to know what a
column means, what it is invariant under or which family it belongs to pays for none of the
structure parsing, contact building or potential loading that computing one would cost.
The layers above this one are tcren.descriptors.compute, which turns a structure into the
values, and tcren.descriptors.table, which runs that over a set. tcren.recognition is
kept as the public name and re-exports all three.
- tcren.descriptors.catalogue.RECOGNITION_FEATURES = ('extent', 'chain_balance', 'pitch', 'crossing', 'crossing_signed', 'dock_d', 'dock_torsion', 'dock_tcr_uy', 'dock_tcr_uz', 'dock_mhc_uy', 'dock_mhc_uz', 'Phi_cdr12', 'Phi_cdr3a', 'Phi_cdr3b', 'Phi_tcr_pep', 'Phi_tcr_mhc', 'Phi_pep_mhc', 'dPhi_tcr_pep', 'dPhi_pep_mhc', 'dPhi_pep_soft', 'varPhi_pep_soft', 'dPhi_tcr_soft', 'varPhi_tcr_soft', 'dPhi_tra_soft', 'dPhi_trb_soft', 'n_contacts_tp', 'n_pep_contacted', 'n_contacts_tm', 'ct_tp_salt_bridge', 'ct_tm_salt_bridge', 'ct_tm_hydrogen_bond', 'ct_tp_aromatic', 'ct_tm_aromatic', 'ct_tp_hydrophobic', 'ct_tm_hydrophobic', 'ct_tp_other', 'ct_tm_other', 'n_hbond', 'burial', 'mhc_class_bin')#
40 columns, a subset of the 164-row catalogue.
Every statistical-potential energy is named
Phi_*— there is one potential per interface (TCRen on TCR:peptide, MJ on the two presentation interfaces), so the potential’s name does not belong in the column’s.dindPhi_*is the reference difference ΔΦ = Φ(sequence) − Φ(reference), never a derivative. Two exact duplicates were dropped in the 2026-07-28 audit:e_tcr_mhc(the same number asPhi_tcr_mhc) andct_tp_hydrogen_bond(the same number asn_hbond, which is the name Eq. Q uses).- Type:
The core descriptor block
recognizeemits
- tcren.descriptors.catalogue.INTERFACE_SYMMETRY_FEATURES = ('cdr3_dominance', 'cdr3_ab_imbalance', 'chain_cdr_imbalance')#
peptide contact counts (not energies), emitted as extra
recognizeoutput columns — not part ofRECOGNITION_FEATURES(that 40-column block is fixed).cdr3_dominance= CDR3(α+β) share of CDR contacts (higher = CDR3-dominated, oriented positive);cdr3_ab_imbalance=|CDR3α−CDR3β|normalised (absolute);chain_cdr_imbalance=|α−β|whole-CDR normalised (absolute). See_interface_symmetry().- Type:
Interface-symmetry descriptors from per-loop TCR
- tcren.descriptors.catalogue.TCR_PLACEMENT_FEATURES = ('height', 'shift_u', 'shift_w', 'offset')#
Where the receptor body sits over the groove (
tcren.docking.tcr_placement()), emitted as extra output columns — not part ofRECOGNITION_FEATURES(that 40-column block is fixed).height= elevation of the CDR centroid above the groove plane,shift_u/shift_wits in-plane displacement from the peptide centroid along the groove long/short axes,offsetthe in-plane distance. These are the translational degrees of freedom no docking angle can see, and the mechanism behind the coverage entropy (uniform coverage = riding low).
- tcren.descriptors.catalogue.PEPTIDE_INTERNAL_FEATURES = ('Phi_pep_int', 'n_pep_int')#
The intra-peptide term, emitted as extra
recognize --fulloutput columns — not part ofRECOGNITION_FEATURES(that 40-column block is fixed).Phi_pep_int= the peptide’s MJ contact energy with itself (tcren.intra_peptide_energy()), the term every interface sum omits;n_pep_int= how many such contacts there are. Both are properties of the pMHC alone — no receptor enters them — so they carry cohort identity; seeDESCRIPTORS.
- tcren.descriptors.catalogue.FULL_FEATURES = ('extent', 'chain_balance', 'pitch', 'crossing', 'crossing_signed', 'dock_d', 'dock_torsion', 'dock_tcr_uy', 'dock_tcr_uz', 'dock_mhc_uy', 'dock_mhc_uz', 'Phi_cdr12', 'Phi_cdr3a', 'Phi_cdr3b', 'Phi_tcr_pep', 'Phi_tcr_mhc', 'Phi_pep_mhc', 'dPhi_tcr_pep', 'dPhi_pep_mhc', 'dPhi_pep_soft', 'varPhi_pep_soft', 'dPhi_tcr_soft', 'varPhi_tcr_soft', 'dPhi_tra_soft', 'dPhi_trb_soft', 'n_contacts_tp', 'n_pep_contacted', 'n_contacts_tm', 'ct_tp_salt_bridge', 'ct_tm_salt_bridge', 'ct_tm_hydrogen_bond', 'ct_tp_aromatic', 'ct_tm_aromatic', 'ct_tp_hydrophobic', 'ct_tm_hydrophobic', 'ct_tp_other', 'ct_tm_other', 'n_hbond', 'burial', 'mhc_class_bin', 'cdr3a_reach', 'cdr3a_ou', 'cdr3a_ow', 'cdr3a_on', 'cdr3a_au', 'cdr3a_aw', 'cdr3a_an', 'cdr3a_topep', 'cdr3a_ext', 'cdr3b_reach', 'cdr3b_ou', 'cdr3b_ow', 'cdr3b_on', 'cdr3b_au', 'cdr3b_aw', 'cdr3b_an', 'cdr3b_topep', 'cdr3b_ext')#
the core recognition descriptors + the 18 CDR3-frame descriptors.
The 12 “matrix-swap” columns (
tcren_{g}/mj_{g}/d_{g}forg∈ {tp, cdr12, cdr3a, cdr3b}) were removed in the 2026-07-28 audit. Thetcren_*four were exact duplicates of thePhi_*energies; themj_*four scored TCR:peptide contacts under the generic MJ potential, which is not the potential this method uses on that interface, and thed_*four were their difference. Nothing consumed them.- Type:
The full feature vector
- tcren.descriptors.catalogue.DESCRIPTORS: dict[str, tuple[str, bool]] = {'D1_cell': ('topology', True), 'D2_cell': ('topology', True), 'D2_loop': ('topology', True), 'D2_pep24': ('topology', True), 'H_cell': ('topology', True), 'H_loop': ('topology', True), 'J_cell': ('topology', True), 'K_shear': ('kinetics', True), 'K_tens': ('kinetics', True), 'L_canon': ('topology', True), 'Phi_cdr12': ('energetics', True), 'Phi_cdr3a': ('energetics', True), 'Phi_cdr3b': ('energetics', True), 'Phi_pep_int': ('energetics', False), 'Phi_pep_mhc': ('energetics', False), 'Phi_tcr_mhc': ('energetics', True), 'Phi_tcr_pep': ('energetics', True), 'S_cell': ('topology', True), 'S_tot': ('kinetics', True), 'ab_imb': ('topology', True), 'ab_imb_mhc': ('topology', True), 'ab_imb_pep': ('topology', True), 'aniso': ('kinetics', True), 'burial': ('interface', True), 'ca_cb_agreement_tm': ('topology', True), 'ca_cb_agreement_tp': ('topology', True), 'cdr3_ab_imbalance': ('interface', True), 'cdr3_dominance': ('interface', True), 'cdr3a_an': ('placement', True), 'cdr3a_au': ('placement', True), 'cdr3a_aw': ('placement', True), 'cdr3a_ext': ('placement', True), 'cdr3a_on': ('placement', True), 'cdr3a_ou': ('placement', True), 'cdr3a_ow': ('placement', True), 'cdr3a_reach': ('placement', True), 'cdr3a_topep': ('placement', True), 'cdr3b_an': ('placement', True), 'cdr3b_au': ('placement', True), 'cdr3b_aw': ('placement', True), 'cdr3b_ext': ('placement', True), 'cdr3b_on': ('placement', True), 'cdr3b_ou': ('placement', True), 'cdr3b_ow': ('placement', True), 'cdr3b_reach': ('placement', True), 'cdr3b_topep': ('placement', True), 'chain_balance': ('interface', True), 'chain_cdr_imbalance': ('interface', True), 'clash_score': ('interface', True), 'co_mhc': ('topology', True), 'co_pep': ('topology', True), 'couple_mhc': ('kinetics', True), 'couple_pep': ('kinetics', True), 'couple_tcr': ('kinetics', True), 'couple_total': ('kinetics', True), 'crossing': ('placement', True), 'crossing_signed': ('placement', True), 'ct_tm_aromatic': ('interface', True), 'ct_tm_hydrogen_bond': ('interface', True), 'ct_tm_hydrophobic': ('interface', True), 'ct_tm_other': ('interface', True), 'ct_tm_salt_bridge': ('interface', True), 'ct_tp_aromatic': ('interface', True), 'ct_tp_hydrophobic': ('interface', True), 'ct_tp_other': ('interface', True), 'ct_tp_salt_bridge': ('interface', True), 'dPhi_pep_mhc': ('energetics', False), 'dPhi_pep_soft': ('energetics', True), 'dPhi_tcr_pep': ('energetics', True), 'dPhi_tcr_soft': ('energetics', True), 'dPhi_tra_soft': ('energetics', True), 'dPhi_trb_soft': ('energetics', True), 'degree_evenness_tp': ('topology', True), 'dock_d': ('placement', True), 'dock_mhc_uy': ('placement', True), 'dock_mhc_uz': ('placement', True), 'dock_tcr_uy': ('placement', True), 'dock_tcr_uz': ('placement', True), 'dock_torsion': ('placement', True), 'exp_lost': ('kinetics', True), 'extent': ('interface', True), 'fp_b0_frac_r7': ('topology', True), 'fp_b0_frac_r8': ('topology', True), 'fp_b0_r7': ('topology', True), 'fp_b0_r8': ('topology', True), 'fp_b1_r7': ('topology', True), 'fp_b1_r8': ('topology', True), 'fp_chi_r7': ('topology', True), 'fp_chi_r8': ('topology', True), 'frac_robust': ('kinetics', True), 'frac_well_coordinated_tp': ('topology', True), 'g_alg_conn': ('topology', True), 'g_assort': ('topology', True), 'g_comp_frac': ('topology', True), 'g_cyclo_frac': ('topology', True), 'g_even_pmhc': ('topology', True), 'g_even_tcr': ('topology', True), 'g_loop_even': ('topology', True), 'g_loop_overlap': ('topology', True), 'h0_pers_ent': ('topology', True), 'height': ('placement', True), 'lam_max': ('kinetics', True), 'lam_min': ('kinetics', True), 'log_lik': ('potts', True), 'log_z': ('potts', True), 'm_erank_tm': ('topology', True), 'm_erank_tp': ('topology', True), 'm_face_tm': ('topology', True), 'm_face_tp': ('topology', True), 'm_gap_tm': ('topology', True), 'm_gap_tp': ('topology', True), 'mean_margin': ('kinetics', True), 'mhc_class_bin': ('interface', False), 'n_clashes': ('interface', True), 'n_contacts': ('potts', True), 'n_contacts_tm': ('interface', True), 'n_contacts_tp': ('interface', True), 'n_hbond': ('interface', True), 'n_interface': ('kinetics', True), 'n_loop_contacts': ('interface', True), 'n_mhc_contacts': ('interface', True), 'n_pep_contacted': ('interface', True), 'n_pep_contacts': ('interface', True), 'n_pep_int': ('interface', False), 'n_spring': ('kinetics', True), 'neg_energy': ('potts', True), 'offset': ('placement', True), 'p_cdr3_pep': ('topology', True), 'p_germ_mhc': ('topology', True), 'partcoef_pmhc': ('topology', True), 'partcoef_tcr': ('topology', True), 'pep_cov_centre': ('topology', True), 'pep_cov_d2n': ('topology', True), 'pep_cov_even': ('topology', True), 'pep_cov_frac': ('topology', True), 'pep_cov_spread': ('topology', True), 'pep_free_frac': ('topology', True), 'pitch': ('placement', True), 'psi': ('potts', True), 'rupture_force': ('kinetics', True), 'rupture_work': ('kinetics', True), 'sc_cells': ('topology', True), 'sc_charge': ('topology', True), 'sc_charge_prod': ('topology', True), 'sc_coverage': ('topology', True), 'sc_dcharge': ('topology', True), 'sc_dh': ('topology', True), 'sc_dphobic': ('topology', True), 'sc_gap_asym': ('topology', True), 'sc_gap_depth': ('topology', True), 'sc_gap_height': ('topology', True), 'sc_gap_index': ('topology', True), 'sc_gap_mean': ('topology', True), 'sc_gap_sd': ('topology', True), 'sc_gap_vol': ('topology', True), 'sc_interlock': ('topology', True), 'sc_interlock_frac': ('topology', True), 'sc_phobic': ('topology', True), 'sc_phobic_prod': ('topology', True), 'sc_shape': ('topology', True), 'shift_u': ('placement', True), 'shift_w': ('placement', True), 'varPhi_pep_soft': ('energetics', True), 'varPhi_tcr_soft': ('energetics', True)}#
Family of each descriptor, and whether the TCR enters its definition.
Five families, split by what each quantity is invariant under — which is also the axis along which they carry independent evidence:
placement— where the receptor sits, expressed in the pMHC groove frame: docking angles, the TCRdock rigid-body parameters, the ride height/shift/offset of the receptor body, and the per-loop CDR3 frame descriptors. Frame-dependent: these change if the groove frame does.interface— how much contact there is and of what chemical kind: buried area, contact counts and types, hydrogen bonds, clashes, chain and loop balance. SE(3)-invariant. This is the channel Eq. Q is built from.topology— the shape of the contact set, independent of both its size and its chemistry: coverage entropy and Hill numbers over the CDR-loop x target cells, the footprint’s Betti numbers and persistence entropy, the canonical germline/CDR3 preference. SE(3)-invariant, which is why these need no canonical orientation (tcren.footprint).energetics— statistical-potential interface energiesFand their poly-alanine referencesdF. Lower is more favourable. SE(3)-invariant.kinetics— the interface as a network of breakable springs: stiffness, anisotropy, strain, rupture, and the residues that couple the pre-formed scaffold to the interface.
placementandinterfacewere onegeometryfamily until 2026-08-24. Splitting them is what lets the three-channel claim be stated at all: the coverage entropy is coupled to the ride height (Spearman -0.559 / -0.525) and so is not independent ofplacement, while it is a different question whether it is independent ofinterface.descriptors()keeps"geometry"and"physics"working as aliases.involves_tcrisFalsefor a quantity computed from the peptide and the MHC alone. Such a column is a property of the cohort, not of the receptor: two structures of the same epitope on the same allele share its value whatever their TCR. A model handed one can reach a cohort-level label through epitope or allele identity instead of through interface physics, so any analysis whose question is about receptors must select withtcr_only=True(descriptors()).Fitted and cohort-relative composites (
p_real,p_real_bn,p_forced,p_bind,q_bind,s_strain) are listed underscore. They are outputs built from the descriptors above and must never be fed back in as inputs;descriptors()excludes them by default.
- tcren.descriptors.catalogue.INVARIANCE: dict[str, str] = {'D1_cell': 'compositional', 'D2_cell': 'compositional', 'D2_loop': 'compositional', 'D2_pep24': 'compositional', 'H_cell': 'compositional', 'H_loop': 'compositional', 'J_cell': 'compositional', 'K_shear': 'geometric', 'K_tens': 'geometric', 'L_canon': 'compositional', 'Phi_cdr12': 'energetic', 'Phi_cdr3a': 'energetic', 'Phi_cdr3b': 'energetic', 'Phi_pep_int': 'energetic', 'Phi_pep_mhc': 'energetic', 'Phi_tcr_mhc': 'energetic', 'Phi_tcr_pep': 'energetic', 'S_cell': 'compositional', 'S_tot': 'geometric', 'ab_imb': 'compositional', 'ab_imb_mhc': 'compositional', 'ab_imb_pep': 'compositional', 'aniso': 'geometric', 'burial': 'geometric', 'ca_cb_agreement_tm': 'geometric', 'ca_cb_agreement_tp': 'geometric', 'cdr3_ab_imbalance': 'compositional', 'cdr3_dominance': 'compositional', 'cdr3a_an': 'geometric', 'cdr3a_au': 'geometric', 'cdr3a_aw': 'geometric', 'cdr3a_ext': 'geometric', 'cdr3a_on': 'geometric', 'cdr3a_ou': 'geometric', 'cdr3a_ow': 'geometric', 'cdr3a_reach': 'geometric', 'cdr3a_topep': 'geometric', 'cdr3b_an': 'geometric', 'cdr3b_au': 'geometric', 'cdr3b_aw': 'geometric', 'cdr3b_ext': 'geometric', 'cdr3b_on': 'geometric', 'cdr3b_ou': 'geometric', 'cdr3b_ow': 'geometric', 'cdr3b_reach': 'geometric', 'cdr3b_topep': 'geometric', 'chain_balance': 'compositional', 'chain_cdr_imbalance': 'compositional', 'clash_score': 'geometric', 'co_mhc': 'compositional', 'co_pep': 'compositional', 'couple_mhc': 'compositional', 'couple_pep': 'compositional', 'couple_tcr': 'compositional', 'couple_total': 'compositional', 'crossing': 'geometric', 'crossing_signed': 'geometric', 'ct_tm_aromatic': 'compositional', 'ct_tm_hydrogen_bond': 'compositional', 'ct_tm_hydrophobic': 'compositional', 'ct_tm_other': 'compositional', 'ct_tm_salt_bridge': 'compositional', 'ct_tp_aromatic': 'compositional', 'ct_tp_hydrophobic': 'compositional', 'ct_tp_other': 'compositional', 'ct_tp_salt_bridge': 'compositional', 'dPhi_pep_mhc': 'energetic', 'dPhi_pep_soft': 'energetic', 'dPhi_tcr_pep': 'energetic', 'dPhi_tcr_soft': 'energetic', 'dPhi_tra_soft': 'energetic', 'dPhi_trb_soft': 'energetic', 'degree_evenness_tp': 'compositional', 'dock_d': 'geometric', 'dock_mhc_uy': 'geometric', 'dock_mhc_uz': 'geometric', 'dock_tcr_uy': 'geometric', 'dock_tcr_uz': 'geometric', 'dock_torsion': 'geometric', 'exp_lost': 'geometric', 'extent': 'compositional', 'fp_b0_frac_r7': 'topological', 'fp_b0_frac_r8': 'topological', 'fp_b0_r7': 'topological', 'fp_b0_r8': 'topological', 'fp_b1_r7': 'topological', 'fp_b1_r8': 'topological', 'fp_chi_r7': 'topological', 'fp_chi_r8': 'topological', 'frac_robust': 'compositional', 'frac_well_coordinated_tp': 'compositional', 'g_alg_conn': 'topological', 'g_assort': 'topological', 'g_comp_frac': 'topological', 'g_cyclo_frac': 'topological', 'g_even_pmhc': 'topological', 'g_even_tcr': 'topological', 'g_loop_even': 'compositional', 'g_loop_overlap': 'compositional', 'h0_pers_ent': 'geometric', 'height': 'geometric', 'lam_max': 'geometric', 'lam_min': 'geometric', 'log_lik': 'energetic', 'log_z': 'energetic', 'm_erank_tm': 'geometric', 'm_erank_tp': 'geometric', 'm_face_tm': 'geometric', 'm_face_tp': 'geometric', 'm_gap_tm': 'geometric', 'm_gap_tp': 'geometric', 'mean_margin': 'geometric', 'mhc_class_bin': 'categorical', 'n_clashes': 'compositional', 'n_contacts': 'energetic', 'n_contacts_tm': 'compositional', 'n_contacts_tp': 'compositional', 'n_hbond': 'compositional', 'n_interface': 'compositional', 'n_loop_contacts': 'compositional', 'n_mhc_contacts': 'compositional', 'n_pep_contacted': 'compositional', 'n_pep_contacts': 'compositional', 'n_pep_int': 'compositional', 'n_spring': 'compositional', 'neg_energy': 'energetic', 'offset': 'geometric', 'p_cdr3_pep': 'compositional', 'p_germ_mhc': 'compositional', 'partcoef_pmhc': 'compositional', 'partcoef_tcr': 'compositional', 'pep_cov_centre': 'compositional', 'pep_cov_d2n': 'compositional', 'pep_cov_even': 'compositional', 'pep_cov_frac': 'compositional', 'pep_cov_spread': 'compositional', 'pep_free_frac': 'compositional', 'pitch': 'geometric', 'psi': 'energetic', 'rupture_force': 'geometric', 'rupture_work': 'geometric', 'sc_cells': 'geometric', 'sc_charge': 'compositional', 'sc_charge_prod': 'compositional', 'sc_coverage': 'geometric', 'sc_dcharge': 'compositional', 'sc_dh': 'geometric', 'sc_dphobic': 'compositional', 'sc_gap_asym': 'geometric', 'sc_gap_depth': 'geometric', 'sc_gap_height': 'geometric', 'sc_gap_index': 'geometric', 'sc_gap_mean': 'geometric', 'sc_gap_sd': 'geometric', 'sc_gap_vol': 'geometric', 'sc_interlock': 'geometric', 'sc_interlock_frac': 'geometric', 'sc_phobic': 'compositional', 'sc_phobic_prod': 'compositional', 'sc_shape': 'geometric', 'shift_u': 'geometric', 'shift_w': 'geometric', 'varPhi_pep_soft': 'energetic', 'varPhi_tcr_soft': 'energetic'}#
What each descriptor is invariant under – the axis along which
geometryandtopologyare different questions rather than two names for the contact set.Geometry is the study of properties preserved by distance-preserving transformations; topology is the study of properties preserved by continuous deformation. Applied here:
"geometric"A continuous quantity in physical units – a length (A), an area (A^2), an angle, or a direction cosine. Preserved by isometry, destroyed by deformation. This is the docking: where the receptor sits on the groove and how it leans.
"topological"An invariant of the contact complex under continuous deformation – Betti numbers, the Euler characteristic, and their size-normalized forms. This is the interface surface: how many patches it falls into and how many holes it has, whatever its shape.
"compositional"A count over the labelled contact set, or a ratio, share, entropy or Hill number built from such counts. Preserved by both, because it reads the labelling rather than the shape.
"energetic"A statistical-potential or Potts energy.
"categorical"Which class of MHC presents the peptide – class I or class II.
Two consequences worth knowing before building a block from a family. The topology family is mostly compositional: 20 of its 29 columns are diversity or coverage measures over labelled cells and positions, and only 8 are topological invariants. And
h0_pers_entis filed"geometric", not"topological", because the H0 barcode’s bar lengths are the minimum spanning tree’s edge lengths in angstroms – persistent homology is a metric construction, and the entropy of a length distribution is not a homeomorphism invariant.
- tcren.descriptors.catalogue.DETAIL: dict[str, tuple[str, str]] = {'D1_cell': ('count', 'Hill number of order 1 over the same cells, exp(H); monotone in H_cell.'), 'D2_cell': ('count', 'Hill number of order 2, 1/sum(p^2); the effective number of engaged cells, discounting the weakly populated ones.'), 'D2_loop': ('count', 'Hill number of order 2 over the six loops.'), 'D2_pep24': ('count', 'Hill number of order 2 over the twenty-four-cell partition, the peptide split into N-terminal, central and C-terminal bands.'), 'H_cell': ('fraction', 'Normalized Shannon entropy of the contact composition over the twelve cells (six CDR loops x {peptide, MHC}).'), 'H_loop': ('fraction', 'Normalized entropy over the six CDR loops alone, ignoring which target each contact reaches.'), 'J_cell': ('fraction', 'Pielou evenness over the occupied cells. NaN when one cell is occupied.'), 'K_shear': ('N/m', 'In-plane stiffness, S_tot minus K_tens.'), 'K_tens': ('N/m', 'Tensile stiffness along the docking axis.'), 'L_canon': ('log-odds', 'Canonical-docking log odds-ratio of loop class (germline, CDR3) against target (MHC, peptide), Haldane-Anscombe corrected. High when CDR3 sits on the peptide and the germline loops on the helices.'), 'Phi_cdr12': ('log-odds', 'The CDR1 + CDR2 part of the TCR:peptide energy, both chains.'), 'Phi_cdr3a': ('log-odds', 'The CDR3alpha part of the TCR:peptide energy.'), 'Phi_cdr3b': ('log-odds', 'The CDR3beta part of the TCR:peptide energy.'), 'Phi_pep_int': ('log-odds', "The peptide's own intra-chain contact energy. Computed without the receptor."), 'Phi_pep_mhc': ('log-odds', 'Phi over peptide-MHC contacts under Miyazawa-Jernigan. Computed without the receptor.'), 'Phi_tcr_mhc': ('log-odds', 'Phi over TCR-MHC contacts under Miyazawa-Jernigan.'), 'Phi_tcr_pep': ('log-odds', 'Phi over TCR-peptide contacts under TCRen2, summed over all TCR regions. Lower is more favourable.'), 'S_cell': ('count', 'Richness: how many of the twelve cells are occupied.'), 'S_tot': ('N/m', 'Trace of the stiffness tensor; total interface stiffness.'), 'ab_imb': ('signed fraction', 'Signed (TRA - TRB)/(TRA + TRB) over CDR-loop contacts; positive is alpha-shifted.'), 'ab_imb_mhc': ('signed fraction', 'The same restricted to MHC-side contacts.'), 'ab_imb_pep': ('signed fraction', 'The same restricted to peptide-side contacts.'), 'aniso': ('ratio', 'K_shear / K_tens; how much stiffer the interface is along the pull than across it.'), 'burial': ('A^2', 'Interface buried surface, SASA(TCR) + SASA(pMHC) - SASA(complex), by Shrake-Rupley.'), 'ca_cb_agreement_tm': ('ratio', 'The same rank correlation across the TCR:MHC approach shell.'), 'ca_cb_agreement_tp': ('ratio', 'Spearman correlation between the Calpha and Cbeta distance maps over the TCR:peptide approach shell. High when the side chains track the backbone, as they do in a crystal.'), 'cdr3_ab_imbalance': ('fraction', 'abs(CDR3a - CDR3b) / (CDR3a + CDR3b); how one-sided the CDR3 engagement is.'), 'cdr3_dominance': ('fraction', 'CDR3(alpha+beta) share of all CDR TCR:peptide contacts.'), 'cdr3a_an': ('cosine', "Orientation of CDR3alpha's N->C axis against the groove normal n."), 'cdr3a_au': ('cosine', "Orientation of CDR3alpha's N->C axis against the groove long axis u."), 'cdr3a_aw': ('cosine', "Orientation of CDR3alpha's N->C axis against the groove short axis w."), 'cdr3a_ext': ('A', 'End-to-end extension of CDR3alpha, the Calpha_N to Calpha_C distance.'), 'cdr3a_on': ('cosine', 'Where CDR3alpha sits over the groove, along the groove normal n.'), 'cdr3a_ou': ('cosine', 'Where CDR3alpha sits over the groove, along the long axis u.'), 'cdr3a_ow': ('cosine', 'Where CDR3alpha sits over the groove, along the short axis w.'), 'cdr3a_reach': ('A', "Distance from the loop's Calpha centroid to the peptide Calpha centroid; how far CDR3alpha reaches."), 'cdr3a_topep': ('A', 'Minimum Calpha-Calpha distance from CDR3alpha to the peptide; its engagement depth.'), 'cdr3b_an': ('cosine', "Orientation of CDR3beta's N->C axis against the groove normal n."), 'cdr3b_au': ('cosine', "Orientation of CDR3beta's N->C axis against the groove long axis u."), 'cdr3b_aw': ('cosine', "Orientation of CDR3beta's N->C axis against the groove short axis w."), 'cdr3b_ext': ('A', 'End-to-end extension of CDR3beta, the Calpha_N to Calpha_C distance.'), 'cdr3b_on': ('cosine', 'Where CDR3beta sits over the groove, along the groove normal n.'), 'cdr3b_ou': ('cosine', 'Where CDR3beta sits over the groove, along the long axis u.'), 'cdr3b_ow': ('cosine', 'Where CDR3beta sits over the groove, along the short axis w.'), 'cdr3b_reach': ('A', "Distance from the loop's Calpha centroid to the peptide Calpha centroid; how far CDR3beta reaches."), 'cdr3b_topep': ('A', 'Minimum Calpha-Calpha distance from CDR3beta to the peptide; its engagement depth.'), 'chain_balance': ('fraction', 'min(a,b)/(a+b) over TCR:peptide contacts by chain; 0.5 when both chains engage equally, 0 when only one does.'), 'chain_cdr_imbalance': ('fraction', 'abs(a - b) / (a + b) over all CDR contacts; the chain-level mirror of chain_balance.'), 'clash_score': ('A', 'Summed overlap depth of those clashing pairs; the steric burden of a forced pose.'), 'co_mhc': ('ratio', 'Contact order on the MHC helices, by the same construction.'), 'co_pep': ('ratio', "Contact order on the peptide: mean sequence separation of the peptide residues one CDR loop reaches, averaged over loops and divided by the peptide's span."), 'couple_mhc': ('count', 'MHC residues contacting both the peptide and the TCR.'), 'couple_pep': ('count', 'Peptide residues contacting both the MHC and the TCR.'), 'couple_tcr': ('count', 'TCR residues in the Valpha-Vbeta interface that also contact the pMHC.'), 'couple_total': ('count', 'Sum of the three coupling counts.'), 'crossing': ('deg', 'Crossing (scanning) angle between the Valpha->Vbeta axis projected into the groove plane and the groove long axis.'), 'crossing_signed': ('deg', 'The same angle on [-180, 180); its sign is the docking polarity, canonical or reversed.'), 'ct_tm_aromatic': ('count', 'Aromatic contacts across TCR:MHC.'), 'ct_tm_hydrogen_bond': ('count', 'Hydrogen bonds across TCR:MHC.'), 'ct_tm_hydrophobic': ('count', 'Hydrophobic contacts across TCR:MHC.'), 'ct_tm_other': ('count', 'Remaining classified TCR:MHC contacts.'), 'ct_tm_salt_bridge': ('count', 'Salt bridges across TCR:MHC.'), 'ct_tp_aromatic': ('count', 'Ring-atom pairs between aromatic residues across TCR:peptide.'), 'ct_tp_hydrophobic': ('count', 'Apolar C-C pairs between apolar residues across TCR:peptide.'), 'ct_tp_other': ('count', 'Remaining classified TCR:peptide contacts.'), 'ct_tp_salt_bridge': ('count', 'Cationic-N / anionic-O pairs within 4 A across TCR:peptide.'), 'dPhi_pep_mhc': ('log-odds', 'The same reference across peptide:MHC. Computed without the receptor.'), 'dPhi_pep_soft': ('log-odds', "Smoothed reference delta, peptide direction: the peptide's energy minus the free energy of the residue background at each peptide position, receptor frozen."), 'dPhi_tcr_pep': ('log-odds', 'Poly-alanine reference delta of the TCR:peptide energy; the pose-geometry baseline removed.'), 'dPhi_tcr_soft': ('log-odds', "Smoothed reference delta, receptor direction: the receptor's energy minus the free energy of the residue background at each contacted TCR position, peptide frozen."), 'dPhi_tra_soft': ('log-odds', 'The alpha-chain part of the receptor-direction smoothed reference delta.'), 'dPhi_trb_soft': ('log-odds', 'The beta-chain part of the receptor-direction smoothed reference delta.'), 'degree_evenness_tp': ('fraction', 'Participation ratio of the receptor-side contact degrees across TCR:peptide, in [0, 1]. Low when a few over-reaching side chains hoard the contact budget.'), 'dock_d': ('A', 'MHC-stub to TCR-stub rigid-body separation.'), 'dock_mhc_uy': ('cosine', 'y component of the MHC stub unit vector.'), 'dock_mhc_uz': ('cosine', 'z component of the MHC stub unit vector.'), 'dock_tcr_uy': ('cosine', 'y component of the TCR stub unit vector in the MHC frame.'), 'dock_tcr_uz': ('cosine', 'z component of the TCR stub unit vector; how high the receptor body rides over the groove.'), 'dock_torsion': ('rad', 'Rigid-body dihedral of the TCR about the MHC stub; the docking twist. Circular, wraps at +-pi.'), 'exp_lost': ('count', 'Expected TCR:peptide contacts lost under a 1 A isotropic shift.'), 'extent': ('count', 'Distinct TCR residues contacting the pMHC over both receptor interfaces.'), 'fp_b0_frac_r7': ('fraction', 'Patches per contacted residue at 7 A; the size-free form of Betti-0.'), 'fp_b0_frac_r8': ('fraction', 'Patches per contacted residue at 8 A; the size-free form of Betti-0.'), 'fp_b0_r7': ('count', 'Betti-0 of the flag complex on the contacted pMHC Calpha atoms at 7 A: how many disconnected patches the footprint falls into.'), 'fp_b0_r8': ('count', 'Betti-0 of the flag complex on the contacted pMHC Calpha atoms at 8 A: how many disconnected patches the footprint falls into.'), 'fp_b1_r7': ('count', 'Betti-1 at 7 A: how many holes the footprint encloses.'), 'fp_b1_r8': ('count', 'Betti-1 at 8 A: how many holes the footprint encloses.'), 'fp_chi_r7': ('count', 'Euler characteristic b0 - b1 at 7 A.'), 'fp_chi_r8': ('count', 'Euler characteristic b0 - b1 at 8 A.'), 'frac_robust': ('fraction', 'Share of TCR:peptide contacts with at least 1 A of margin.'), 'frac_well_coordinated_tp': ('fraction', 'Share of contacting receptor residues reaching no more than three peptide residues, the count a crystal side chain typically makes.'), 'g_alg_conn': ('ratio', 'Second-smallest eigenvalue of the normalised Laplacian on the largest contact-graph component, in [0, 2]. Near 0 when the footprint is about to fall into two patches.'), 'g_assort': ('ratio', 'Degree assortativity of the contact graph: the correlation, over contacts, between the degrees of the two residues involved.'), 'g_comp_frac': ('fraction', 'Connected components of the bipartite contact graph per node; the parameter-free form of the footprint patch count, needing no Calpha radius.'), 'g_cyclo_frac': ('fraction', "Contacts beyond a spanning forest over all contacts, (E - V + C) / E, which is the contact graph's first Betti number made size-free. High when the footprint is interlocked."), 'g_even_pmhc': ('fraction', 'Pielou evenness of the contact degrees of the engaged pMHC residues, base the engaged count.'), 'g_even_tcr': ('fraction', 'Pielou evenness of the contact degrees of the engaged CDR-loop residues, base the engaged count. 1 when every engaged residue carries the same number of partners.'), 'g_loop_even': ('fraction', 'Pielou evenness over the six CDR loops of the number of distinct pMHC residues each reaches, base 6. Counts partners rather than contacts, so it does not track residue size.'), 'g_loop_overlap': ('fraction', "Mean pairwise Jaccard overlap of the engaged CDR loops' pMHC partner sets. High when the loops crowd onto the same residues instead of partitioning the surface."), 'h0_pers_ent': ('fraction', "Normalized entropy of the H0 barcode of the contacted pMHC Calpha cloud. The bar lengths are the minimum spanning tree's edges, so no filtration is chosen."), 'height': ('A', 'Elevation of the CDR Calpha centroid above the groove plane.'), 'lam_max': ('N/m', 'Largest eigenvalue of the stiffness tensor.'), 'lam_min': ('N/m', 'Smallest eigenvalue of the stiffness tensor.'), 'log_lik': ('kT', 'Log probability of the observed contact map; its typicality.'), 'log_z': ('kT', "Log partition function over every contact map the geometry admits; the interface's capacity."), 'm_erank_tm': ('fraction', 'Effective rank fraction of the CDR-loop x MHC-helix kernel. CDR3-length coupled (Spearman -0.437), so it reads how much loop there is to spread over the helices.'), 'm_erank_tp': ('fraction', 'Effective rank of the CDR-loop x peptide Calpha proximity kernel over its maximum: how many independent approach modes the interface has. Peptide-length coupled (Spearman -0.547 on 148 class I crystals) because a longer class I peptide bulges.'), 'm_face_tm': ('A', 'Mean Calpha-Calpha minus Cbeta-Cbeta distance over the contacting TCR:MHC residue pairs.'), 'm_face_tp': ('A', 'Mean Calpha-Calpha minus Cbeta-Cbeta distance over the contacting TCR:peptide residue pairs. Positive when side chains lean towards each other, negative when the backbones are the close part and the side chains point away.'), 'm_gap_tm': ('ratio', 'Second over first singular value of the CDR-loop x MHC-helix kernel. The least length-coupled column in the catalogue: -0.012 against CDR3 length, +0.027 against peptide length, 99.9 per cent of its variance surviving both.'), 'm_gap_tp': ('ratio', 'Second over first singular value of that kernel. Near 0 when the approach is separable into a loop profile times a peptide profile rather than pairing specific residues.'), 'mean_margin': ('A', 'Mean contact margin, cutoff minus minimum heavy-atom distance.'), 'mhc_class_bin': ('class I / II', 'Which class of MHC presents the peptide: 0 for class I, 1 for class II. Class I and class II grooves differ in shape and in how they hold a peptide, so this conditions the other descriptors rather than being scored beside them; a coefficient fitted across both classes without it is fitted to a mixture.'), 'n_clashes': ('count', 'Peptide-partner heavy-atom pairs overlapping by more than 0.4 A on Bondi radii.'), 'n_contacts': ('count', "Available residue pairs that engaged. Distinct from the footprint's loop tally."), 'n_contacts_tm': ('count', 'TCR-MHC residue-residue contacts.'), 'n_contacts_tp': ('count', 'TCR-peptide residue-residue contacts.'), 'n_hbond': ('count', 'Polar N/O atom pairs within 3.5 A across TCR:peptide.'), 'n_interface': ('count', 'Interface residue count; the size denominator for the coupling counts.'), 'n_loop_contacts': ('count', 'Contacts the six-CDR-loop partition sees; framework contacts are outside it by construction.'), 'n_mhc_contacts': ('count', 'Of those loop contacts, the ones reaching the MHC.'), 'n_pep_contacted': ('count', 'Distinct peptide residues the TCR contacts.'), 'n_pep_contacts': ('count', 'Of those loop contacts, the ones reaching the peptide.'), 'n_pep_int': ('count', 'Intra-peptide residue contacts, at 5 A with a sequence separation of at least three.'), 'n_spring': ('count', 'Springs in the interface network; every other kinetics column is NaN below three.'), 'neg_energy': ('kT', '-E of the observed contact map under the coupled Potts model; higher is more native-like. Exactly log_z + log_lik.'), 'offset': ('A', 'Length of the in-plane displacement; lateral shift whatever its direction.'), 'p_cdr3_pep': ('fraction', 'Share of CDR3 contacts that reach the peptide.'), 'p_germ_mhc': ('fraction', 'Share of germline (CDR1/CDR2) contacts that reach the MHC.'), 'partcoef_pmhc': ('fraction', 'The same over engaged pMHC residues with the six CDR loops as modules.'), 'partcoef_tcr': ('fraction', 'Mean over engaged TCR residues of 1 - sum_s (k_s/k)^2 with the modules peptide and MHC; 0 when every residue reads one target only.'), 'pep_cov_centre': ('fraction', 'Contact-weighted mean position on [0, 1] from N- to C-terminus; 0.5 is centred.'), 'pep_cov_d2n': ('fraction', 'Hill number of order 2 of that distribution over peptide length; the effective share of the peptide engaged.'), 'pep_cov_even': ('fraction', 'Pielou evenness of the accessibility-discounted contact distribution, base ln(peptide length); how evenly the receptor uses the peptide it can reach.'), 'pep_cov_frac': ('fraction', 'Peptide positions the TCR contacts, over peptide length.'), 'pep_cov_spread': ('fraction', 'Contact-weighted standard deviation of that position, doubled; approaches 1 when the receptor reaches both termini.'), 'pep_free_frac': ('fraction', "Share of the peptide the groove leaves for the receptor: mean over positions of n_TCR/(n_TCR + n_MHC). The threshold-free reading of 'peptide without its MHC anchors'."), 'pitch': ('deg', 'Incident angle of the TCR out of the groove plane. **Banned as a feature**: it reproduces no clean geometric angle yet out-discriminates every one of them, which is AlphaFold-confidence contamination rather than geometry.'), 'psi': ('kT/site', 'log_lik per available site, so interfaces of different size compare.'), 'rupture_force': ('N', 'Peak resisting force under steered separation along the weaker axis.'), 'rupture_work': ('J', 'Force integrated to full separation; the off-rate proxy, and a geometry-only quantity no potential enters.'), 'sc_cells': ('count', 'Grid cells entering the comparison; bookkeeping, so a low complementarity can be told from a thin one.'), 'sc_charge': ('ratio', 'Pearson r between the two charge fields; NEGATIVE is complementary, plus meeting minus.'), 'sc_charge_prod': ('ratio', 'Mean per-cell product of the two charge fields.'), 'sc_coverage': ('fraction', 'Retained cells as a share of the occupied pMHC cells in the window; bookkeeping.'), 'sc_dcharge': ('ratio', 'Mean absolute per-cell charge difference between the two faces.'), 'sc_dh': ('A', 'Mean absolute per-cell height difference between the two faces.'), 'sc_dphobic': ('ratio', 'Mean absolute per-cell hydropathy difference between the two faces.'), 'sc_gap_asym': ('signed fraction', '(void - interlock) / (void + interlock); -1 for a face that only interlocks, +1 for one that only stands off.'), 'sc_gap_depth': ('A', 'Mean depth over the interlocked cells alone: how far the receptor reaches in where it does.'), 'sc_gap_height': ('A', 'Mean standoff over the void cells alone: how high it stands where it does not mesh.'), 'sc_gap_index': ('A', 'Void volume over retained contact area; the intensive form of the gap-volume channel.'), 'sc_gap_mean': ('A', 'Mean of h(TCR) - h(pMHC) over retained cells. Negative on a real interface: the median cell interdigitates.'), 'sc_gap_sd': ('A', 'Spread of the same gap. High when the receptor rests on a few high points rather than meshing.'), 'sc_gap_vol': ('A^3', 'Void volume, the gap integrated over the contact plane where it is positive.'), 'sc_interlock': ('A^3', 'Interdigitated volume, the gap integrated where it is negative. The larger of the two on a real interface.'), 'sc_interlock_frac': ('fraction', 'Share of retained cells whose gap is negative; the per-structure form of the corpus 71% interdigitation.'), 'sc_phobic': ('ratio', 'Pearson r between the two Kyte-Doolittle fields; positive is complementary, apolar meeting apolar.'), 'sc_phobic_prod': ('ratio', 'Mean per-cell product of the two hydropathy fields.'), 'sc_shape': ('ratio', "Pearson r between the pMHC and TCR height fields over the shared grid; positive is complementary, the receptor riding up where the groove rises. Lawrence & Colman's Sc is the same idea on a dot surface."), 'shift_u': ('A', 'In-plane displacement of that centroid from the peptide centroid along the groove long axis.'), 'shift_w': ('A', 'The same along the groove short axis.'), 'varPhi_pep_soft': ('log-odds^2', "Variance of the local field under the background, peptide direction: how sharply each peptide position's energy responds to residue identity, summed over positions. NOT a ddG -- it is a second cumulant, not a difference of differences."), 'varPhi_tcr_soft': ('log-odds^2', 'Variance of the local field under the background, receptor direction, summed over contacted TCR positions.')}#
Units and a one-line definition for every descriptor. The single source the docs table is generated from, so a new descriptor cannot reach a feature table undocumented.
unitsis what the number is measured in –A,A^2,deg,rad,kT,N/m, or one of the dimensionless kindscount,fraction,signed fraction,ratio,cosine,log-odds,indicator. It is what a transform has to respect: a count is variance-stabilized by a square root, a fraction by the arcsine (the classical angular transformation), and an unbounded continuous quantity by neither.
- tcren.descriptors.catalogue.INVARIANCE_CLASSES: tuple[str, ...] = ('geometric', 'topological', 'compositional', 'energetic', 'categorical')#
The invariance classes, in the order the catalogue reports them.
- tcren.descriptors.catalogue.STATUS: dict[str, tuple[str, str]] = {'D1_cell': ('suspicious', 'determined: D1 = 12 ** H_cell.'), 'J_cell': ('suspicious', 'determined: J = H_cell * ln 12 / ln S_cell.'), 'Phi_pep_int': ('suspicious', 'no receptor; see Phi_pep_mhc.'), 'Phi_pep_mhc': ('suspicious', 'no receptor: constant across every structure of one epitope on one allele, so a receptor-ranking model reading it reaches the cohort label without reading an interface.'), 'S_tot': ('suspicious', 'determined by K_tens and K_shear.'), 'aniso': ('suspicious', 'determined by K_tens and K_shear.'), 'ca_cb_agreement_tm': ('suspicious', 'coupled to both lengths: -0.349 / -0.334, 70.5 per cent beyond them -- the most length-loaded column here.'), 'co_mhc': ('suspicious', "CDR3-length coupled: +0.011 / +0.345, 88.0 per cent beyond both. Contact order divides by the target's span, not the loop's, so a longer loop spreads over more helix."), 'couple_total': ('suspicious', 'determined: couple_pep + couple_mhc + couple_tcr.'), 'crossing': ('suspicious', 'determined: abs(crossing_signed).'), 'ct_tp_salt_bridge': ('stalled', 'only 3 distinct values over 1,707 modelled complexes: a salt bridge across the TCR:peptide interface is rare enough that the count is almost always 0. The TCR:MHC counterpart ct_tm_salt_bridge does move.'), 'dPhi_pep_mhc': ('suspicious', 'no receptor; see Phi_pep_mhc.'), 'dPhi_tcr_soft': ('suspicious', 'determined: dPhi_tra_soft + dPhi_trb_soft.'), 'degree_evenness_tp': ('suspicious', 'peptide-length coupled: -0.450 / -0.006, 88.7 per cent beyond both. It reads the class I bulge.'), 'fp_chi_r7': ('suspicious', 'determined: chi = b0 - b1 at the same radius.'), 'fp_chi_r8': ('suspicious', 'determined: chi = b0 - b1 at the same radius.'), 'frac_well_coordinated_tp': ('suspicious', 'peptide-length coupled: -0.440 / -0.064, 83.4 per cent beyond both; see degree_evenness_tp.'), 'g_comp_frac': ('suspicious', 'CDR3-length coupled: -0.032 / +0.280, 92.7 per cent beyond both.'), 'g_even_tcr': ('suspicious', 'peptide-length coupled: -0.368 / -0.061, 91.0 per cent beyond both.'), 'g_loop_even': ('suspicious', 'CDR3-length coupled: -0.186 / -0.350, 87.7 per cent beyond both.'), 'm_erank_tm': ('suspicious', 'CDR3-length coupled: -0.186 / -0.437, 69.4 per cent beyond both. It reads how much loop there is to spread over the helices.'), 'm_erank_tp': ('suspicious', 'peptide-length coupled: -0.547 / -0.105, 58.3 per cent of variance beyond both lengths. It reads the class I bulge.'), 'm_gap_tp': ('suspicious', 'peptide-length coupled: -0.322 / +0.041, 90.8 per cent beyond both.'), 'mhc_class_bin': ('suspicious', 'no receptor: it is the MHC class, I or II. It is also constant on any single-class cohort -- both receptor benchmarks are class I -- so it contributes nothing there and separates the classes everywhere else.'), 'n_contacts_tm': ('suspicious', 'determined: the sum of the five ct_tm_* contact-type tallies.'), 'n_contacts_tp': ('suspicious', 'determined: ct_tp_salt_bridge + ct_tp_aromatic + ct_tp_hydrophobic + ct_tp_other + n_hbond.'), 'n_loop_contacts': ('suspicious', 'determined: n_pep_contacts + n_mhc_contacts.'), 'n_pep_int': ('suspicious', 'no receptor; see Phi_pep_mhc.'), 'neg_energy': ('suspicious', 'determined: log_z + log_lik.'), 'offset': ('suspicious', 'determined: offset = hypot(shift_u, shift_w).'), 'pitch': ('suspicious', "reads the generator's confidence rather than the interface: it is docking_angles' incident_angle, and it out-discriminates every clean docking angle for that reason. Never use it as a feature."), 'sc_gap_depth': ('suspicious', 'peptide-length coupled: +0.366 / +0.124, 71.8 per cent of variance beyond both lengths. A longer class I peptide bulges, and the receptor reaches further in where it does.'), 'sc_gap_index': ('suspicious', 'determined: (sc_dh + sc_gap_mean) / 2.')}#
Descriptors that need a second look before they are used, and why. A name absent from here has no known defect; presence is not a reason to drop the column, only to know what it is.
Two flags:
"suspicious"– the quantity is not measuring what its family name suggests. Either it reads the generator rather than the interface, or it is fixed by an exact identity over other columns, or it identifies the cohort rather than the complex."stalled"– the quantity is defined but does not move: near-zero spread, or undefined on most of the corpus, so nothing downstream can use it.
The identities were each verified to float tolerance on both receptor benchmarks (max relative difference 3.6e-15), so they are algebra rather than correlation. A determined column is exact information the model already has – harmless in a report, and a rank deficiency in a fit.
- tcren.descriptors.catalogue.descriptors(family=None, *, tcr_only=False, invariance=None)[source]#
Descriptor names from
DESCRIPTORS, filtered by family and receptor involvement.- Parameters:
family (str | None) – keep one of
FAMILIES("placement","interface","topology","energetics","kinetics"), or all of them ifNone. The retired names"geometry"(=placement+interface) and"physics"(=energetics) still work.tcr_only (bool) – keep only descriptors the receptor enters. Set this whenever the question being asked is about receptors — a peptide- or MHC-only column carries cohort identity.
invariance (str | None) – keep one class of
INVARIANCE—"geometric"for the docking’s isometry invariants,"topological"for the interface surface’s homeomorphism invariants,"compositional"for counts over the labelled contact set,"energetic"or"categorical". Combines withfamily.
- Returns:
The matching names, in catalogue order.
- Return type:
tuple[str, …]
Example
>>> descriptors("energetics", tcr_only=True) ('Phi_tcr_pep', 'Phi_tcr_mhc', 'Phi_cdr12', 'Phi_cdr3a', 'Phi_cdr3b', 'dPhi_tcr_pep') >>> descriptors("physics") == descriptors("energetics") # retired alias True >>> descriptors("topology", invariance="topological") ('fp_b0_r7', 'fp_b1_r7', 'fp_chi_r7', 'fp_b0_frac_r7', 'fp_b0_r8', 'fp_b1_r8', 'fp_chi_r8', 'fp_b0_frac_r8')
- tcren.descriptors.catalogue.OPERATOR = {'correlation': ('sc_shape', 'sc_charge', 'sc_phobic', 'ca_cb_agreement_tp', 'ca_cb_agreement_tm', 'g_assort'), 'frame': ('pitch', 'crossing', 'crossing_signed', 'dock_d', 'dock_torsion', 'dock_tcr_uy', 'dock_tcr_uz', 'dock_mhc_uy', 'dock_mhc_uz', 'height', 'shift_u', 'shift_w', 'offset', 'cdr3a_reach', 'cdr3a_ou', 'cdr3a_ow', 'cdr3a_on', 'cdr3a_au', 'cdr3a_aw', 'cdr3a_an', 'cdr3a_topep', 'cdr3a_ext', 'cdr3b_reach', 'cdr3b_ou', 'cdr3b_ow', 'cdr3b_on', 'cdr3b_au', 'cdr3b_aw', 'cdr3b_an', 'cdr3b_topep', 'cdr3b_ext'), 'hill': ('H_cell', 'D1_cell', 'D2_cell', 'S_cell', 'J_cell', 'H_loop', 'D2_loop', 'D2_pep24', 'pep_cov_even', 'pep_cov_d2n', 'h0_pers_ent', 'g_even_tcr', 'g_even_pmhc', 'g_loop_even', 'degree_evenness_tp', 'm_erank_tp', 'm_erank_tm', 'partcoef_tcr', 'partcoef_pmhc'), 'homology': ('fp_b0_r7', 'fp_b1_r7', 'fp_chi_r7', 'fp_b0_frac_r7', 'fp_b0_r8', 'fp_b1_r8', 'fp_chi_r8', 'fp_b0_frac_r8', 'g_comp_frac', 'g_cyclo_frac'), 'moment': ('sc_gap_mean', 'sc_gap_sd', 'sc_gap_vol', 'sc_interlock', 'sc_gap_index', 'sc_interlock_frac', 'sc_gap_depth', 'sc_gap_height', 'sc_gap_asym', 'sc_dh', 'sc_dcharge', 'sc_dphobic', 'sc_charge_prod', 'sc_phobic_prod', 'm_face_tp', 'm_face_tm', 'mean_margin', 'clash_score', 'exp_lost'), 'potential': ('Phi_tcr_pep', 'Phi_tcr_mhc', 'Phi_cdr12', 'Phi_cdr3a', 'Phi_cdr3b', 'dPhi_tcr_pep', 'dPhi_pep_soft', 'varPhi_pep_soft', 'dPhi_tcr_soft', 'varPhi_tcr_soft', 'dPhi_tra_soft', 'dPhi_trb_soft', 'Phi_pep_mhc', 'dPhi_pep_mhc', 'Phi_pep_int', 'neg_energy', 'log_z', 'log_lik', 'psi'), 'spectral': ('g_alg_conn', 'm_gap_tp', 'm_gap_tm', 'K_tens', 'K_shear', 'S_tot', 'aniso', 'lam_max', 'lam_min'), 'work': ('rupture_force', 'rupture_work')}#
Which operator of section 3 produces the descriptor. A descriptor absent from here is a count, a share or a raw geometric coordinate – the two operators that need no derivation.
- tcren.descriptors.catalogue.OPERATOR_OBJECT = {'correlation': '$\\mathcal{H}$, $\\mathcal{D}$, $\\mathcal{B}$', 'count': '$\\mathcal{C}$, $\\mathcal{L}$', 'frame': '$\\mathcal{F}$', 'hill': '$\\mathcal{T}$, $\\mathcal{B}$, spectra', 'homology': '$\\mathcal{B}$, $\\mathcal{C}$', 'moment': '$\\mathcal{H}$, $\\mathcal{D}$', 'potential': '$\\mathcal{L}$', 'spectral': '$\\mathcal{B}$, $\\mathcal{D}$, $\\Sigma$', 'work': '$\\Sigma$'}#
The object each operator reads, keyed to the reduction chain of section 2.
tcren.descriptors.compute module#
Structure -> descriptor values: the interface terms this package computes itself.
Everything here reads a ContactMap or a
Structure and returns numbers. The catalogue – what the columns
mean and which family each belongs to – is tcren.descriptors.catalogue, and the batch
dispatch is tcren.descriptors.table.
The energetics, topology, potts and kinetics families are not computed here: they belong to
tcren.pipeline / tcren.ddg, tcren.footprint / tcren.interface_graph,
tcren.potts and tcren.mechanics respectively, and this module calls them. What is left
here is the interface block – burial, extent, chain balance, the contact-type tallies and the
CDR3-frame placement terms – which has no other home.
Heavy imports stay function-local so a bare import tcren remains dependency-light.
- tcren.descriptors.compute.recognition_features(source, *, organism='human', potential=None, full=False, annotate=True)[source]#
Extract the core recognition vector from a TCR–pMHC structure (path or parsed).
Returns a dict keyed by
RECOGNITION_FEATURES(degenerate/undefined terms areNaN): docking geometry, per-interface energies (rawFand poly-alanineΔF), contact-type tallies, interface ΔSASAburial, and themhc_class_binindicator. The structure is chain-typed and MHC-annotated in place.DESCRIPTORSgives each column’s family and whether the receptor enters its definition.With
full=Truethe row is extended with the 18 CDR3-frame descriptors (CDR3_FRAME_FEATURES) — the completeFULL_FEATURESvector.- Parameters:
organism (str)
full (bool)
annotate (bool)
- Return type:
dict[str, float]
tcren.descriptors.table module#
Batched featurisation: one structure set -> one row per structure.
The dispatch layer. It owns the two things a whole-set run needs and a single-structure call does
not: the single arda call per organism plus the single mmseqs MHC search that annotate the set,
and the process pool that featurises it. Which columns each family contributes is
tcren.descriptors.catalogue; how the interface block is computed is
tcren.descriptors.compute.
- tcren.descriptors.table.recognition_table(items, *, organism='human', full=False, threads=1, chunk=64, autodetect_species=True, mechanics=False, include=None, radii=(7.0, 8.0), _mmseqs_threads=0)[source]#
Batched feature (+score) extraction for a whole set of TCR–pMHC structures.
itemsis an iterable of(id, structure-or-path). The set is annotated with a single arda call per organism (tcren.paper.helpers._batch_annotate()) and a single mmseqs MHC search (tcren.mhc.annotate_mhc_batch()) — the dataset-scale path that avoids the per-structure annotation cost — thenrecognition_features()(full=) is extracted for each. This emits descriptors only: the fitted composites and cohort-relative scores that used to ride along here were removed in 2.26.0, and scoring istcren.reliability.s_score()on the table.fullalso appends the intra-peptide columnsPEPTIDE_INTERNAL_FEATURES(Phi_pep_int,n_pep_int) — the peptide’s contact energy with itself, which the interface energies omit. Returns one row dict per structure (complex.id+ features); a structure that fails yields{"complex.id": id, "error": ...}so the batch stays resilient.The two stages run in sequence and never compete for the machine.
Search is one arda call per organism plus one mmseqs MHC search, each given every core, over the whole set. Featurisation is where the time actually goes — a 100-pose probe spends 96 s there against 2.4 s of arda and 0.9 s of MHC search — and it is pure Python/numpy, so
threads> 1 runs it in that many worker processes. The flag keeps its name for compatibility; it has always meant “how much of this machine may I use”.It used to mean concurrent threads over
chunk-sized batches, which was the wrong shape twice over: the GIL serialised the 94 % of the work that dominates, and each batch spawned its own mmseqs, so N batches asked for N x cores. Sharding the same work across independent subprocesses was measured 8x faster, which is what this now does directly.chunkis retained for signature compatibility and is no longer used.autodetect_speciessearchesorganismand mouse so a mis-declared cohort is still typed correctly. That doubles the annotation cost, so passFalsewhen the organism is known — it halves the mmseqs work and changes nothing else.mechanicsappends thetcren.mechanicskoff proxies (n_spring,S_tot,K_tens,K_shear,aniso,rupture_force,rupture_work,couple_*) to the same rows. They need the same annotated structure the descriptors do, so computing them here costs only their own arithmetic — runningtcren mechanicsseparately repeats the whole parse and both mmseqs searches, and returns a second table keyed differently.- Parameters:
organism (str)
full (bool)
threads (int)
chunk (int)
autodetect_species (bool)
mechanics (bool)
include (Sequence[str] | None)
radii (Sequence[float])
_mmseqs_threads (int)
- Return type:
list[dict]
Topology: the shape of the contact set#
tcren.topology package#
The shape of the contact set, and of the surface it sits on. No energy anywhere.
Every quantity here is a count, a share, an entropy, a Betti number or an Angstrom – nothing in this package loads a potential, which is what makes these descriptors independent of the energy channel they are reported beside. footprint partitions the contacts and measures how evenly they spread; graph reads the same contact map as a bipartite graph and the Calpha/Cbeta maps as matrices; surface is the pMHC face the receptor actually meets; pose asks whether the tight contacts and the favourable chemistry are the same contacts.
tcren.topology.footprint module#
Footprint shape: how a receptor’s contacts are distributed, not what they score.
Every other scorer in tcren reads the interface as a sum over contacts. The same contact map
also has a shape — which of the six CDR loops touched what, and whether the resulting footprint
is one connected patch — and that shape is a different observable. It carries no potential, no
fitted parameter and no reference structure.
Two families, both computed from one contact map:
Coverage. Partition the TCR:pMHC residue contacts into cells and measure how evenly they are
spread. With p_i the fraction of contacts in cell i over k cells,
H is the normalised Shannon entropy (1.0 = perfectly even) and D_q the Hill number of order
q — the effective number of engaged cells (Hill 1973, doi:10.2307/1934352; Jost 2006, doi:10.1111/j.2006.0030-1299.14714.x). D_1 = exp H_raw is a monotone transform of H and
ranks identically; D_2 = 1/\sum_i p_i^2 discounts weakly populated cells and separates better.
Two partitions ship: the 12 cells of the 6 CDR loops × {peptide, MHC}, and the 24 cells that
additionally split the peptide into N-terminal, central and C-terminal bands. Refining the peptide
side helps; refining the MHC side into its helices does not, which is why it is not offered.
Topology. Join the contacted pMHC residues at a Cα threshold and build the flag (clique)
complex on them. b0 counts disconnected footprint patches and b1 its holes. The cyclomatic
number of the bipartite contact graph (E - V + C) is deliberately not the headline: with
of order thirty contacts among of order thirty residues it is dominated by E and simply tracks
interface size. The patch count is scale-free and is not redundant with the coverage entropy.
Everything here is invariant under rigid motion, so no canonical orientation is required — only
chain typing and CDR region markup (tcren.annotation.classify_chains()). MHC region markup
is not needed either, so the two-pass MHC annotation trap does not apply.
>>> from tcren.footprint import footprint_features
>>> row = footprint_features(structure)
>>> row["D2_pep24"], row["fp_b0_r7"]
- tcren.topology.footprint.CELL_LOOPS: tuple[str, ...] = ('TRA:CDR1', 'TRA:CDR2', 'TRA:CDR3', 'TRB:CDR1', 'TRB:CDR2', 'TRB:CDR3')#
The six CDR loops, in the order the cell partition indexes them.
- tcren.topology.footprint.FOOTPRINT_FEATURES: tuple[str, ...] = ('n_loop_contacts', 'n_pep_contacts', 'n_mhc_contacts', 'H_cell', 'D1_cell', 'D2_cell', 'S_cell', 'J_cell', 'H_loop', 'D2_loop', 'D2_pep24', 'ab_imb', 'ab_imb_pep', 'ab_imb_mhc', 'L_canon', 'p_germ_mhc', 'p_cdr3_pep', 'pep_free_frac', 'pep_cov_frac', 'pep_cov_even', 'pep_cov_d2n', 'pep_cov_centre', 'pep_cov_spread', 'h0_pers_ent', 'g_even_tcr', 'g_even_pmhc', 'g_comp_frac', 'g_alg_conn', 'g_cyclo_frac', 'g_loop_even', 'g_loop_overlap', 'g_assort', 'degree_evenness_tp', 'frac_well_coordinated_tp', 'm_erank_tp', 'm_gap_tp', 'm_erank_tm', 'm_gap_tm', 'm_face_tp', 'm_face_tm', 'ca_cb_agreement_tp', 'ca_cb_agreement_tm')#
Every column
footprint_features()guarantees, size columns included. The radius-tagged Betti columns (fp_b0_r7and friends) are named from theradiiargument and so are not listed here;footprint_topology_features()gives the shape-only subset.
- tcren.topology.footprint.FOOTPRINT_SIZE_FEATURES: tuple[str, ...] = ('n_loop_contacts', 'n_pep_contacts', 'n_mhc_contacts')#
The three raw contact counts this module emits alongside the shape measures. They are interface size, not shape, and are catalogued under
interfaceintcren.recognition.DESCRIPTORSfor exactly that reason: a shape channel that carried the contact count would correlate with the interface channel by construction, and the whole point of the coverage and topology measures is that they are size-free.The total is
n_loop_contacts, notn_contacts: through 2.19.0 it was written under the latter name, whichtcren.pottsalso emits for a different quantity — the available pairs that engaged, 29 against this module’s 66 on 1ao7. Whichever pass ran last won the column, so a feature table built withoutpottshanded the footprint tally to a read-out standardized on the Potts population. The two now have two names.
- tcren.topology.footprint.footprint_topology_features(radii=(7.0, 8.0))[source]#
The shape-only feature names:
FOOTPRINT_FEATURESwithout the size counts, plus the radius-tagged Betti columns thatradiiproduces.- Parameters:
radii (Sequence[float])
- Return type:
tuple[str, …]
- tcren.topology.footprint.cell_counts(structure, cutoff=5.0)[source]#
Long
(loop, target, band, n)tally of TCR:pMHC residue contacts.One
ContactMapbuild covers both TCR interfaces.loopis"<chain>:<region>"restricted toCELL_LOOPS;targetis"pep"or"mhc";bandis the peptide third ("pN"/"pM"/"pC") or"mhc". Counting is a single polarsgroup_by— no Python loop over contacts.- Parameters:
structure (Structure) – a chain-typed, CDR-region-annotated TCR-pMHC structure.
cutoff (float) – heavy-atom contact threshold in Angstrom.
- Returns:
A frame with columns
loop,target,band,n. Empty if the structure makes no CDR-loop contact with the pMHC.- Return type:
DataFrame
- tcren.topology.footprint.footprint_batch(structures, *, cutoff=5.0, radii=(7.0, 8.0), organism='human')[source]#
One row per structure, over a folder / glob / archive or an iterable of structures.
A path is resolved through
tcren.paper.helpers.iter_annotated_set(), which sends every chain of every structure to arda in one mmseqs call per organism. Nothing here annotates per structure and nothing here uses a process pool: mmseqs is the parallel layer.The MHC pass then runs after chain typing, in one batched call. It is not optional and its order is not free:
classify_chainsleaves an MHC chain typed generically as"MHC", andContactMap.interface("tcr_mhc")matches on the supertype thattcren.mhc.annotate_mhc()assigns. Skip it and every TCR:MHC contact vanishes without an error – six of the twelve cells empty,p_germ_mhccollapses from ~0.78 to ~0.06, andH_cellis computed over a partition half of which is structurally unreachable.- Parameters:
structures (str | Path | Iterable[Structure]) – a directory, glob,
.tar.gzor manifest of structures, or an iterable of already chain-typedStructureobjects.cutoff (float) – heavy-atom contact threshold in Angstrom.
radii (Sequence[float]) – Calpha thresholds for the footprint flag complex.
organism (str) – organism for the single-structure path; ignored when a set is batched.
- Returns:
A frame with
pdb.idplus every feature offootprint_features().- Return type:
DataFrame
- tcren.topology.footprint.footprint_features(structure, *, cutoff=5.0, radii=(7.0, 8.0))[source]#
Every coverage and topology feature of one structure, as a flat row.
- Parameters:
structure (Structure) – a chain-typed, CDR-region-annotated TCR-pMHC structure. No canonical orientation is needed — every feature is invariant under rigid motion.
cutoff (float) – heavy-atom contact threshold in Angstrom.
radii (Sequence[float]) – Calpha thresholds at which the footprint’s flag complex is built. The patch count
b0is most informative at 7 A and the hole countb1at 8 A, so both ship.
- Returns:
{feature: value}overFOOTPRINT_FEATURESplusfp_b0_r<r>,fp_b1_r<r>,fp_chi_r<r>andfp_b0_frac_r<r>for each radius. Values arenanwhere the structure gives them no support (no contacts, a single contacted residue).- Return type:
dict[str, float]
Note
n_loop_contactsand its two components count the contacts the partition sees — those made by the six CDR loops. Framework contacts are outsideCELL_LOOPSand are excluded by construction, so this is smaller than the full interface contact count, and it is a different quantity again fromtcren.potts’sn_contacts, which counts the available pairs that engaged rather than the residue pairs in reach of a loop. The topology features are not restricted this way: they are built on every contacted pMHC residue, framework-driven ones included, because the footprint is a region on the pMHC and does not care which part of the receptor produced it.
tcren.topology.graph module#
The interface as a graph, and as a matrix.
Two families, both read off the same complex, both free of the two free parameters the older footprint measures carry.
The graph. tcren.footprint measures coverage by tallying residue-pair contacts into a
fixed partition – twelve cells (six CDR loops x {peptide, MHC}) or twenty-four – and then taking
the diversity of the tally. But the contact map at 5 A already is a bipartite graph, with the CDR
loop residues on one side and the pMHC residues they touch on the other, and the cell partition
throws that incidence structure away: it records how many contacts each bin holds, never which
residue touched which. Everything here is a functional of the biadjacency matrix B alone, so
there is no binning to choose. Degree evenness replaces cell entropy, the component count of the
contact graph replaces Betti-0 of a Calpha flag complex at an arbitrary 7 or 8 A, and the
normalised cyclomatic number replaces the raw one the footprint docstring rejects for tracking
interface size.
The matrix. A Calpha map between one region and another is an L x M matrix whose shape is
the two regions’ lengths, so no entry of it can be compared across a 9-mer and a 15-mer, or across
CDR3 loops of different length. Its singular values can. Turn the distance map into a soft
adjacency K = exp(-D^2 / 2 sigma^2) and the normalised spectrum of K is a shape descriptor
that does not know how long either side was: the effective rank fraction says how many independent
approach modes the interface has, and s2/s1 says how far it is from a rank-one (separable)
approach, which is what a receptor that leans on a surface rather than reading it produces. This is
not the graphon registration of tcren.contactmap.registered_map(), which resamples the map
onto a fixed grid and whose signal is epitope-identity provenance; a singular value is an invariant
of the map, not a resampling of it.
Alongside them, the one thing Calpha cannot see. d_Calpha - d_Cbeta over the contacting pairs is
positive when two side chains point at each other and negative when the backbones are close and the
side chains point away – the shape a pose forced to satisfy a contact-count objective takes.
>>> from tcren.interface_graph import graph_features, matrix_features
>>> graph_features(structure)["g_loop_overlap"]
>>> matrix_features(structure)["m_erank_tp"]
- tcren.topology.graph.MATRIX_FEATURES: tuple[str, ...] = ('m_erank_tp', 'm_gap_tp', 'm_erank_tm', 'm_gap_tm', 'm_face_tp', 'm_face_tm', 'ca_cb_agreement_tp', 'ca_cb_agreement_tm')#
The Calpha-against-Cbeta block.
m_face_tp,ca_cb_agreement_tpandca_cb_agreement_tmare promotions:tcren.posehas computed all three since the pose layer was written and no catalogued family reached them.m_face_tpwas calledsidechain_towardthere and is renamed rather than duplicated – catalguing a second name for a number the package already computes is the defect the 2026-07-28 descriptor audit removed. Onlym_face_tmis new.
- tcren.topology.graph.MHC_HELIX_REGIONS: tuple[str, ...] = ('HELIX_A1', 'HELIX_A2', 'HELIX_B1')#
a receptor approaching from above meets the two helix crests, and the floor residues it reaches at all it reaches through the peptide.
- Type:
The MHC groove regions the receptor meets. The floor is excluded
- tcren.topology.graph.PROMOTED_POSE_FEATURES: tuple[str, ...] = ('degree_evenness_tp', 'frac_well_coordinated_tp')#
Two descriptors that
tcren.posehas computed since the pose layer was written and that no catalogued family reached, becausePOSE_FEATURESis notDESCRIPTORS. They are the order-2 (participation ratio) reading of the same receptor-side degreesg_even_tcrreads at order 1, over TCR:peptide alone rather than the whole pMHC. Computed here from the contact map this module already builds, throughtcren.pose._degree_descriptors()so there is exactly one formula.max_degree_tp, which that function also returns, is deliberately not promoted.
- tcren.topology.graph.graph_features(structure, *, cutoff=5.0)[source]#
Every graph functional of the 5 A contact map, as a flat row.
- Parameters:
structure (Structure) – a chain-typed, CDR-region-annotated TCR-pMHC structure. Run
tcren.mhc.annotate_mhc()first or the MHC half of the graph is unreachable and every measure here is computed on peptide contacts alone.cutoff (float) – heavy-atom contact threshold in Angstrom. The only parameter in the family.
- Returns:
{feature: value}overGRAPH_FEATURESandPROMOTED_POSE_FEATURES. Values arenanwhere the structure gives them no support, never 0 – an interface that makes no contact has no evenness, and reporting 0 would rank it below a bad one.- Return type:
dict[str, float]
- tcren.topology.graph.matrix_features(structure, *, cutoff=5.0, sigma=5.0)[source]#
Length-agnostic comparisons of the interface Calpha and Cbeta maps.
- Parameters:
structure (Structure) – a chain-typed TCR-pMHC structure. The
_tmcolumns needtcren.mhc.annotate_mhc()to have run, sinceMHC_TYPESmatches the supertype it assigns; without it they arenan.cutoff (float) – heavy-atom contact threshold in Angstrom, for the
m_face_*pair.sigma (float)
- Returns:
{feature: value}overMATRIX_FEATURES,nanwhere unsupported.- Return type:
dict[str, float]
Note
Measured against peptide length on 196 Native2026 crystals (peptide 6-20 residues):
m_face_tm+0.023,ca_cb_agreement_tp-0.043,m_face_tp+0.161,ca_cb_agreement_tm-0.272. The first two are the cleanest columns in this module.
tcren.topology.surface module#
pMHC surface topology — the height and chemistry of the face a TCR actually sees.
A contact potential scores an interface that already exists. This module describes the pMHC
before a TCR arrives: how the presented surface is shaped and what it is made of. The peptide
sits in a groove between two helices, and a TCR approaching from above meets one surface — so the
descriptor is a height field h(x, y) over the groove plane, with per-cell chemistry painted
on. Two epitopes are then comparable as two rasters, which is what
surface_distance() exploits.
The method follows SURFMAP (Schweke et al., J Chem Inf Model 2022, 62:1595) — surface shell, per-point feature, grid, 8-neighbour smoothing, Manhattan map distance — with one deliberate departure. SURFMAP projects a globular protein onto an equal-area spherical chart because a closed surface has no undistorted plane. The TCR-facing pMHC surface is an open, near-planar patch sitting in a groove frame we can define from the coordinates, so a flat raster is both simpler and undistorting. Protein Surface Topography (Berkut et al., JBC 2019) supplies the other idea taken here: centre the chart on the functional site, so maps of different molecules are registered.
The frame is refit from every structure, not inherited. _groove_frame takes the SVD of the
MHC groove-floor Cα and signs the axes from the peptide and the TCR, so x = groove width,
y = peptide N→C, z = toward the TCR, always. Maps are therefore comparable without
prealigning the inputs — SURFMAP’s standing caveat — and without depending on whether the caller
ran tcren.docking.canonicalize_structure() first.
What “featureless” means numerically. surface_stats() reports relief (the height
spread over the peptide’s own footprint), peak_to_valley and frac_above_ridge (how much
peptide surface clears the helix rims). A flat, MHC-dominated landscape — the “featureless” epitope
of Tynan et al. (Nat Immunol 2007) and Motozono et al. (J Immunol 2014) — scores low on all
three; a bulged epitope scores high.
- tcren.topology.surface.DEFAULT_EXTENT = (-20.0, 20.0, -25.0, 25.0)#
Default map window in Å, (x0, x1, y0, y1), centred on the groove-floor centroid. Fixed rather than fitted per structure, so every map shares one grid and cells correspond across epitopes. Wide enough for a class-II 15-mer with its flanking overhangs.
- tcren.topology.surface.RIDGE_PERCENTILE = 90.0#
Percentile of MHC-helix cell heights taken as the groove rim in
surface_stats().
- tcren.topology.surface.MAX_GAP = 10.0#
Z cutoff for
surface_complementarity()— the largesth_tcr − h_pmhcclearance, in Å, at which a cell still counts as surface facing surface. Calibrated over 60 Native2026 crystals: insideCOMPARE_WINDOWthe cutoff reaches 0.895 of occupied pMHC cells at 4 Å, 0.951 at 10 Å and 0.962 with no cutoff at all, so 10 Å sits where the curve has gone flat. One-sided on purpose: the median gap is −1.7 Å and 71% of cells are interdigitated (the receptor’s lowest point in a cell lies below the groove’s highest point in the same cell), because the two faces interlock rather than stack. Confirmed on the wider population 2026-09-02: over 371 of the 374 Native2026 crystals the mean interdigitated fraction is 0.750 (s.d. 0.096) and the mean gap −1.14 Å, with the interdigitated volume 1,004 ų against a 373 ų void. The 60-crystal figures above stand for their own population; this is the same statement on six times as many.
- tcren.topology.surface.COMPARE_WINDOW = (12.0, 12.0)#
Half-widths
(x, y)in Å of the windowsurface_complementarity()compares over.DEFAULT_EXTENTis sized for a class-II 15-mer with overhangs, which is much wider than any receptor’s footprint: over the full extent a TCR projection reaches only 0.741 of occupied pMHC cells however large the Z cutoff, and the shortfall is nearly all at the far groove end (coverage 0.348 beyond y = +15 Å against 0.987 near y = 0). Cropping to ±12 Å lifts coverage to 0.951 without a Z cutoff doing the work. The peptide’s own cells are covered at 0.917 even over the full extent, so no part of the epitope surface is being discarded here.
- class tcren.topology.surface.SurfaceMap(structure_id, grid, extent, channels, source, scale='kd', n_atoms=0, peptide='', side='pmhc')[source]#
Bases:
objectA gridded height + chemistry map of one pMHC’s TCR-facing surface.
- Parameters:
structure_id (str)
grid (tuple[int, int])
extent (tuple[float, float, float, float])
channels (dict[str, ndarray])
source (ndarray)
scale (str)
n_atoms (int)
peptide (str)
side (str)
- structure_id: str#
- grid: tuple[int, int]#
- extent: tuple[float, float, float, float]#
- channels: dict[str, ndarray]#
- source: ndarray#
- scale: str#
- n_atoms: int#
- peptide: str#
- side: str#
- tcren.topology.surface.surface_map(structure, *, grid=(64, 32), extent=(-20.0, 20.0, -25.0, 25.0), scale='kd', probe=1.4, smooth=True, side='pmhc')[source]#
Build the TCR-facing height + chemistry map of one pMHC, or the TCR face that meets it.
Each cell keeps the highest point of the solvent-accessible surface above it — the first thing a TCR descending onto the groove would touch — and takes its chemistry from the atom that height belongs to. A mean over the column would average the exposed tip together with the flank behind it and blur exactly the relief the map exists to measure.
- Parameters:
structure – chain-typed (and ideally MHC-annotated) TCR-pMHC or pMHC structure.
grid (tuple[int, int]) –
(n_y, n_x)cell counts.extent (tuple[float, float, float, float]) –
(x0, x1, y0, y1)window in Å, registered on the peptide centroid.scale (str) – hydropathy scale for the
phobicchannel —"kd"(Kyte-Doolittle) or"mj"(the hydrophobicity axis recovered from the MJ 1996 contact matrix bytcren.potential.Potential.hydrophobicity_fit()).probe (float) – solvent probe radius in Å, added to each atom’s vdW radius.
smooth (bool) – apply the 8-neighbour average to the numeric channels.
side (str) –
"pmhc"(default) keeps the highest surface point per cell — the groove face a TCR descends onto."tcr"keeps the lowest point of the TCR V domains in the same groove frame, i.e. the receptor’s underside. Both maps carry the same grid, extent and frame, so they register cell-for-cell and can be compared bysurface_complementarity().
- Returns:
A
SurfaceMap.- Raises:
ValueError – if the groove plane or the pMHC atoms cannot be located.
- Return type:
- tcren.topology.surface.surface_stats(smap)[source]#
Reduce a map to the scalars that say how featured the epitope’s surface is.
Returns a dict with:
reliefStandard deviation of height over the cells the peptide owns — the spread of the peptide’s own topography. A flat epitope is small here.
peak_to_valleyMax minus min height over the same cells.
frac_above_ridgeFraction of peptide cells that clear the MHC helix crest (
RIDGE_PERCENTILEof the helix cell heights). This is the one that separates a bulged epitope (much of it above the rims) from a featureless one (buried between them), and it needs no reference structure.phobic_mean/phobic_centreMean hydropathy over all peptide cells, and over the central third along the groove axis (the TCR-facing bulge, where the Chowell et al. 2015 immunogenicity signal sits).
charge_meanMean formal charge over peptide cells.
area_frac_peptideShare of occupied cells the peptide owns rather than the MHC.
- Parameters:
smap (SurfaceMap)
- Return type:
dict[str, float]
- tcren.topology.surface.surface_distance(maps, channel='h', region=None)[source]#
Pairwise Manhattan distance between maps (SURFMAP eq. 1), normalised per shared cell.
Cells occupied in only one of the two maps carry no comparison, so the sum runs over the intersection and is divided by its size. Without that, a map with fewer occupied cells would look closer to everything.
- Parameters:
maps (list[SurfaceMap]) – maps sharing one
gridandextent.channel (str) – which channel to compare (
"h","phobic","charge").region (str | None) – restrict to cells of one source, e.g.
"peptide";Noneuses every cell.
- Returns:
(ids, distances)— the structure ids in row order and a square(n, n)matrix.- Raises:
ValueError – if the maps do not share a grid and extent.
- Return type:
tuple[list[str], ndarray]
- tcren.topology.surface.surface_complementarity(pmhc, tcr, *, max_gap=10.0, window=(12.0, 12.0), region=None, tcr_region=None)[source]#
Cell-for-cell agreement between a pMHC face and the TCR underside that meets it.
Both maps must come from the same structure (same groove frame, grid and extent), one built with
side="pmhc"and one withside="tcr". A cell enters the comparison when both maps reach it and the vertical clearance between them is at mostmax_gap— the Z cutoff that keeps the footprint to surface actually facing surface, rather than the map corners where the receptor overhangs nothing. SeeMAX_GAPfor how the default was chosen.Returned keys, all over the retained cells:
n_cells/coverageCells retained, and their share of the occupied pMHC cells inside
window.gap_mean/gap_sdMean and spread of
h_tcr − h_pmhcin Å. A tight, even gap is a well-packed interface; a wide or ragged one is a receptor resting on a few high points.interlock_frac/gap_depth/gap_height/gap_asymThe gap resolved by sign, because a cell where the receptor rides above the groove and one where it dips into it are different events and every pooled moment mixes them.
interlock_fracis the share of retained cells with a negative gap — the per-structure form of the 71 % this module’s calibration reports over the corpus.gap_depthis the mean of-gapover those cells alone, in Å: how far the receptor reaches in where it does.gap_heightis the mean ofgapover the positive cells alone, in Å: how high it stands off where it does not.gap_asymis(gap_vol - interlock) / (gap_vol + interlock), in [-1, 1], -1 for a face that only interlocks and +1 for one that only stands off. All four are NaN when their side is empty, never 0 — a face with no void has no standoff height, and 0 Å would read as perfect contact.gap_vol/interlock/gap_indexThe gap integrated over the contact plane, with the two signs kept apart because they mean opposite things and a mean cancels them.
gap_volis the void, \(\int \max(0, \mathrm{gap})\,\mathrm{d}A\), in ų;interlockis the interdigitated volume, \(\int \max(0, -\mathrm{gap})\,\mathrm{d}A\), in ų – and it is the larger of the two on a real interface, since the median gap is −1.7 Å and 71 % of cells interlock.gap_indexisgap_volover the retained contact area, in Å, which is the intensive form Jones & Thornton’s gap volume index takes. The area element is the grid cell,(x1-x0)(y1-y0) / (n_x n_y)= 0.977 Ų at the default extent and grid.shape_rPearson r between the two height fields. Positive is complementary: where the groove rises the receptor must ride up over it.
charge_r/charge_productPearson r between the two charge fields, and the mean of their product. Negative is complementary — plus meeting minus.
phobic_r/phobic_productThe same for hydropathy. Positive is complementary — apolar meeting apolar.
d_h/d_charge/d_phobicMean absolute difference per cell in each channel, the SURFMAP map distance of
surface_distance()applied across the two faces instead of across two structures.
- Raises:
ValueError – if the maps disagree on grid/extent, or are not one of each side.
- Parameters:
pmhc (SurfaceMap)
tcr (SurfaceMap)
max_gap (float)
window (tuple[float, float] | None)
region (str | tuple[str, ...] | None)
tcr_region (str | tuple[str, ...] | None)
- Return type:
dict[str, float]
- tcren.topology.surface.surface_tree(maps, channel='h', region=None, method='complete')[source]#
Hierarchical clustering of maps by
surface_distance()(SURFMAP’s distance tree).- Returns:
(ids, linkage)— the ids in row order and ascipy.cluster.hierarchy.linkage()matrix, ready fordendrogramorfcluster.- Parameters:
maps (list[SurfaceMap])
channel (str)
region (str | None)
method (str)
- tcren.topology.surface.surface_table(maps)[source]#
One row of
surface_stats()per map, with the peptide and grid occupancy.- Parameters:
maps (list[SurfaceMap])
- Return type:
DataFrame
tcren.topology.literature module#
Published interface descriptors, computed on the objects this package already builds.
Three families the protein-protein interface literature defines and this catalogue did not reach, each measured against the whole 19,213-structure benchmark corpus before being added here.
Surface complementarity and the gap. tcren.topology.surface has shipped
surface_complementarity() since before this module existed and not
one of its twelve outputs was catalogued, so three channels an audit listed as unreachable were in
fact two lines away:
surface_complementarity(surface_map(s, side="pmhc"), surface_map(s, side="tcr"))
Both faces are rasterised as height fields on one shared grid in a groove frame refit from the
structure, so the gap is a subtraction, not new geometry: gap = h_tcr - h_pmhc per cell, in
Angstrom. The module’s own calibration over 60 Native2026 crystals records the fact that makes the
sign readable – the median gap is -1.7 A and 71 % of cells are interdigitated, the receptor’s
lowest point in a cell lying below the groove’s highest point in the same cell. The two faces
interlock rather than stack, so a gap that grows is a receptor riding on a few high points.
The mean cancels the two signs, so the gap is also integrated over the contact plane with
them kept apart: sc_gap_vol is the void volume, sc_interlock the interdigitated volume,
both in Angstrom^3, and sc_gap_index is the void divided by the retained contact area, which is
the intensive form. On a real interface sc_interlock is the larger of the two.
Four more read the same field by sign rather than pooling it, because a cell the receptor
stands off from and one it reaches into are different events: sc_interlock_frac (the share of
cells that interlock, the per-structure form of the corpus 71 %), sc_gap_depth (how far in,
over the interlocked cells alone), sc_gap_height (how far off, over the void cells alone) and
sc_gap_asym (the balance of the two volumes, in [-1, 1]). sc_gap_mean is
interlock_frac weighting gap_height against gap_depth, so these are the three numbers
it collapses into one, not a reparameterisation of it.
Measured over 4,907 labelled structures, gap_mean and gap_sd carry the largest
binder/non-binder contrasts of any published descriptor tested (Cohen’s d -0.651 and -0.681
out-of-panel) at an R^2 on all 141 incumbent descriptors of 0.131 and 0.255 – they are a
channel this catalogue could not otherwise reach. sc_shape is the more familiar quantity
(Lawrence & Colman’s Sc is the same idea on a dot surface) and the less novel of the two: R^2
0.445, closest incumbent m_erank_tm at rho 0.414.
This is the gap channel Jones & Thornton 1996 named, not their gap volume index, which is a gap volume from a Voronoi construction divided by interface ASA. Ours is a raster height-field gap and is documented as such rather than cited to their formula.
Contact order. Plaxco et al. (J Mol Biol 1998, 277:985) normalise mean sequence separation by chain length, and the whole lesson of that paper is that the normalisation is what makes the quantity useful. The catalogue had no sequence-separation descriptor at all. On the MHC helix it is genuinely new (R^2 0.275); on the peptide it is not (R^2 0.519), because our peptides are overwhelmingly 9-mers and the length normalisation has almost no range to work with – 619 distinct values over 12,662 structures.
Participation coefficient. Di Paola et al. (Front Bioeng Biotechnol 2015, 3:170) read PPI
interfaces as contact networks and found P_i = 1 - sum_s (k_si / k_i)^2 their most
discriminative descriptor. On this corpus it is redundant: R^2 0.730 (TCR side) and 0.720
(pMHC side) on the incumbents, the pMHC side’s nearest neighbour being g_loop_overlap from the
same bipartite object. It is emitted anyway, at no extra cost over the contact map already built,
and tcren.recognition.STATUS says what it duplicates.
- tcren.topology.literature.LITERATURE_FEATURES: tuple[str, ...] = ('sc_shape', 'sc_charge', 'sc_phobic', 'sc_charge_prod', 'sc_phobic_prod', 'sc_gap_mean', 'sc_gap_sd', 'sc_gap_vol', 'sc_interlock', 'sc_gap_index', 'sc_interlock_frac', 'sc_gap_depth', 'sc_gap_height', 'sc_gap_asym', 'sc_dh', 'sc_dcharge', 'sc_dphobic', 'sc_cells', 'sc_coverage', 'co_pep', 'co_mhc', 'partcoef_tcr', 'partcoef_pmhc')#
Everything this module emits, in catalogue order.
- tcren.topology.literature.contact_order(t)[source]#
Plaxco’s length-normalised sequence spread, per target.
For each CDR loop, the mean absolute sequence separation \(|i - j|\) between the distinct target residues it reaches; averaged over loops and divided by the target’s own span L in residues, which is the normalisation Plaxco’s result rests on. Higher means one loop reaches residues far apart in the target sequence.
- Parameters:
t (DataFrame) – the long contact frame from
_contact_frame().- Returns:
{"co_pep": float, "co_mhc": float}, NaN where the target spans under two residues or no loop reaches more than one of its residues.- Return type:
dict[str, float]
- tcren.topology.literature.literature_features(structure, *, cutoff=5.0)[source]#
Every descriptor in this module, as a flat row.
- Parameters:
structure (Structure) – a chain-typed, CDR-region-annotated, MHC-annotated TCR-pMHC structure.
cutoff (float) – heavy-atom contact threshold in Angstrom for the graph-derived pair. The surface pair has its own length scales, fixed in
tcren.topology.surface.
- Returns:
{name: value}overLITERATURE_FEATURES, NaN wherever the structure gives a descriptor no support.- Return type:
dict[str, float]
- tcren.topology.literature.participation_coefficient(t)[source]#
Di Paola’s \(P_i = 1 - \sum_s (k_{si} / k_i)^2\), averaged over the engaged residues.
The modules \(s\) are the two things a TCR residue can touch – peptide and MHC – on the receptor side, and the six CDR loops on the pMHC side. \(k_{si}\) is residue i’s edge count into module s and \(k_i\) its total degree, so \(P_i\) is 0 for a residue whose contacts all land in one module and approaches \(1 - 1/S\) for one that spreads them evenly over all S. Higher is more shared.
- Parameters:
t (DataFrame) – the long contact frame from
_contact_frame().- Returns:
{"partcoef_tcr": float, "partcoef_pmhc": float}, NaN where no residue is engaged.- Return type:
dict[str, float]
- tcren.topology.literature.surface_features(structure)[source]#
The twelve surface-complementarity quantities, or NaN throughout if the maps cannot be built.
- Parameters:
structure (Structure) – a chain-typed, MHC-annotated TCR-pMHC structure.
- Returns:
{name: value}overSURFACE_FEATURES. NaN rather than 0 wherever a map is unreachable – an interface whose surface could not be rasterised has no complementarity, and a 0 would rank it as perfectly anti-complementary.- Return type:
dict[str, float]
tcren.topology.pose module#
Per-structure pose consistency: do the tight contacts carry the favourable chemistry?
tcren.cohort.coupling() measures the forced-pose signature across a cohort as
C* = corr(Q, dPhi): in a genuine complex a better interface holds more favourable contacts, so
the two channels rise together, while a generator that manufactures a pose optimises contacts
without the interface and breaks that tie. It is the right diagnostic and the wrong estimator for a
user with two or three models — at n = 2 the sample correlation is +-1 by construction, and its
sign is wrong in roughly a third to a half of draws (bench/scripts/coupling_smalln.py).
The tie it measures also holds within one structure, over that structure’s own contacts. In a
crystal the residue pairs that sit tightest are the ones whose identities are complementary, because
that is what selected the pose; a pose built to satisfy a contact-density prior has no such
alignment. Correlating contact tightness against contact favourability inside a single complex
therefore reads the same physics from n = 1 structure, over its ~20–120 interface pairs.
Three superimposable maps over one residue-pair index carry it, and
tcren.contacts.multi_contacts() already returns all three stacked (layer column):
d1— closest heavy-atom distance (the 5 A contact definition used everywhere else);d2— Cbeta distance (Calpha for glycine), i.e. where the side chains point;d3— Calpha distance, i.e. where the backbones sit.
The chemistry axis is J, not the raw potential entry. A contact energy splits as
e(a,b) = mean + H_tcr(a) + H_pep(b) + J(a,b), where the two H terms depend on one residue
each and J is the double-centred remainder. Correlating distance against raw e would partly
measure which residues happen to sit at the interface rather than whether they suit each other;
J is the pair-specific part, and complementarity lives there.
tcren.Potential.decompose() performs that split but only for a symmetric matrix, and TCRen2
is deliberately directional (TCR residue by peptide residue; symmetrising it costs measurable
accuracy). _double_centred() therefore applies the same two-way centring to the matrix as
given, which is well defined whether or not it is symmetric and reduces to decompose when it is.
Every descriptor is oriented higher = more crystal-like, so they compose with
tcren.cohort.q_score() under the same all-descriptors-higher-is-better convention.
Evaluation (ROC/PR/CI) belongs downstream in the benchmark repo, not here.
- tcren.topology.pose.pose_consistency(structure, potential=None, cutoff=5.0, ca_radius=12.0)[source]#
Cross-map consistency descriptors of one TCR:peptide interface.
Reads whether the structure’s tight contacts are its complementary ones — the within-structure analogue of
tcren.cohort.coupling(), and unlike it defined for a single complex. Every value is oriented so that higher is more crystal-like.- Parameters:
structure (Structure) – a chain-typed complex (
tcren.annotation.classify_chains()run) with a peptide chain and at least one receptor chain.potential – the residue-pair potential whose double-centred
Jsupplies the chemistry axis; defaults to the bundled TCRen2 matrix.cutoff (float) – the heavy-atom contact cutoff (A) defining the d1 layer and the contact margin.
ca_radius (float)
- Returns:
A dict with
POSE_FEATURESplusn_contacts(the pair count every value rests on) andn_cb_close. Descriptors that cannot be estimated — fewer than three contacts, a constant axis, no Cbeta-close pairs — come back asnanrather than a made-up number.- Return type:
dict[str, float]
Note
This is a pose readout, not a binder score: it says whether the geometry and the chemistry of one model agree, not whether the receptor binds.
- tcren.topology.pose.POSE_FEATURES = ('c_local', 'e_tight_minus_loose', 'frac_close_favourable', 'frac_cb_close_engaged', 'm_face_tp', 'margin_energy_slope', 'ca_energy_coupling_tp', 'ca_energy_slope_tp', 'frac_ca_close_engaged_tp', 'ca_cb_agreement_tp', 'ca_energy_coupling_tm', 'ca_energy_slope_tm', 'frac_ca_close_engaged_tm', 'ca_cb_agreement_tm', 'degree_evenness_tp', 'frac_well_coordinated_tp')#
The cross-map descriptors
pose_consistency()returns, each oriented positive-is-crystal-like. These are thekterms a pose score standardizes against a native-crystal reference.
- tcren.topology.pose.POSE_FEATURES_CONTACT = ('c_local', 'e_tight_minus_loose', 'frac_close_favourable', 'frac_cb_close_engaged', 'm_face_tp', 'margin_energy_slope')#
The six read over the realized 5 A contacts. Grouped by what they read, not by what they score. This is the subset to use for provenance (crystal against generated): on 374 crystals against 2,000 AlphaFold models it gives ROC 0.706 [0.675, 0.736] against 0.629 for the full set, because the shell and degree terms carry little provenance signal and dilute the whitened sum.
- tcren.topology.pose.POSE_FEATURES_SHELL = ('ca_energy_coupling_tp', 'ca_energy_slope_tp', 'frac_ca_close_engaged_tp', 'ca_cb_agreement_tp', 'ca_energy_coupling_tm', 'ca_energy_slope_tm', 'frac_ca_close_engaged_tm', 'ca_cb_agreement_tm')#
The eight read over the Calpha approach shell — an order of magnitude more pairs than the contact set, and the only ones that see residues that are close but form nothing.
- tcren.topology.pose.POSE_FEATURES_DEGREE = ('degree_evenness_tp', 'frac_well_coordinated_tp')#
The two describing how the contact budget is distributed over receptor residues.
Energetics: sums of a pair potential, and differences of them#
tcren.energetics package#
Everything measured in kT: sums of a pair potential over a contact set, and differences of them.
scoring owns the interface energy sum and the per-contact weighting the others use; ddg is the
change on mutation, virtually or on rebuilt coordinates; rotamers averages the contact map over
side-chain states so one modelled rotamer does not decide the answer. The potentials themselves are
tcren.potential and tcren.potts, one layer down – this package APPLIES a Hamiltonian,
it does not define one.
tcren.energetics.scoring module#
Candidate-peptide scoring by amino-acid substitution.
Ports the second half of run_TCRen.R: for each candidate peptide, substitute its
amino acids at the contacted peptide positions of a structure’s contact map and sum the
pairwise potential over all contacts. Lower scores indicate more favourable interactions.
- tcren.energetics.scoring.intra_peptide_energy(contact_map, potential, peptide=None, contact_weight='residue')[source]#
The peptide’s contact energy with itself, the term the interface energies omit.
Summed over
tcren.peptide_internal_contacts()(5 Å, sequence separation ≥ 3, so sequence neighbours — in contact because they are bonded, not because the peptide folded — are excluded), under the symmetrised potential(F + Fᵀ) / 2: an intra-chain pair has nofrom/toorientation to respect, and the canonical residue order labelling a pair’s sides is an artefact of the contact table, not chemistry. That matters for a directed potential such as TCRen and is a no-op for a symmetric one such as MJ. Lower is more favourable, as everywhere in tcren.On an extended class-I 9-mer this is a small, sparse term: such peptides make one or two internal contacts, so it moves a score only where a peptide is genuinely bulged or packed against itself. That is the point — it is the sequence-dependence the interface sum cannot see.
- Parameters:
contact_map (ContactMap) – a map built with
ContactMap.from_structure(..., peptide_internal=True).potential (Potential) – pairwise potential (MJ is the sensible choice — TCRen is derived from TCR↔peptide contacts, not from a chain’s contacts with itself).
peptide (str | None) – candidate sequence threaded onto the structure’s peptide positions.
None(default) scores the structure’s own residues.contact_weight (str) –
"residue"(default, one per contacting pair) or"atomic"(weight each pair by itsn_atom_contactsheavy-atom-pair count).
- Returns:
The summed energy;
0.0when the peptide contacts nothing of itself.- Return type:
float
- tcren.energetics.scoring.score_peptides(contact_map, candidates, potential, interface='tcr_peptide', require_same_length=True, substituted_side=None, tcr_regions='all', contact_weight='residue', intra_weight=0.0, intra_potential=None, weights=None)[source]#
Score candidate peptides against a structure’s contact map.
- Parameters:
contact_map (ContactMap) – The structure’s contact map.
candidates (Iterable[str]) – Candidate peptide sequences (one-letter).
potential (Potential) – Pairwise potential to score with.
interface (Literal['tcr_peptide', 'tcr_mhc', 'peptide_mhc']) – Which interface to score over (default
"tcr_peptide").require_same_length (bool) – Only score candidates whose length matches the structure’s peptide length (mirrors the legacy length join). Ignored when the contact map has no recorded peptide length.
substituted_side (str | None) –
"to"or"from"— which contact side the candidate is threaded onto. Defaults to the peptide side ofinterface.tcr_regions (str) – which TCR regions to keep on the TCR side (
"all"default = no filter = legacy behaviour;"cdr"or"cdr+fr"to restrict).contact_weight (str) –
"residue"(default, legacy) gives every contacting residue pair unit weight;"atomic"weights each residue pair by itsn_atom_contactsheavy-atom-pair count, so the energy tracks the LJ+Coulomb atom-pair sum more closely."atomic"requires the contact map to have been built withcount_atoms=True.intra_weight (float) – weight
wof the intra-peptide term, added asscore = E_interface + w * E_intra(intra_peptide_energy(), threaded with the same candidate).0.0(default) leaves the score byte-identical to the interface sum. A non-zero weight requires the contact map to have been built withpeptide_internal=True. The term is on the same energy scale as the interface sum, sow=1treats an internal contact as worth an interface contact.intra_potential (Potential | None) – potential for the intra-peptide term; defaults to MJ, not to
potential— TCRen is derived from TCR↔peptide contacts and says nothing about a chain’s contacts with itself.weights (ndarray | None) – an explicit per-contact multiplier applied on top of
contact_weight, one value per row of the selected interface and in its row order. This is how a rotamer-averaged contact probability (tcren.rotamers.contact_probabilities()), a per-position weight (position_weights()) or a contact-type filter enters the sum.None(default) leaves the score byte-identical.
- Returns:
Columns
complex.id,peptide,potential,scoresorted bycomplex.idthen ascendingscore.- Return type:
DataFrame
- tcren.energetics.scoring.score_structures(contact_maps, candidates, potential, **kwargs)[source]#
Score candidates against several structures and stack the results.
- Parameters:
contact_maps (Iterable[ContactMap])
candidates (Iterable[str])
potential (Potential)
- Return type:
DataFrame
- class tcren.energetics.scoring.RecognitionMatrix(positions, aa, energy, side, interface)[source]#
Bases:
objectPer-position × amino-acid substitution-energy landscape (see
recognition_matrix()).energy[i, a]is the summed pairwise potential over positionpositions[i]’s contacts when amino acidaa[a]sits there and the other side is held fixed. Lower = more favourable, so a per-position preference is-energy(higher = preferred). Positions with no contact are omitted. Entries areNaNfor amino acids the potential leaves undefined (e.g. cysteine pairs in TCRen), exactly asscore_peptides()drops those contacts — so reduce columns withnp.nan*ops.- Parameters:
positions (list)
aa (tuple)
energy (object)
side (str)
interface (str)
- positions: list#
one
(chain_type, region, pos, native_aa)tuple per row ofenergy
- aa: tuple#
the 20 amino-acid column order
- energy: object#
(n_positions, 20)float ndarray of substitution energies
- side: str#
which side was scanned (
"from"= TCR,"to"= peptide)
- interface: str#
- tcren.energetics.scoring.recognition_matrix(contact_map, potential, *, interface='tcr_peptide', side=None, tcr_regions='all')[source]#
The per-position × 20-AA substitution-energy matrix for one interface side.
For
interface="tcr_peptide",side="from"scans the TCR/CDR3 (the motif-matrix analog) andside="to"scans the peptide (the CPL-matrix analog). Each entry is the Φ energy summed over that position’s contacts with the given amino acid substituted in, the opposite side fixed — the same virtual-substitution path asscore_peptides(), resolved per position rather than summed over the whole sequence.- Parameters:
contact_map (ContactMap) – the structure’s contact map.
potential (Potential) – pairwise potential (TCRen for TCR:peptide).
interface (Literal['tcr_peptide', 'tcr_mhc', 'peptide_mhc']) – which interface to score over.
side (str | None) –
"from"or"to"; defaults to the non-peptide side fortcr_peptide(i.e. the TCR), and to the peptide side for the presentation interfaces.tcr_regions (str) – TCR-region filter (
"all"/"cdr"/"cdr+fr") — use"cdr"to restrict a TCR-side scan to the CDRs.
- Returns:
A
RecognitionMatrix. Rows are the contacted positions in(chain, region, pos)order; columns areRecognitionMatrix.aa.- Return type:
- tcren.energetics.scoring.POSITION_SCHEMES = ('uniform', 'central', 'tcr_facing')#
Named per-position weighting schemes for
position_weights().
- tcren.energetics.scoring.peptide_positions(contact_map, structure=None, interface='tcr_peptide', tcr_regions='all')[source]#
Annotate an interface’s contacts with the peptide position and role they involve.
The position was always there —
pos.toon thetcr_peptideinterface is the 0-based peptide index, because the peptide chain carries one full-length region starting at 0 — andtcren.refine.anchorshas always predicted anchors. The two were never joined, so nothing downstream could ask whether a contact sits on an anchor or in the TCR-facing bulge.- Parameters:
contact_map (ContactMap) – the structure’s contact map.
structure – the source structure. Passed to
tcren.refine.predict_anchors(), which then uses the real MHC-class call rather than the peptide-length heuristic. Recommended for class II, where a 12-20mer would otherwise be misread as class I.interface (Literal['tcr_peptide', 'tcr_mhc', 'peptide_mhc']) – which interface (must have a peptide side).
tcr_regions (str) – TCR-region filter, passed through to
ContactMap.interface().
- Returns:
The interface frame plus
peptide.pos(1-based P-number),peptide.aaandpeptide.role("anchor"or"tcr_facing").- Raises:
ValueError – if the peptide side carries no region markup (null positions).
- Return type:
DataFrame
- tcren.energetics.scoring.position_weights(annotated, scheme='uniform', length=None)[source]#
Per-contact weights from where along the peptide each contact sits.
A contact potential sums every contact alike, so a clash at an anchor — which the groove tolerates and a TCR never touches — costs the same as one under the CDR3 loops. These schemes let the sum say otherwise; feed the result to
score_peptides(..., weights=...).- Parameters:
annotated (DataFrame) – the frame
peptide_positions()returns.scheme (str) –
"uniform"(all ones — the default everywhere, so nothing moves unless asked),"central"(triangular inpeptide.pos, peaking at the middle of the peptide and falling to 0 at either terminus), or"tcr_facing"(1 off the anchors, 0 on them).length (int | None) – peptide length for the
"central"ramp; taken from the annotation when omitted.
- Returns:
One float per row of
annotated, in its row order.- Raises:
ValueError – for an unknown
scheme.- Return type:
ndarray
- tcren.energetics.scoring.position_profile(contact_map, potential, structure=None, interface='tcr_peptide', tcr_regions='all', contact_weight='residue')[source]#
Per-peptide-position decomposition of the interface energy.
The sum
score_peptides()reports, resolved along the peptide instead of collapsed: which positions carry the interaction, and which carry strain. Summingphireproduces the total.- Parameters:
contact_map (ContactMap) – the structure’s contact map.
potential (Potential) – the pairwise potential (TCRen for TCR:peptide).
structure – source structure, for the anchor call (see
peptide_positions()).interface (Literal['tcr_peptide', 'tcr_mhc', 'peptide_mhc']) – which interface.
tcr_regions (str) – TCR-region filter.
contact_weight (str) –
"residue"or"atomic", as elsewhere.
- Returns:
complex.id,peptide.pos,peptide.aa,peptide.role,n_contacts,phi.- Return type:
One row per contacted position
- tcren.energetics.scoring.central_strain(profile, band=0.3333333333333333)[source]#
Interface energy carried by the peptide’s central, TCR-facing band.
The review’s concern, made a number: a TCR has to clear the middle of the peptide to dock at all, so an unfavourable (positive) energy there is a viability question in a way that the same value at P1 or PΩ is not. Positive = the centre is repulsive.
- Parameters:
profile (DataFrame) – the frame
position_profile()returns.band (float) – fraction of the peptide’s length counted as central, centred on the middle.
- Returns:
Summed
phiover the central band, ornanfor an empty profile.- Return type:
float
tcren.energetics.mutation module#
ΔΔG of peptide point mutations, virtually or on rebuilt coordinates.
ddg = E(native) - E(mutant), and lower energy is a more favourable interface throughout
tcren, so a positive value flags a stabilising mutation: the mutant scores below the
native and binds better. A negative value is the destabilising one.
Two ways to get E(mutant), and they are not the same measurement.
Virtual (no structure argument) is the paper’s fast path: no atoms move, and the mutant
sequence is re-indexed against the potential over the native contact map. It is exact for the
energy bookkeeping and wrong about geometry — a contact that exists only because a long arginine
reaches across is still counted after that arginine is notionally an alanine, whose Cβ stops 4 Å
short. On the 374 reference crystals only 54 % of 5 Å TCR:peptide residue pairs have both side
chains in range at all, so this is not a rare corner.
Structural (pass structure=) rebuilds the mutant’s coordinates with
tcren.refine.substitute.substitute_peptide(), recomputes its contact map, and scores that.
For an alanine target it is exact and needs no relaxation, because alanine’s heavy atoms are
exactly backbone + Cβ: truncating at Cβ is the alanine, and a position mutated from glycine gets
an ideal-geometry Cβ built. Contacts the wild-type side chain alone was reaching then disappear, as
they physically must. For any other target the substituted residue is left as a Cβ stub, so its
reach is under-stated – see tcren.refine.substitute.substitute_peptide() for what would
have to be built.
- tcren.energetics.mutation.ddg(contact_map, native, mutant, potential, *, interface='tcr_peptide', tcr_regions='all', contact_weight='residue', structure=None, cutoff=5.0, sidechain=False, weights=None, mhc_potential=None)[source]#
ΔΔG of a peptide mutation as
E(native) - E(mutant).- Parameters:
contact_map (ContactMap) – The structure’s contact map.
native (str) – Native peptide sequence.
mutant (str) – Mutant peptide sequence (same length as
native).potential (Potential) – Pairwise potential to score with.
interface (Literal['tcr_peptide', 'tcr_mhc', 'peptide_mhc'] | str) – Which interface to score over (default
"tcr_peptide")."complex"scores BOTH peptide-bearing interfaces and sums them –potentialover TCR:peptide plusmhc_potentialover peptide:MHC – which is the conventiontcren.cpl.response_matrix()has always used for a response-matrix cell, and the one an activation read-out needs: the assay fires only if the peptide is presented AND the receptor engages. Scoring"tcr_peptide"alone answers a recognition question and is blind to presentation, so a peptide whose anchors are destroyed scores like any other. Note the two channels are NOT separable in a library that varies every position.mhc_potential (Potential | None) – The peptide:MHC potential used by
interface="complex".None(default) is Miyazawa-Jernigan, matchingtcren.cpl.response_matrix(). Ignored for any single-interface call.tcr_regions (str) – Which TCR regions to keep on the TCR side (passed through to
score_peptides).contact_weight (str) –
"residue"(default) or"atomic"; passed through toscore_peptides.structure (Structure | None) – When given, the mutant is built — its side chains are replaced and its contact map recomputed — rather than re-indexed on the native map. Exact for an alanine target; a Cβ stub for anything longer (see the module docstring).
cutoff (float) – Contact distance threshold for the rebuilt map (Å). Ignored when
structureisNone, in which casecontact_map’s own cutoff applies.sidechain (bool) – Passed to the rebuilt contact map, so a caller filtering on side-chain participation filters the mutant by the mutant’s reach and not the native’s.
weights (ndarray | None) – An explicit per-contact multiplier, one value per row of the selected interface and in its row order, forwarded to
tcren.scoring.score_peptides(). Its use here is to replace the map’s hard 0/1 contact indicator with a contact probability –tcren.potts.contact_probabilities()’p_model, or a rotamer-averaged occupancy – so a substitution is scored against how often each pair actually touches rather than against one frozen snapshot of whether it did.None(default) leaves the result byte-identical. Ignored on a rebuilt mutant map (structure=given), whose rows are its own and no longer align with the native’s.
- Returns:
E(native) - E(mutant); positive means the mutant has the LOWER energy, i.e. the mutation is stabilising. Negative means destabilising. Always0.0for interfaces that do not contain the peptide (e.g."tcr_mhc"), since a peptide mutation cannot affect them.- Return type:
float
- tcren.energetics.mutation.alanine_scan(contact_map, native, potential, *, interface='tcr_peptide', tcr_regions='all', contact_weight='residue', structure=None, cutoff=5.0, sidechain=False)[source]#
Alanine scan of the native peptide.
Mutates each position of
nativeto alanine in turn and reports the ΔΔG of that single substitution. One row per peptide position.With
structureonly that position is truncated to alanine in 3D and the contact map recomputed, so a position whose side chain was the only thing reaching the TCR loses those contacts as it physically must, while its neighbours keep theirs. This is the case the structural path gets exactly right (see the module docstring), and it costs one contact-map rebuild per position.Before 2.25.0 this path threaded the whole peptide through
substitute_peptide(), which truncates every residue to backbone + Cβ. The scan therefore measured each position against a poly-stub baseline rather than the native: on 1ao7 the native sequence threaded back through it kept 14 of 29 TCR:peptide contacts, and the resulting offset appeared in every position, including positions with no contacts at all.- Parameters:
contact_map (ContactMap) – The structure’s contact map.
native (str) – Native peptide sequence.
potential (Potential) – Pairwise potential to score with.
interface (Literal['tcr_peptide', 'tcr_mhc', 'peptide_mhc']) – Which interface to score over (default
"tcr_peptide").tcr_regions (str) – Which TCR regions to keep on the TCR side.
contact_weight (str) –
"residue"(default) or"atomic"; passed through toscore_peptides.structure (Structure | None) – Build each alanine mutant and rescore it on its own contact map.
cutoff (float) – Contact threshold for the rebuilt maps (Å).
sidechain (bool) – Passed to the rebuilt maps.
- Returns:
Columns
pos(0-based),wt_aa(native residue at that position) andddG(E(native) - E(Ala@pos)). Positions without TCR contacts yieldddG == 0.0. For interfaces that do not contain the peptide (e.g."tcr_mhc") every position yieldsddG == 0.0.- Return type:
DataFrame
- tcren.energetics.mutation.tcr_alanine_scan(contact_map, structure, potential, *, peptide=None, tcr_regions='cdr', contact_weight='residue', cutoff=5.0, sidechain=False)[source]#
Alanine scan of the receptor side, on rebuilt coordinates.
The mirror of
alanine_scan(). Each contacted TCR residue is truncated to alanine in 3D bytcren.refine.substitute.substitute_residues(), the contact map is recomputed and the interface rescored, so a loop residue whose side chain was the only thing bridging to the peptide loses those contacts exactly as it physically must. One rebuild per contacted residue.Only residues that actually contact the peptide are walked, because a residue with no contact has
ddG == 0by construction and rebuilding it would cost a contact map for nothing.Scored over
"tcr_peptide"alone: a receptor substitution cannot change the peptide:MHC energy, so the complex sum would only add a constant.- Parameters:
contact_map (ContactMap) – The native structure’s contact map.
structure (Structure) – The annotated structure the map came from. Required — there is no virtual path here, because truncating a receptor side chain without moving atoms would leave every contact it made in place, which is the failure mode this function exists to fix.
potential (Potential) – Pairwise potential to score with.
peptide (str | None) – Peptide sequence; taken from the structure’s peptide chain when omitted.
tcr_regions (str) – Which TCR regions to walk —
"cdr"(default),"cdr+fr"or"all".contact_weight (str) –
"residue"(default) or"atomic".cutoff (float) – Contact threshold for the rebuilt maps (Å).
sidechain (bool) – Passed to the rebuilt maps.
- Returns:
One row per contacted receptor residue, with
chain.id,chain.type(TRA/TRB),region.type,residue.index,pos(0-based within its region),wt_aaandddG=E(native) - E(Ala@residue). A positiveddGmarks a stabilising residue: removing it costs energy, so it was earning its place.- Return type:
DataFrame
- tcren.energetics.mutation.tcr_alanine_reference(scan)[source]#
Per-loop poly-alanine references, summed from a
tcr_alanine_scan().Four numbers per structure: the germline loops together, each CDR3 on its own, and their total. Each is the sum of the per-residue 3D ΔΔGs of that loop, which is the additive reading and is defined whether or not a loop is engaged (an unengaged loop contributes 0).
It is deliberately not the energy of mutating a whole loop to poly-alanine in one pass. Those differ once atoms move: truncating every side chain at once loses contacts that each residue alone retains, so the one-pass value is not the sum of the parts. The additive form is the one that says how much each residue earns.
- Parameters:
scan (DataFrame) – The frame returned by
tcr_alanine_scan().- Returns:
dPhi_ala_cdr12,dPhi_ala_cdr3a,dPhi_ala_cdr3banddPhi_ala_tcr.- Return type:
dict[str, float]
- tcren.energetics.mutation.neoantigen_ddg(contact_map, native, mutants, potential, **kw)[source]#
ΔΔG of candidate neoantigen mutants relative to a native peptide.
- Parameters:
contact_map (ContactMap) – The structure’s contact map.
native (str) – Native peptide sequence.
mutants (Iterable[str]) – Candidate mutant peptides (each the same length as
native).potential (Potential) – Pairwise potential to score with.
**kw – Forwarded to
ddg()(interface,tcr_regions).
- Returns:
Columns
native,mutantandddG(E(native) - E(mutant); positive means the mutant has the lower energy, i.e. the substitution is stabilising), one row per mutant – so ranking candidates by descendingddGputs the best first.- Return type:
DataFrame
- tcren.energetics.mutation.reference_delta(contact_map, peptide, potential, *, interface='tcr_peptide', reference_aa='A', tcr_regions='all', contact_weight='residue', structure=None, cutoff=5.0, sidechain=False, mhc_potential=None)[source]#
Poly-alanine reference difference ΔΦ = Φ(peptide) − Φ(reference) on this contact map.
ΔΦ is the full-peptide alanine-scan difference — the sum of the per-position native→Ala ΔΔGs of
alanine_scan(). It subtracts the interface’s identity-independent baseline Φ(reference), i.e. what the pose geometry scores when every peptide residue isreference_aa, leaving the sequence-specific part.On a fixed contact map this is Φ(peptide) minus a constant, so it does not change the ranking of candidates threaded onto one structure. It differs from raw Φ only across candidates that each have their own structure (e.g. AlphaFold peptide-swap models), where it normalises out the per-pose interface geometry. That normalisation rescues forced / wrong-register poses whose geometry corrupts the raw contact energy (the CPL ila1 case: TCR-ranking ROC 0.35 → 0.83), at a small cost on clones where the generated geometry is itself informative — so it is a scoring mode for generated poses, not a default. It is not an affinity ΔΔG: a dimensionless contact-preference difference, not a free energy (see
tcren.refine.registerfor the geometry defect it corrects). Empirically both raw Φ and ΔΦ are within-receptor ranking scores, not binding constants — on the ATLAS SPR set they correlate with ΔG/Kd/koff/kon only at ρ ≤ 0.3 in magnitude (off-rate comes fromtcren.mechanics).- Parameters:
contact_map (ContactMap) – The candidate’s own contact map.
peptide (str) – The candidate peptide sequence.
potential (Potential) – Pairwise potential to score with.
interface (Literal['tcr_peptide', 'tcr_mhc', 'peptide_mhc'] | str) – Which interface to score over (default
"tcr_peptide")."complex"sums both peptide-bearing interfaces, which is the whole-complex ΔΦ a combinatorial library ranking needs – the receptor term alone cannot see a destroyed anchor.mhc_potential (Potential | None) – peptide:MHC potential for
interface="complex"(default Miyazawa-Jernigan).reference_aa (str) – The amino acid the reference peptide is made of (default alanine).
tcr_regions (str) – Which TCR regions to keep on the TCR side.
contact_weight (str) –
"residue"(default) or"atomic".structure (Structure | None) – Build the reference peptide and score it on its own contact map instead of re-indexing it on the candidate’s. This changes what ΔΦ means. Virtually, the poly-Ala baseline is charged for every contact the real side chains make, so ΔΦ measures only the substitution of identities on a fixed contact set. Structurally, the baseline is what the backbone plus Cβ alone can reach, so ΔΦ measures what the side chains contribute at all – which is what the poly-alanine reference is meant to mean, and on 1ao7 is the difference between 29 TCR:peptide contacts and 14.
cutoff (float) – Contact threshold for the rebuilt reference map (Å).
sidechain (bool) – Passed to the rebuilt reference map, so a side-chain-filtered score filters the reference by the reference’s own reach.
- Returns:
ΔΦ = Φ(peptide) − Φ(reference); more negative = the sequence adds more favourable contacts than the reference baseline.
0.0for interfaces without the peptide (e.g."tcr_mhc").- Return type:
float
- tcren.energetics.mutation.SMOOTH_INTERFACES: dict[str, tuple[tuple[str, str], ...]] = {'peptide': (('tcr_peptide', 'to'), ('peptide_mhc', 'from')), 'tcr': (('tcr_peptide', 'from'), ('tcr_mhc', 'from'))}#
Which interfaces a smoothed reference varies, and which side of each carries the varying chain.
The complex Hamiltonian is \(\Phi = c_{\mathrm{TP}}\Phi_{\mathrm{TCR:pep}} + c_{\mathrm{TM}}\Phi_{\mathrm{TCR:MHC}} + c_{\mathrm{PM}}\Phi_{\mathrm{pep:MHC}}\), so a substitution on one chain leaves one whole term untouched and that term drops out of the difference:
varying the peptide kills \(\Delta\Phi_{\mathrm{TCR:MHC}}\) – no peptide residue is in it;
varying the TCR kills \(\Delta\Phi_{\mathrm{pep:MHC}}\) – no TCR residue is in it.
Each remaining interface is scored with its own potential and divided by its own Native2026 scale, so the two surviving terms are commensurate before they are added.
- tcren.energetics.mutation.smoothed_reference(contact_map, potential, *, side='peptide', beta=1.0, background=None, tcr_regions='all', chain=None, mhc_potential=None, weights=None)[source]#
Boltzmann-smoothed reference difference \(\delta\Phi\) and its curvature.
The hard reference of
reference_delta()subtracts the energy of one arbitrary sequence (poly-alanine). This subtracts the free energy of the residue background instead, so the baseline is a distribution rather than a choice of amino acid.Both interfaces that contain the varying chain are summed; the third drops out identically (see
SMOOTH_INTERFACES). Because no interface energy carries a within-chain term, \(\Phi\) is a sum of independent local fields over the varying positions,\[\varphi_i(a) \;=\; \sum_{\text{interfaces } I} c_I \sum_{j \,:\, (i,j) \in C_I} e_I(a, y_j)\]– position \(i\) of the varying chain, amino acid \(a\), summed over that position’s contacts with the frozen partners \(y\), each interface weighted by its own \(c_I = 1/\mathrm{sd}_{\mathrm{Native2026}}(\Phi_I)\). The partition function therefore factorizes exactly, and the reference free energy is available in closed form:
\[\Phi_{\mathrm{ref}} \;=\; -\frac{1}{\beta}\sum_i \log \sum_a p(a)\, e^{-\beta \varphi_i(a)}\]with \(p\) the background composition over the 20 amino acids. Then
\[\delta\Phi \;=\; \Phi(\text{observed}) - \Phi_{\mathrm{ref}}, \qquad \operatorname{Var}\Phi \;=\; \sum_i \operatorname{Var}_{\beta}\!\left[\varphi_i\right]\]where the variance is taken under the tilted weights \(p(a)e^{-\beta\varphi_i(a)}/\sum_b p(b)e^{-\beta\varphi_i(b)}\).
\(\delta\Phi\) is a first difference in sequence, against a smooth baseline; \(\operatorname{Var}\Phi\) is the second cumulant of the same log partition function, i.e. how sharply that position’s energy responds to residue identity at all. A position whose twenty fields are equal contributes nothing to either; one with a single strongly preferred residue contributes to both.
\(\beta\) sets how much of the background is averaged over. \(\beta \to 0\) gives the arithmetic mean field \(\varphi_i(a_i) - \langle\varphi_i\rangle_p\), which is the reference state a combinatorial peptide library actually realises (every other position held at an equimolar mixture); \(\beta \to \infty\) gives \(\varphi_i(a_i) - \min_a \varphi_i(a)\), the distance from the best residue available at that position. The default \(\beta = 1\) is the potential’s own scale, since a Boltzmann-inverted potential is already in units of \(k_{\mathrm B}T\).
- Parameters:
contact_map (ContactMap) – the structure’s contact map.
potential (Potential) – the TCR:peptide potential (TCRen2).
side (str) –
"peptide"(vary the peptide, receptor frozen – the peptide scan) or"tcr"(vary the receptor, peptide frozen – the TCR scan). SeeSMOOTH_INTERFACES.beta (float) – inverse temperature in the potential’s units (default 1.0).
background – 20-vector of amino-acid frequencies in
tcren.scoring.RecognitionMatrixcolumn order, orNone(default) for the equimolar background.tcr_regions (str) – TCR-region filter, applied on the TCR side.
chain (str | None) – restrict a
side="tcr"scan to one chain ("TRA"or"TRB"), so the two chains can be read apart rather than pooled.None(default) keeps both.mhc_potential (Potential | None) – the potential for the presentation interface (default Miyazawa-Jernigan, which is what
tcren.pipelineassigns there).weights (dict | None) –
{interface: coefficient}override;None(default) reads the Native2026 scales throughtcren.pipeline._phi_scale().
- Returns:
{"dPhi": float, "varPhi": float, "n_positions": int}. Both sums are0.0over an empty position set, which is what an interface with no contacts on that side should score.- Return type:
dict
tcren.energetics.rotamers module#
Rotamer-averaged contacts — a contact map that does not depend on one side-chain guess.
A modelled side chain is a guess, and the contact map reads it as fact. Rotate a Tyr by one χ1 step and a contact appears or vanishes; the energy moves with it, though nothing about the two residues changed. On forced poses (AlphaFold peptide swaps, threaded mutants) that is a large part of why a pairwise contact energy stops discriminating.
Rather than pick a better single rotamer, this samples the χ angles of each interface side chain,
weights each rotamer by its Boltzmann factor under DOPE, and returns a contact probability per
residue pair instead of a 0/1 indicator. The score becomes sum_ij p_ij * e(a_i, b_j) — the same
sum with a softer indicator — via score_peptides(..., weights=...).
What is exact and what is not.
Rotating every atom beyond Cβ about the Cα–Cβ axis is a χ1 change: deeper torsions ride along unchanged. The same holds at each subsequent depth. So the geometry is exact, not interpolated.
Rotamers are enumerated on a uniform grid anchored on the native χ (so the input pose is always in the set), not drawn from a backbone-dependent library. A Dunbrack-style library would give better priors; DOPE supplies the energy here instead.
Residues are weighted independently, each against the rest of the structure held at its input conformation (mean field). Two side chains that would have to move together are not coupled.
max_chidefaults to 2. χ1 and χ2 carry the reorientation; χ3/χ4 on Arg, Lys, Met and Glu are reachable by raising it, at 3× the rotamers per extra angle.
DOPE is used for the weights and nothing else, deliberately: the potential tcren scores with must not also be the one that decided which contacts exist.
Measured (six crystals, χ1 of every peptide side chain rotated 120° — a deliberately wrong
guess). The hard 5 Å contact set keeps a Jaccard of only 0.66 against its unperturbed self;
the rotamer-averaged map keeps 0.95. The energy is the sharper reading: mean |ΔΦ| falls from
0.524 to 0.054, a factor of ten, against interface energies whose own magnitude is 0.4–2.2. On
1ao7 the hard map’s error under one wrong rotamer (−0.64) is larger than the energy itself
(−0.50), and on 2ckb it flips a +0.43 to +1.66. That is the failure mode this module exists to
remove.
Because most alternative rotamers clash, DOPE separates them by ~200 units and the weights are
sharp; temperature around 1 is therefore close to a repack, and raising it hedges further. The
default is not tuned for softness — 1.0 measured best on |ΔΦ| and near-best on Jaccard.
- tcren.energetics.rotamers.N_CHI = {'A': 0, 'C': 1, 'D': 2, 'E': 3, 'F': 2, 'G': 0, 'H': 2, 'I': 2, 'K': 4, 'L': 2, 'M': 3, 'N': 2, 'P': 0, 'Q': 3, 'R': 4, 'S': 1, 'T': 1, 'V': 1, 'W': 2, 'Y': 2}#
Rotatable side-chain torsions per residue (one-letter). Ala/Gly have none; Pro’s ring is closed.
- tcren.energetics.rotamers.DEFAULT_STEP = 120.0#
degrees between sampled rotamers (the g-/t/g+ spacing)
- tcren.energetics.rotamers.DEFAULT_TEMPERATURE = 1.0#
Boltzmann temperature in DOPE energy units
- tcren.energetics.rotamers.chi_axes(residue)[source]#
Rotatable torsions of one residue as
(axis_start, axis_end, moving_atom_indices).χ_n rotates about the bond from the depth
n-2atom to the depthn-1atom, moving every atom deeper than that (Cα–Cβ for χ1 moves everything from Cγ out). Indices are intoresidue.atoms. Branch points resolve to the alphabetically first name at the depth, which is the convention that makes Ile’s χ2 run along Cβ–Cγ1 rather than Cβ–Cγ2.- Return type:
list[tuple[int, int, ndarray]]
- tcren.energetics.rotamers.residue_rotamers(residue, max_chi=2, step=120.0)[source]#
All sampled conformers of one residue as
(n_rotamers, n_atoms, 3).The first entry is always the input conformation, so a caller that keeps only the best rotamer can never do worse than the pose it was given.
- Parameters:
max_chi (int)
step (float)
- Return type:
ndarray
- tcren.energetics.rotamers.contact_probabilities(structure, interface='tcr_peptide', *, cutoff=5.0, max_chi=2, step=120.0, temperature=1.0, shell=12.0)[source]#
Rotamer-averaged contact probability for every residue pair near an interface.
Each interface side chain is rotated through its χ grid, weighted by
exp(-E_DOPE / T)against the rest of the structure, and a pair’s probability is the chance that some pair of their rotamers is withincutoff—p_ij = sum_r sum_s w_r w_s [d(r, s) <= cutoff].Pairs the input pose does not contact can still acquire probability, and pairs it does contact can fall below 1. Both are the point: a contact map built from one modelled rotamer asserts certainty it does not have.
- Parameters:
structure – chain-typed, annotated structure.
interface (str) –
"tcr_peptide","tcr_mhc"or"peptide_mhc".cutoff (float) – contact distance (Å), matching the hard contact map.
max_chi (int) – how many χ angles to sample per residue (see the module docstring).
step (float) – degrees between sampled rotamers.
temperature (float) – Boltzmann temperature in DOPE units. Larger = flatter weights; as it approaches 0 the result converges on the single best rotamer per residue.
shell (float) – only partner atoms within this distance enter a residue’s DOPE weighting.
- Returns:
chain.id.from,residue.index.from,chain.id.to,residue.index.to,residue.aa.from,residue.aa.to,dist(the input pose’s closest heavy-atom distance) andp(the rotamer-averaged contact probability), one row per pair withp > 0.fromis the interface’s first side (the TCR for"tcr_peptide"), asinterface()orients it, so a directed potential indexes the right way round.- Raises:
ValueError – for an unknown
interface, or if the structure is not chain-typed.- Return type:
DataFrame
- tcren.energetics.rotamers.soft_energy(structure, potential, interface='tcr_peptide', **kwargs)[source]#
Interface energy summed over rotamer-averaged contact probabilities.
sum_ij p_ij * e(a_i, b_j)— the same sum astcren.pipeline._interface_energy(), with the hard 0/1 contact indicator replaced bycontact_probabilities().- Parameters:
structure – chain-typed, annotated structure.
potential – the pairwise potential (TCRen for TCR:peptide).
interface (str) – which interface.
**kwargs – passed to
contact_probabilities().
- Returns:
The summed energy.
- Return type:
float
- tcren.energetics.rotamers.repack(structure, *, chains=('PEPTIDE',), max_chi=2, step=120.0, temperature=1.0, shell=12.0)[source]#
Place each side chain in the χ conformer DOPE likes best — the native
_relaxpacker.This is what the rigid-body refiner could not do.
substitute_peptidestrips a peptide to backbone + Cβ and the DOPE Monte Carlo moves it rigidly, so a refined model came back with 44 heavy atoms where the crystal peptide has 77 — nothing to compare against OpenMM or FlexPepDock on any side-chain-sensitive measure. Seerefine/CPP_REWRITE.md.The same enumeration as
residue_rotamers(), run in C++: exact χ rotations (every atom past the axis moves together, so deeper torsions are carried unchanged), scored with the same DOPE tablecontact_probabilities()uses, mean field (each residue against its neighbours at their input conformation).- Parameters:
structure – chain-typed structure.
chains (tuple[str, ...] | None) – which
chain_typevalues to repack.Nonerepacks everything, which is slow and rarely what you want — the default is the peptide alone.max_chi (int) – how many χ angles per residue (
3 ** max_chiconformers).step (float) – degrees between conformers.
temperature (float) – Boltzmann temperature for the reported weights, in DOPE units.
shell (float) – only atoms within this distance enter a residue’s energy.
- Returns:
(structure, report)— a copy with the repacked side chains, and apolars.DataFramewithchain.id,residue.index,residue.aa,n_conformers,energy(of the chosen conformer) andp_best(its Boltzmann weight; near 1 means the choice was unambiguous).- Raises:
ValueError – if nothing is selected to repack.
Mechanics: the contact map as something that can break#
tcren.mechanics package#
Forces, stiffness and the kinetics proxies: the contact map as something that can break.
Everything here is mechanical rather than thermodynamic – a stiffness in N/m, a rupture force in N, a work in J, a margin in Angstrom. No potential enters springs or stability; the network is built from the geometry alone, which is what makes the off-rate proxy independent of the energy channel it is compared against.
tcren.mechanics.springs module#
Interface mechanics: the TCR↔pMHC contact map as a network of breakable springs.
Each inter-body residue contact is a Hookean spring (stiffness from atomic-contact multiplicity), anchored at the two Cα atoms. Two quantities are exposed:
stiffness_tensor()— the linear-response stiffness tensorK = Σ kᵢ ûᵢ⊗ûᵢof the interface, split into a tensile component along the docking axis and an in-plane shear component.rupture()— a static steered-unbinding cartoon: rigidly displace the pMHC body along a pull direction, letting springs break past a strain threshold, and record the peak resisting force and the cumulative work.
Rationale (validated on ATLAS, 2026): these mechanical measures track the kinetic stability of the
complex — the dissociation off-rate koff — far better than the equilibrium ΔG/Kd (Bell–Evans: rupture
resistance reflects the dissociation barrier, not the well depth). This is the physically apt axis for the
TCR, a mechanosensor whose pMHC bonds are catch bonds. Pure-numpy, single structure, no MD.
- tcren.mechanics.springs.WEIGHTS = ('unit', 'count', 'invdist2')#
Spring-stiffness models for an interface contact.
"unit"= 1 per contact (pure topology);"count"= heavy-atom-pair multiplicity;"invdist2"= multiplicity / dist² (Hookean-ish, the validated default).
- class tcren.mechanics.springs.InterfaceSprings(a, b, k, rest, axis)[source]#
Bases:
objectThe TCR↔pMHC spring network of one structure.
a/bare the (n, 3) Cα anchor coordinates on the TCR and pMHC sides;kthe (n,) spring stiffnesses;restthe (n,) rest lengths|b − a|;axisthe unit docking axis (stiffness- weighted separation of the two interface centroids, pointing TCR→pMHC).- Parameters:
a (ndarray)
b (ndarray)
k (ndarray)
rest (ndarray)
axis (ndarray)
- a: ndarray#
- b: ndarray#
- k: ndarray#
- rest: ndarray#
- axis: ndarray#
- tcren.mechanics.springs.interface_springs(structure, *, cutoff=8.0, weight='invdist2')[source]#
Build the TCR↔pMHC interface spring network from residue contacts.
- Parameters:
- Returns:
The
InterfaceSprings(empty arrays if no inter-body contact is found).- Return type:
- tcren.mechanics.springs.stiffness_tensor(structure, *, cutoff=8.0, weight='invdist2')[source]#
Linear-response stiffness descriptors of the TCR↔pMHC interface.
Forms
K = Σ kᵢ ûᵢ⊗ûᵢover interface springs and resolves it along the docking axis.- Returns a dict with:
S_tot(trace K, total stiffness),K_tens(tensile, along the docking axis),K_shear(in-plane,S_tot − K_tens),aniso(K_shear / K_tens),lam_max/lam_min(extreme eigenvalues),n_spring. Allnanif < 3 springs.
- Parameters:
structure (Structure)
cutoff (float)
weight (str)
- Return type:
dict[str, float]
- tcren.mechanics.springs.rupture(structure, *, direction='tensile', cutoff=8.0, weight='invdist2', break_strain=0.5, steps=80)[source]#
Steered-unbinding cartoon: pull the pMHC body off the TCR and measure rupture resistance.
The pMHC anchors are rigidly translated along the pull
direction; each spring resists in tension (Hookean) and is removed once its strain exceedsbreak_strain. Integrates until all springs break.- Parameters:
direction (str) –
"tensile"(docking axis; the default and the right choice for affinity/koff ranking),"shear"(stiffest in-plane), or"auto"(the minimum-force of tensile and shear — the easiest rupture path). For affinity prediction prefer"tensile"orstiffness_tensor()K_tens: on ATLAS the"auto"/"shear"force is size-confounded (its koff correlation collapses under a weighted-size control) while tensile survives it.break_strain (float) – fractional extension at which a spring breaks (the tuning knob; 0.5 = 50 %).
steps (int) – displacement increments.
structure (Structure)
cutoff (float)
weight (str)
- Return type:
dict[str, float]
- Returns a dict:
rupture_force(peak resisting force along the pull),rupture_work (∫ force · displacement),
n_spring,break_strain.nanif < 3 springs.
- tcren.mechanics.springs.coupling_residues(structure, *, cutoff=5.0)[source]#
Residues that couple the pre-formed internal scaffold to the TCR↔pMHC interface.
Counts residues sitting in both an intra-body contact and the binding interface (computed from the bound complex; the intra-body scaffold — Vα↔Vβ pairing, peptide-in-groove — is pre-formed, so this approximates the internal-contact residues the interface recruits):
couple_pep— peptide residues contacting both MHC (groove-anchored) and TCR (dual-role);couple_mhc— MHC residues contacting both peptide and TCR (groove-rim presenting);couple_tcr— TCR residues in the Vα↔Vβ interface that also contact pMHC (combining-site apex).Also returns
couple_totalandn_interface(interface residue count, a size denominator).Note: as a binding estimator these are weak/underpowered on current data —
couple_peptracks the dissociation off-rate (koff) at r≈−0.34 (class I, borderline; partially survives interface-size control), but the TCR/MHC sets are near-null. Useful primarily as interpretable structural descriptors.- Parameters:
structure (Structure)
cutoff (float)
- Return type:
dict[str, int]
- tcren.mechanics.springs.interface_mechanics(structure, *, cutoff=8.0, weight='invdist2', direction='tensile', break_strain=0.5)[source]#
Every mechanics column for one annotated structure, in one dict.
The union of
stiffness_tensor(),rupture()andcoupling_residues()under their shipped defaults. This is the one definition of “the mechanics row”: bothtcren mechanicsandtcren recognize --mechanicscall it, so the two agree by construction rather than by two call sites being kept in step.The structure must already be annotated — chain typing and the MHC call are batch operations that belong to the caller’s single mmseqs search, not to a per-structure helper. An unannotated MHC chain silently empties the TCR:MHC half of the spring network.
- Parameters:
structure (Structure) – An annotated structure (chains typed, MHC called, peptide present).
cutoff (float) – Heavy-atom contact cutoff (Å) defining a spring.
weight (str) – Spring-stiffness model, one of
WEIGHTS.direction (str) – Rupture pull direction —
tensile,shearorauto.break_strain (float) – Fractional extension at which a spring breaks.
- Returns:
n_spring,S_tot,K_tens,K_shear,aniso,rupture_force,rupture_work,couple_pep/couple_mhc/couple_tcr/couple_totalandn_interface.- Return type:
dict[str, float]
Example
>>> interface_mechanics(s)["K_tens"] 41.7
tcren.mechanics.stability module#
Contact stability / fragility at the TCR:peptide interface.
The 5 Å contact cutoff is a hard threshold on a continuous distance: a contact at 4.9 Å has almost
no positional slack (a ~1 Å shift, or tightening the cutoff, kills it), while one at 3.5 Å is robust.
contact_stability() reads that slack directly off the contact map. For each TCR:peptide
contact — a receptor-residue / peptide-residue pair whose closest heavy-atom pair is within
cutoff (the same contact definition as tcren.contacts.geometry.all_atom_contacts()) — the
margin m = cutoff - dmin is the contact’s positional tolerance, and a rigid isotropic shift of
size delta loses it with probability clip((delta - m) / (2*delta), 0, 1). Aggregated over the
interface these give mean_margin (how deep the contacts sit), frac_robust (fraction with
m >= delta) and exp_lost (expected contacts lost under a delta shift) — a coordinate-only
read of interface positional confidence, the physical analogue of an interface PAE.
The per-residue-pair minimum-distance scan is a native _geom kernel; the numpy implementation
behind it (_contact_stability_numpy()) is the reference and a fallback where the extension is
unavailable.
- class tcren.mechanics.stability.StabilityReport(n_contacts, mean_margin, frac_marg_lt1, frac_robust, exp_lost)[source]#
Bases:
objectTCR:peptide contact-stability summary.
- Variables:
n_contacts (int) – Receptor-residue / peptide-residue contacts within
cutoff.mean_margin (float) – Mean
cutoff - dminover contacts (Å); larger = contacts sit deeper.frac_marg_lt1 (float) – Fraction of contacts with margin below
delta(fragile).frac_robust (float) – Fraction of contacts with margin at least
delta(robust to adeltashift).exp_lost (float) – Expected number of contacts lost under a
deltaÅ isotropic shift.
- Parameters:
n_contacts (int)
mean_margin (float)
frac_marg_lt1 (float)
frac_robust (float)
exp_lost (float)
- n_contacts: int#
- mean_margin: float#
- frac_marg_lt1: float#
- frac_robust: float#
- exp_lost: float#
- tcren.mechanics.stability.contact_stability(structure, *, cutoff=5.0, delta=1.0)[source]#
Contact stability / fragility of the TCR:peptide interface.
- Parameters:
structure (Structure) – A chain-typed complex with a peptide chain and at least one receptor chain.
cutoff (float) – Contact distance cutoff (Å); the closest heavy-atom pair of a residue pair must be within this to count as a contact (matches
all_atom_contacts).delta (float) – Positional shift (Å) used for the fragility metrics (
frac_marg_lt1,exp_lost).
- Returns:
- Raises:
ValueError – If the structure has no peptide chain or no receptor chain.
- Return type:
tcren.mechanics.dynamics module#
Peptide conformational stability — does the pose hold, or was it merely drawn that way?
A contact potential scores whichever conformation it is handed. It cannot tell a peptide whose own side chains hold it in the TCR-facing conformation from one that happens to have been modelled there, because both present the same contact list. That blind spot is a specific, testable hypothesis rather than a general complaint:
Sewell (2026-08,
manuscripts/2026-tcren/suggestions/sewell.txt): Supplementary Fig. 4 of Dolton et al. (J Clin Invest 2024) shows a substantial intra-peptide interaction between P3 and P6 that stabilises the central bulge the TCR reads. “Poor binders could perhaps still make many contacts but fail to stabilise the productive peptide conformation” — which would explain why an additive contact model describes some systems well and others badly.
peptide_stability() measures it. Backbone φ/ψ are sampled by Metropolis Monte Carlo at
temperature against the DOPE potential, and the readout is not a better pose but how far the peptide
wanders: rmsf (the spread of the sampled ensemble) and drift (how far its mean moves from the
conformation it was given). A peptide held by its own interactions stays; one that is not, does not.
The intra-peptide term is a switch, not a fixture. intra_weight=0 reruns the identical
sampling with the peptide’s contacts with itself removed, so delta_rmsf between the two is the
stabilisation those interactions actually provide. That difference is the quantity Sewell’s
hypothesis is about, and stability_table() computes it for a set of structures.
Moves are torsional and exact: perturb one φ or ψ and rotate every atom downstream of that bond. A
peptide has two free termini, so no loop closure is needed — the same rotation primitive
tcren.rotamers uses for χ, applied to N–Cα and Cα–C.
What this is not. It is not molecular dynamics: there is no solvent, no force field, no time, and
DOPE is a knowledge-based potential rather than an energy surface with physical units. It samples a
knowledge-based conformational basin, and rmsf is comparable between structures run with the
same settings, not against an MD RMSF in Å.
- tcren.mechanics.dynamics.DEFAULT_TEMPERATURE = 4.0#
MC temperature in DOPE units
- tcren.mechanics.dynamics.DEFAULT_SIGMA = 6.0#
st. dev. of a single φ/ψ perturbation, degrees
- tcren.mechanics.dynamics.DEFAULT_ANCHOR_W = 1.0#
harmonic weight holding the anchor Cα in their pockets
- class tcren.mechanics.dynamics.Stability(structure_id, peptide, rmsf, drift, energy, energy_start, energy_gap, accept_rate, n_samples, intra_weight)[source]#
Bases:
objectHow well one peptide holds the conformation it was given.
- Parameters:
structure_id (str)
peptide (str)
rmsf (float)
drift (float)
energy (float)
energy_start (float)
energy_gap (float)
accept_rate (float)
n_samples (int)
intra_weight (float)
- structure_id: str#
- peptide: str#
- rmsf: float#
ensemble spread, Å — larger = floppier
- drift: float#
distance of the ensemble mean from the input pose, Å
- energy: float#
best DOPE energy seen
- energy_start: float#
DOPE energy of the pose as given
- energy_gap: float#
how much better MC could do than the input
- Type:
energy_start - energy
- accept_rate: float#
- n_samples: int#
- intra_weight: float#
- tcren.mechanics.dynamics.backbone_torsions(peptide_atoms)[source]#
φ/ψ torsions of a peptide as
(axis_start, axis_end, moving_atom_indices).peptide_atomsis[(residue_index, atom_name), ...]in the flat order the coordinates are packed in. Indices returned are into that same array.A torsional rotation splits the chain at the bond and turns one side, so:
φ rotates about N–Cα, moving everything of that residue except N and Cα — the side chain included, since Cβ hangs off Cα and is not on the axis — plus every later residue;
ψ rotates about Cα–C, moving only the carbonyl O and every later residue. The side chain stays: it is on the Cα side of the bond.
- tcren.mechanics.dynamics.peptide_stability(structure, *, intra_weight=1.0, n_steps=4000, temperature=4.0, sigma_deg=6.0, anchor_weight=1.0, min_sep=3, shell=12.0, burn_in=None, seed=0)[source]#
Sample the peptide backbone and report how far it wanders from the pose it was given.
- Parameters:
structure – chain-typed, MHC-annotated structure.
intra_weight (float) – weight on the peptide’s DOPE contacts with itself.
1.0includes them,0.0removes them — the comparison Sewell’s hypothesis turns on.n_steps (int) – Metropolis steps.
temperature (float) – MC temperature in DOPE units. Higher = more exploration; the value matters only in that every structure being compared must use the same one.
sigma_deg (float) – st. dev. of one φ/ψ perturbation, in degrees.
anchor_weight (float) – harmonic weight pinning the anchor Cα, so the peptide samples conformations rather than falling out of the groove. Anchors come from
tcren.refine.predict_anchors().min_sep (int) – sequence separation below which intra-peptide pairs are ignored — neighbours are in contact by covalent geometry, not by folding.
shell (float) – partner atoms within this distance of the peptide enter the energy.
burn_in (int | None) – steps discarded before sampling; defaults to
n_steps // 8.seed (int) – RNG seed. The result is deterministic given one.
- Returns:
A
Stability.- Raises:
ValueError – if the structure has no chain-typed peptide, or its atoms are not contiguous.
- Return type:
- tcren.mechanics.dynamics.stability_table(structures, *, intra_weights=(1.0, 0.0), **kwargs)[source]#
Run
peptide_stability()at eachintra_weightand report the difference.The difference is the point.
delta_rmsf = rmsf(intra=0) - rmsf(intra=1)is how much the peptide’s own contacts steady it: positive means removing them lets the backbone wander further, i.e. those interactions are holding the conformation together.- Parameters:
structures – an iterable of chain-typed, MHC-annotated structures.
intra_weights – the weights to run. The default pair gives
delta_rmsf/delta_drift.**kwargs – passed to
peptide_stability().
- Returns:
structure.id,peptide,rmsf/drift/energyat each weight (suffixed_intra1/_intra0for the default pair), and the deltas.- Return type:
One row per structure
Docking geometry and canonical orientation#
tcren.docking package#
Canonical TCR-pMHC orientation: MHC-frame superposition + chain renaming.
- class tcren.docking.CanonResult(rotation, translation, rmsd, n_anchor_atoms, reference_id, frame, reversed_dock=None, chain_map=<factory>)[source]
Bases:
objectComposed rigid transform that maps a structure into the canonical frame.
- Parameters:
rotation (ndarray)
translation (ndarray)
rmsd (float)
n_anchor_atoms (int)
reference_id (str | None)
frame (Literal['native', 'pca'])
reversed_dock (bool | None)
chain_map (dict[str, str])
- rotation: ndarray
- translation: ndarray
- rmsd: float
- n_anchor_atoms: int
- reference_id: str | None
- frame: Literal['native', 'pca']
- reversed_dock: bool | None
- chain_map: dict[str, str]
- tcren.docking.canonical_frame(structure, reference_id=None, force_pca=False)[source]
Compose the MHC superposition with the per-class
R_canon(native), or fit the canonical axes directly from the query (PCA fallback when no DB / too few anchors).- Parameters:
structure (Structure)
reference_id (str | None)
force_pca (bool)
- Return type:
- tcren.docking.build_canonical_frame()[source]
(Re)compute
R_canonfor each class reference and return the artifact dict.Writes nothing; the caller serialises to
tcren/data/canonical_frame.json.- Return type:
dict
- tcren.docking.detect_reverse_dock(structure, rotation, translation, margin=2.0)[source]
Apply the canonical transform and check the TCR α/β handedness.
Canonical: VDJ (TRB/TRD) at
+y(peptide-C side) and VJ (TRA/TRG) at−y. ReturnsTruewhen the VJ chain is on the+yside of the VDJ chain by more thanmarginÅ (reverse dock),Falsefor a canonical dock, andNonewhen a TCR side is missing.- Parameters:
structure (Structure)
rotation (ndarray)
translation (ndarray)
margin (float)
- Return type:
bool | None
- tcren.docking.select_primary_complex(structure)[source]
Keep one mutually-contacting chain per canonical role (one TCR-pMHC complex).
Chosen as a connected unit so chains that do not touch the peptide (notably β2m) are not grabbed from another copy: the primary peptide is the one most embedded in an MHC-α groove (then most TCR contacts, then shortest); the TCR α/β and MHC-α are taken by contacts to that peptide; β2m / MHC-β by contacts to the chosen MHC-α. No-op for single complexes.
- tcren.docking.rename_chains(structure)[source]
Return a copy with only the canonical complex, chain ids remapped per
CHAIN_RENAME, plus the old→new map.Chains with no canonical role (tags, additives, unrelated proteins) are dropped so the output is exactly the A–E TCR-pMHC complex. Raises
ValueErrorif two source chains map to the same canonical id (unresolved multi-copy — runselect_primary_complex()first).
- tcren.docking.canonicalize_structure(structure, reference_id=None, force_pca=False, select_primary=True)[source]
Orient an (already chain-typed + MHC-annotated) structure into the canonical frame.
Returns the oriented, A–E renamed structure and the populated
CanonResult(transform, frame, rmsd, reverse-dock flag, chain map). Coordinates are transformed; the chain roles drive the rename, so order matters (frame + reverse-dock are read before the transform clears region markup).- Parameters:
structure (Structure)
reference_id (str | None)
force_pca (bool)
select_primary (bool)
- Return type:
tuple[Structure, CanonResult]
- tcren.docking.align_to_canonical(structure, reference_id=None, organism='human', force_pca=False)[source]
Align a NEW (parsed) structure onto the Native2026 canonical frame.
Runs chain typing + MHC annotation, then
canonicalize_structure(). The stored per-classR_canonis reused, so the result is in the same frame as the dataset and the composed transform in the returnedCanonResultreplays the placement exactly.- Parameters:
structure (Structure)
reference_id (str | None)
organism (str)
force_pca (bool)
- Return type:
tuple[Structure, CanonResult]
- tcren.docking.check_oriented_complex(structure, max_peptide_len=25, max_offset=25.0, max_tcr_gap=15.0, max_orphan=70.0)[source]
Geometric sanity check on an oriented A–E complex;
(ok, reason).Rejects structures whose canonical placement is inconsistent: missing / overlong peptide, peptide not at the groove centre (≈ origin), the TCR not engaging the peptide, or any chain stranded far from the complex (an orphan copy that survived primary-complex selection).
- Parameters:
max_peptide_len (int)
max_offset (float)
max_tcr_gap (float)
max_orphan (float)
- tcren.docking.run_folder(structures, out, metadata=None, organism='human', reference_id=None, force_pca=False, threads=None, mmcif=False, compress=False)[source]
Canonicalize a file or folder of structures; write oriented structures + a metadata table.
Output format follows
mmcif(.cifvs.pdb) andcompress(trailing.gz); plain PDB by default (passcompress=Trueto rebuild the gzipped Canonical2026 set).Annotation is BATCHED — one mmseqs search for all TCR chains (per organism) and one for all MHC chains across the whole set (mmseqs parallelises internally; never per-structure, never Python-threaded). Only the embarrassingly-parallel, mmseqs-free stages — parsing and the structural alignment + write — use a thread pool (
threadsworker threads, defaultos.cpu_count()).- Parameters:
structures (str | Path)
out (str | Path)
metadata (str | Path | None)
organism (str)
reference_id (str | None)
force_pca (bool)
threads (int | None)
mmcif (bool)
compress (bool)
- Return type:
DataFrame
- tcren.docking.superimpose(structure, db_dir=None, organism='human', annotate=True)[source]
Superimpose
structureonto a canonical database by MHC (see module docstring).structureis chain-typed + MHC-annotated here unlessannotate=False(used by the threaded batch driver, which annotates the whole input set in one mmseqs pass first). The ensemble alignment itself is mmseqs-free, so it is the part safe to run on a thread pool.db_dirdefaults todata/Canonical2026. Returns the oriented, A–E renamed structure and the consensusOrientationResult(averaged over the ensemble).- Parameters:
structure (Structure)
db_dir (str | Path | None)
organism (str)
annotate (bool)
- Return type:
tuple[Structure, OrientationResult]
- tcren.docking.run_superimpose(structures, out, db_dir=None, organism='human', mmcif=False, compress=False, threads=None)[source]
Superimpose input structure(s) onto a canonical database; write oriented structures.
structuresis a file, directory,.tar.gz, or a shell glob.outis an output directory, or — for a single input — a structure file whose extension must matchmmcif/compress. Annotation is BATCHED (one mmseqs pass over all inputs); only the mmseqs-free ensemble alignment + write runs on the thread pool (threadsworkers, default all cores). Seetcren.docking.superimpose()for the MHC-ensemble method.- Parameters:
structures (str | Path)
out (str | Path)
db_dir (str | Path | None)
organism (str)
mmcif (bool)
compress (bool)
threads (int | None)
- Return type:
DataFrame
- class tcren.docking.DockingAngles(crossing_angle, crossing_angle_signed, incident_angle, cell_type, n_va, n_vb)[source]
Bases:
objectTCR docking geometry relative to the MHC groove (all angles in degrees).
- Parameters:
crossing_angle (float)
crossing_angle_signed (float)
incident_angle (float)
cell_type (str)
n_va (int)
n_vb (int)
- crossing_angle: float
- crossing_angle_signed: float
- incident_angle: float
- cell_type: str
- n_va: int
- n_vb: int
- class tcren.docking.TcrPlacement(height, shift_u, shift_w, offset, n_cdr)[source]
Bases:
objectWhere the CDR footprint sits over the groove, in Angstrom, in the groove frame
(u, w, n).DockingAnglessays how the receptor is rotated; this says where it is placed. The two are independent: a TCR can carry a canonical crossing angle while its loops sit too high above the groove or shifted off the peptide, which is a failure mode no angle can see.The reference point is the CDR-loop Calpha centroid, not the whole variable-domain centroid. That is not a refinement, it is required:
_groove_frame()fixesnalong the peptide-centroid-to-TCR-centroid direction and setsw = n x u, so the whole-TCR centroid has a lateral component of exactly zero by construction. The loop footprint is also the part that actually touches the pMHC.- Variables:
height (float) –
(CDR centroid - peptide centroid) . n— how far the loops ride above the groove plane. The literal “how far is the TCR from the pMHC plane” scalar. Note it is a distance:DockingGeometry.tcr_unit_zis a dimensionless unit-vector component and is a different quantity.shift_u (float) – displacement along the groove long axis (peptide N->C); positive = toward the peptide C-terminus.
shift_w (float) – displacement across the groove width axis; positive = toward the
n x uside.offset (float) –
hypot(shift_u, shift_w)— total in-plane displacement of the footprint centre from the peptide centre, sign-free.n_cdr (int) – CDR-loop Calpha atoms used.
- Parameters:
height (float)
shift_u (float)
shift_w (float)
offset (float)
n_cdr (int)
- height: float
- shift_u: float
- shift_w: float
- offset: float
- n_cdr: int
- tcren.docking.tcr_placement(structure)[source]
Translational placement of the CDR footprint over the pMHC groove (Angstrom).
Complements
docking_angles()(rotation) with the translations the crossing and incident angles do not carry. Built on the same peptide-PCA groove frame, so it needs no native database and no mmseqs — but it does need CDR region markup (classify_chains), because the footprint centroid is the only reference point with a defined lateral component.- Parameters:
structure (Structure) – a chain-typed, region-annotated TCR-pMHC structure.
- Returns:
A
TcrPlacement.- Raises:
ValueError – if the groove frame is degenerate, or no CDR-loop Calpha is present.
- Return type:
- tcren.docking.docking_angles(structure)[source]
Crossing + incident angle of a chain-typed TCR-pMHC complex.
The structure must already be chain-typed (
classify_chains) and MHC-annotated (annotate_mhc) so the canonical frame can be fit. The frame is taken from the query’s own Cα cloud (PCA), so the result needs no native database.- Parameters:
structure (Structure) – a chain-typed, MHC-annotated TCR-pMHC structure.
- Returns:
A
DockingAngleswith the crossing and incident angles.- Raises:
ValueError – if a receptor chain pair (TRA/TRB or TRG/TRD) is missing, or the canonical frame is degenerate.
- Return type:
- tcren.docking.crossing_incident_from_vector(v_canon)[source]
(crossing, crossing_signed, incident)degrees from a Vα→Vβ vector in canonical axes.v_canonis[vx, vy, vz]along canonical x (groove width), y (groove long axis, peptide N→C) and z (MHC→TCR normal). The crossing angle is measured in the groove plane (xy) from the long axis; the incident angle is the elevation out of that plane.- Parameters:
v_canon (ndarray)
- Return type:
tuple[float, float, float]
- class tcren.docking.DockingGeometry(d, torsion, tcr_unit_y, tcr_unit_z, mhc_unit_y, mhc_unit_z)[source]
Bases:
objectTCR:pMHC rigid-body docking geometry — TCRdock’s 6-parameter form.
- Parameters:
d (float)
torsion (float)
tcr_unit_y (float)
tcr_unit_z (float)
mhc_unit_y (float)
mhc_unit_z (float)
- d: float
- torsion: float
- tcr_unit_y: float
- tcr_unit_z: float
- mhc_unit_y: float
- mhc_unit_z: float
- to_dict()[source]
- Return type:
dict
- tcren.docking.docking_geometry(structure)[source]
Compute the TCRdock docking geometry of a chain-typed, MHC-annotated TCR-pMHC structure.
The structure must already be chain-typed (
tcren.annotation.classify_chains()) and MHC-annotated (tcren.mhc.annotate_mhc()) with arda region markup on the TCR chains.- Parameters:
structure – a chain-typed, annotated TCR-pMHC
Structure.- Returns:
The
DockingGeometry.- Raises:
ValueError – if the MHC β-sheet core or a complete TCR Vα/Vβ core cannot be located.
- Return type:
- tcren.docking.substitute_tcr(host, donor, *, by='mhc')[source]
Graft the
donorTCR onto thehostpMHC → a chimeric TCR:pMHCStructure.The result keeps the host peptide + MHC chains and the donor TCR chains (relabelled to avoid id collisions), with the donor TCR rigidly placed by the
byanchor:"mhc"— superpose the donor MHC groove onto the host MHC groove (both inputs must betcren.mhc.annotate_mhc()-annotated); the donor TCR keeps its native docking geometry."tcr"— superpose the donor TCR onto the host TCR; the donor TCR inherits the host pose.
Both inputs must be chain-typed (
tcren.annotation.classify_chains()). RaisesValueErrorifbyis invalid, the host lacks a peptide or MHC chain, the donor lacks a TCR chain, or too few matched Cα anchors are found to superpose.
tcren.docking.angles module#
TCR docking geometry: crossing angle and incident (tilt) angle.
The TCR “docking angle” (Rudolph, Stanfield & Wilson 2006; Garcia et al.) describes how the
αβ (or γδ) TCR sits on top of the peptide-MHC groove. It is computed here directly from the
canonical frame (tcren.docking.frame.canonical_frame()), so no external package
(TCRdock / STCRpy) is required:
the crossing angle is the angle between the Vα→Vβ pseudo-axis projected into the MHC groove plane and the groove long axis (peptide N→C, canonical
+y— collinear with the MHC α1 helix to within a few degrees). Reported on[0, 180); canonical αβ TCRs cluster around ~20–70°. A signed variant ([-180, 180)) carries the handedness of the docking.the incident (tilt) angle is the elevation of the same Vα→Vβ vector out of the groove plane (canonical
zis the MHC→TCR normal): positive when Vβ rides higher above the groove than Vα.
The Vα/Vβ landmarks are the centroids of the variable-domain Cα atoms of the two receptor chains (TRA/TRB for αβ, TRG/TRD for γδ). The frame is fit from the query itself (PCA), so the calculation needs neither the native database nor mmseqs once the structure is chain-typed.
- class tcren.docking.angles.DockingAngles(crossing_angle, crossing_angle_signed, incident_angle, cell_type, n_va, n_vb)[source]#
Bases:
objectTCR docking geometry relative to the MHC groove (all angles in degrees).
- Parameters:
crossing_angle (float)
crossing_angle_signed (float)
incident_angle (float)
cell_type (str)
n_va (int)
n_vb (int)
- crossing_angle: float#
- crossing_angle_signed: float#
- incident_angle: float#
- cell_type: str#
- n_va: int#
- n_vb: int#
- tcren.docking.angles.crossing_incident_from_vector(v_canon)[source]#
(crossing, crossing_signed, incident)degrees from a Vα→Vβ vector in canonical axes.v_canonis[vx, vy, vz]along canonical x (groove width), y (groove long axis, peptide N→C) and z (MHC→TCR normal). The crossing angle is measured in the groove plane (xy) from the long axis; the incident angle is the elevation out of that plane.- Parameters:
v_canon (ndarray)
- Return type:
tuple[float, float, float]
- tcren.docking.angles.docking_angles(structure)[source]#
Crossing + incident angle of a chain-typed TCR-pMHC complex.
The structure must already be chain-typed (
classify_chains) and MHC-annotated (annotate_mhc) so the canonical frame can be fit. The frame is taken from the query’s own Cα cloud (PCA), so the result needs no native database.- Parameters:
structure (Structure) – a chain-typed, MHC-annotated TCR-pMHC structure.
- Returns:
A
DockingAngleswith the crossing and incident angles.- Raises:
ValueError – if a receptor chain pair (TRA/TRB or TRG/TRD) is missing, or the canonical frame is degenerate.
- Return type:
- class tcren.docking.angles.TcrPlacement(height, shift_u, shift_w, offset, n_cdr)[source]#
Bases:
objectWhere the CDR footprint sits over the groove, in Angstrom, in the groove frame
(u, w, n).DockingAnglessays how the receptor is rotated; this says where it is placed. The two are independent: a TCR can carry a canonical crossing angle while its loops sit too high above the groove or shifted off the peptide, which is a failure mode no angle can see.The reference point is the CDR-loop Calpha centroid, not the whole variable-domain centroid. That is not a refinement, it is required:
_groove_frame()fixesnalong the peptide-centroid-to-TCR-centroid direction and setsw = n x u, so the whole-TCR centroid has a lateral component of exactly zero by construction. The loop footprint is also the part that actually touches the pMHC.- Variables:
height (float) –
(CDR centroid - peptide centroid) . n— how far the loops ride above the groove plane. The literal “how far is the TCR from the pMHC plane” scalar. Note it is a distance:DockingGeometry.tcr_unit_zis a dimensionless unit-vector component and is a different quantity.shift_u (float) – displacement along the groove long axis (peptide N->C); positive = toward the peptide C-terminus.
shift_w (float) – displacement across the groove width axis; positive = toward the
n x uside.offset (float) –
hypot(shift_u, shift_w)— total in-plane displacement of the footprint centre from the peptide centre, sign-free.n_cdr (int) – CDR-loop Calpha atoms used.
- Parameters:
height (float)
shift_u (float)
shift_w (float)
offset (float)
n_cdr (int)
- height: float#
- shift_u: float#
- shift_w: float#
- offset: float#
- n_cdr: int#
- tcren.docking.angles.tcr_placement(structure)[source]#
Translational placement of the CDR footprint over the pMHC groove (Angstrom).
Complements
docking_angles()(rotation) with the translations the crossing and incident angles do not carry. Built on the same peptide-PCA groove frame, so it needs no native database and no mmseqs — but it does need CDR region markup (classify_chains), because the footprint centroid is the only reference point with a defined lateral component.- Parameters:
structure (Structure) – a chain-typed, region-annotated TCR-pMHC structure.
- Returns:
A
TcrPlacement.- Raises:
ValueError – if the groove frame is degenerate, or no CDR-loop Calpha is present.
- Return type:
tcren.docking.tcrdock_geometry module#
TCR:pMHC docking geometry — native reimplementation of TCRdock’s rigid-body parameterisation.
Reimplemented from the Bradley lab’s TCRdock (phbradley/TCRdock, MIT license), commit
c5a7af42eeb0c2a4492a4d4fe803f1f9aafb6193 (2024-03-04), specifically tcrdock/docking_geometry.py,
tcrdock/mhc_util.py, tcrdock/tcr_util.py and tcrdock/superimpose.py. No TCRdock code is imported;
the geometry is ported to tcren’s own Structure and annotation.
The docking geometry describes how the TCR sits on the peptide-MHC groove as a rigid-body transform between two coordinate frames (“stubs”):
the MHC stub — from the ~180° pseudo-symmetry of the class-I α1α2 (or class-II α1β1) β-sheet floor: x points toward the peptide, z from one half of the sheet to the other, origin at the two halves’ midpoint;
the TCR stub — from the ~180° pseudo-symmetry relating Vα and Vβ: x toward the CDR loops, z from Vα to Vβ, origin at the two domains’ midpoint.
Six numbers (DockingGeometry) fix the relative pose: d (frame separation), torsion (dihedral
about the MHC–TCR line), and tcr_unit_y/z + mhc_unit_y/z (each frame’s direction to the other, in the
other’s local axes). For interpretable in-plane / tilt angles use tcren.docking.docking.docking_angles()
(crossing_angle = the groove-plane “scanning” angle; incident_angle = the tilt); this module adds the
full rigid-body pose that those two scalars do not capture.
Class-I core positions are mapped by BLOSUM-aligning the α chain to TCRdock’s class-I template. Class II uses
the same six within-domain strand offsets, mapped through the canonical α1 (MHCa) and β1 (MHCb) sequences in
mhc_canonical.json — the two halves live on separate chains there, but they are the same floor. TCR core
positions are the conserved IMGT framework positions, located from tcren’s arda region markup.
Provenance note (validated on 618 TCRvdb TCRmodel2 models, 2026-07-05): tcren’s crossing_angle reproduces
the “scanning_angle” reported by upstream AF/TCRmodel2 annotation tables (r≈0.88), so that quantity is a
genuine, reproducible interface geometry. The upstream “pitch_angle”, however, matches no clean geometric
angle (best correlate is d, r≈0.42) and discriminates TCRvdb binders better (macro-PR≈0.72) than any
clean docking feature computed here (d≈0.64, torsion≈0.62, tilt≈0.58) — i.e. its extra signal is not
reproducible from coordinates and is likely AlphaFold-confidence contamination. Prefer these documented,
crystal-computable descriptors over the opaque upstream pitch.
- class tcren.docking.tcrdock_geometry.DockingGeometry(d, torsion, tcr_unit_y, tcr_unit_z, mhc_unit_y, mhc_unit_z)[source]#
Bases:
objectTCR:pMHC rigid-body docking geometry — TCRdock’s 6-parameter form.
- Parameters:
d (float)
torsion (float)
tcr_unit_y (float)
tcr_unit_z (float)
mhc_unit_y (float)
mhc_unit_z (float)
- d: float#
- torsion: float#
- tcr_unit_y: float#
- tcr_unit_z: float#
- mhc_unit_y: float#
- mhc_unit_z: float#
- tcren.docking.tcrdock_geometry.docking_geometry(structure)[source]#
Compute the TCRdock docking geometry of a chain-typed, MHC-annotated TCR-pMHC structure.
The structure must already be chain-typed (
tcren.annotation.classify_chains()) and MHC-annotated (tcren.mhc.annotate_mhc()) with arda region markup on the TCR chains.- Parameters:
structure – a chain-typed, annotated TCR-pMHC
Structure.- Returns:
The
DockingGeometry.- Raises:
ValueError – if the MHC β-sheet core or a complete TCR Vα/Vβ core cannot be located.
- Return type:
tcren.docking.frame module#
Canonical TCR-pMHC frame by PCA: z ≈ PC1 (MHC→TCR), y ≈ PC2 (peptide), x ≈ PC3.
Every structure is first superposed onto a per-class native reference by its MHC groove Cα
(tcren.docking.align.align_to_native()); a fixed per-class rotation R_canon then maps
that reference frame into the canonical axes. R_canon is obtained by centring the reference
complex’s Cα cloud at its centre of mass and taking its principal axes (PCA):
z= PC1 (largest variance, the MHC→TCR long axis), signed+ztoward the TCR so the MHC sits at−z;y= PC2 (the groove/peptide axis), signed+ytoward the peptide C-terminus;x= PC3 (the thin axis), signed for a right-handed frame.
R_canon + the variance fractions are cached in the bundled tcren/data/canonical_frame.json
so orientation is reproducible and inspectable. When no native database is available the same
PCA axes are fit directly from the query (the PCA fallback).
- class tcren.docking.frame.CanonResult(rotation, translation, rmsd, n_anchor_atoms, reference_id, frame, reversed_dock=None, chain_map=<factory>)[source]#
Bases:
objectComposed rigid transform that maps a structure into the canonical frame.
- Parameters:
rotation (ndarray)
translation (ndarray)
rmsd (float)
n_anchor_atoms (int)
reference_id (str | None)
frame (Literal['native', 'pca'])
reversed_dock (bool | None)
chain_map (dict[str, str])
- rotation: ndarray#
- translation: ndarray#
- rmsd: float#
- n_anchor_atoms: int#
- reference_id: str | None#
- frame: Literal['native', 'pca']#
- reversed_dock: bool | None#
- chain_map: dict[str, str]#
- tcren.docking.frame.canonical_frame(structure, reference_id=None, force_pca=False)[source]#
Compose the MHC superposition with the per-class
R_canon(native), or fit the canonical axes directly from the query (PCA fallback when no DB / too few anchors).- Parameters:
structure (Structure)
reference_id (str | None)
force_pca (bool)
- Return type:
tcren.docking.align module#
Bring a structure into a canonical reference frame by MHC superposition.
A query complex is oriented onto a native reference by superposing the conserved MHC
groove Cα atoms (the helix/floor residues from tcren.mhc.regions). Because every
structure is aligned to the same reference, all oriented complexes share one frame —
the basis for overlaying structures and for 2D interface projection. Correspondence
between query and reference groove residues is established by sequence alignment, so
different alleles/numbering are handled.
- class tcren.docking.align.OrientationResult(rotation, translation, rmsd, n_anchor_atoms, reference_id)[source]#
Bases:
objectRigid transform that maps a structure onto the canonical reference frame.
- Parameters:
rotation (ndarray)
translation (ndarray)
rmsd (float)
n_anchor_atoms (int)
reference_id (str)
- rotation: ndarray#
- translation: ndarray#
- rmsd: float#
- n_anchor_atoms: int#
- reference_id: str#
- tcren.docking.align.align_to_native(structure, reference_id=None)[source]#
Compute the transform orienting
structureonto a native reference by MHC.structuremust already be chain-typed and MHC-annotated (seetcren.mhc.annotate_mhc()). The reference (default a canonical complex for the structure’s MHC class) is loaded from theNative2026dataset (tcren.paths).- Parameters:
structure (Structure)
reference_id (str | None)
- Return type:
- tcren.docking.align.apply_transform(structure, result)[source]#
Return a copy of
structurewith the orientation transform applied to all atoms.- Parameters:
structure (Structure)
result (OrientationResult)
- Return type:
tcren.docking.superimpose module#
Superimpose query structures onto a canonical database by MHC.
Unlike tcren.docking.run_folder() (which builds a canonical set from native complexes
using the per-class derived frame), superimpose() brings a new structure into the
canonical frame defined by an existing database (data/Canonical2026 by default).
How it works, per query structure:
Chain-type + MHC-annotate the query; read its MHC class (MHCI/MHCII) and species.
Select every database structure with the same class and species (from the database’s
orient_metadata.json).Superpose the query’s conserved groove Cα onto each selected database structure (sequence alignment establishes the residue correspondence, so alleles/numbering differ freely).
Average the resulting rigid transforms — translations by mean, rotations by the chordal (SVD-orthonormalised) mean — into one consensus placement, and apply it.
Because every database member already sits in the same canonical frame, each superposition independently yields a canonical placement; averaging over the whole matching ensemble cancels the per-structure groove variation.
- tcren.docking.superimpose.superimpose(structure, db_dir=None, organism='human', annotate=True)[source]#
Superimpose
structureonto a canonical database by MHC (see module docstring).structureis chain-typed + MHC-annotated here unlessannotate=False(used by the threaded batch driver, which annotates the whole input set in one mmseqs pass first). The ensemble alignment itself is mmseqs-free, so it is the part safe to run on a thread pool.db_dirdefaults todata/Canonical2026. Returns the oriented, A–E renamed structure and the consensusOrientationResult(averaged over the ensemble).- Parameters:
structure (Structure)
db_dir (str | Path | None)
organism (str)
annotate (bool)
- Return type:
tuple[Structure, OrientationResult]
tcren.docking.pipeline module#
Orchestrate canonicalization of TCR-pMHC structures into the common MHC frame.
- tcren.docking.pipeline.canonicalize_structure(structure, reference_id=None, force_pca=False, select_primary=True)[source]#
Orient an (already chain-typed + MHC-annotated) structure into the canonical frame.
Returns the oriented, A–E renamed structure and the populated
CanonResult(transform, frame, rmsd, reverse-dock flag, chain map). Coordinates are transformed; the chain roles drive the rename, so order matters (frame + reverse-dock are read before the transform clears region markup).- Parameters:
structure (Structure)
reference_id (str | None)
force_pca (bool)
select_primary (bool)
- Return type:
tuple[Structure, CanonResult]
- tcren.docking.pipeline.align_to_canonical(structure, reference_id=None, organism='human', force_pca=False)[source]#
Align a NEW (parsed) structure onto the Native2026 canonical frame.
Runs chain typing + MHC annotation, then
canonicalize_structure(). The stored per-classR_canonis reused, so the result is in the same frame as the dataset and the composed transform in the returnedCanonResultreplays the placement exactly.- Parameters:
structure (Structure)
reference_id (str | None)
organism (str)
force_pca (bool)
- Return type:
tuple[Structure, CanonResult]
- tcren.docking.pipeline.check_oriented_complex(structure, max_peptide_len=25, max_offset=25.0, max_tcr_gap=15.0, max_orphan=70.0)[source]#
Geometric sanity check on an oriented A–E complex;
(ok, reason).Rejects structures whose canonical placement is inconsistent: missing / overlong peptide, peptide not at the groove centre (≈ origin), the TCR not engaging the peptide, or any chain stranded far from the complex (an orphan copy that survived primary-complex selection).
- Parameters:
max_peptide_len (int)
max_offset (float)
max_tcr_gap (float)
max_orphan (float)
- tcren.docking.pipeline.run_folder(structures, out, metadata=None, organism='human', reference_id=None, force_pca=False, threads=None, mmcif=False, compress=False)[source]#
Canonicalize a file or folder of structures; write oriented structures + a metadata table.
Output format follows
mmcif(.cifvs.pdb) andcompress(trailing.gz); plain PDB by default (passcompress=Trueto rebuild the gzipped Canonical2026 set).Annotation is BATCHED — one mmseqs search for all TCR chains (per organism) and one for all MHC chains across the whole set (mmseqs parallelises internally; never per-structure, never Python-threaded). Only the embarrassingly-parallel, mmseqs-free stages — parsing and the structural alignment + write — use a thread pool (
threadsworker threads, defaultos.cpu_count()).- Parameters:
structures (str | Path)
out (str | Path)
metadata (str | Path | None)
organism (str)
reference_id (str | None)
force_pca (bool)
threads (int | None)
mmcif (bool)
compress (bool)
- Return type:
DataFrame
- tcren.docking.pipeline.run_superimpose(structures, out, db_dir=None, organism='human', mmcif=False, compress=False, threads=None)[source]#
Superimpose input structure(s) onto a canonical database; write oriented structures.
structuresis a file, directory,.tar.gz, or a shell glob.outis an output directory, or — for a single input — a structure file whose extension must matchmmcif/compress. Annotation is BATCHED (one mmseqs pass over all inputs); only the mmseqs-free ensemble alignment + write runs on the thread pool (threadsworkers, default all cores). Seetcren.docking.superimpose()for the MHC-ensemble method.- Parameters:
structures (str | Path)
out (str | Path)
db_dir (str | Path | None)
organism (str)
mmcif (bool)
compress (bool)
threads (int | None)
- Return type:
DataFrame
tcren.docking.chains module#
Select a single TCR-pMHC complex and rename its chains to the canonical A–E scheme.
- tcren.docking.chains.select_primary_complex(structure)[source]#
Keep one mutually-contacting chain per canonical role (one TCR-pMHC complex).
Chosen as a connected unit so chains that do not touch the peptide (notably β2m) are not grabbed from another copy: the primary peptide is the one most embedded in an MHC-α groove (then most TCR contacts, then shortest); the TCR α/β and MHC-α are taken by contacts to that peptide; β2m / MHC-β by contacts to the chosen MHC-α. No-op for single complexes.
- tcren.docking.chains.rename_chains(structure)[source]#
Return a copy with only the canonical complex, chain ids remapped per
CHAIN_RENAME, plus the old→new map.Chains with no canonical role (tags, additives, unrelated proteins) are dropped so the output is exactly the A–E TCR-pMHC complex. Raises
ValueErrorif two source chains map to the same canonical id (unresolved multi-copy — runselect_primary_complex()first).
tcren.docking.graft module#
Build chimeric TCR:pMHC complexes by grafting one complex’s TCR onto another’s pMHC.
Given a host and a donor TCR:pMHC complex, substitute_tcr() produces a new complex that
keeps the host peptide + MHC and the donor TCR, rigidly positioned by one of two anchors:
by="mhc"— superpose the donor MHC-groove Cα onto the host MHC-groove Cα, then carry the donor TCR into the host frame. The donor TCR keeps its native docking geometry relative to MHC (the pose it adopts on its own pMHC, transferred onto the host groove).by="tcr"— superpose the donor TCR Cα onto the host TCR Cα, then drop the host TCR. The donor TCR inherits the host’s docking pose (it lands where the host TCR sat).
Both yield host pMHC + donor TCR; only the superposition anchor differs. Residue correspondence
between the two MHCs (or the two TCRs) is by sequence alignment, so different alleles / V-genes are
handled. The MHC path needs both inputs MHC-annotated (tcren.mhc.annotate_mhc()); both paths
need chain typing (tcren.annotation.classify_chains()).
- tcren.docking.graft.substitute_tcr(host, donor, *, by='mhc')[source]#
Graft the
donorTCR onto thehostpMHC → a chimeric TCR:pMHCStructure.The result keeps the host peptide + MHC chains and the donor TCR chains (relabelled to avoid id collisions), with the donor TCR rigidly placed by the
byanchor:"mhc"— superpose the donor MHC groove onto the host MHC groove (both inputs must betcren.mhc.annotate_mhc()-annotated); the donor TCR keeps its native docking geometry."tcr"— superpose the donor TCR onto the host TCR; the donor TCR inherits the host pose.
Both inputs must be chain-typed (
tcren.annotation.classify_chains()). RaisesValueErrorifbyis invalid, the host lacks a peptide or MHC chain, the donor lacks a TCR chain, or too few matched Cα anchors are found to superpose.
tcren.docking.exceptions module#
Detect reverse-docked TCR-pMHC complexes (a biological exception, flagged not flipped).
The canonical frame is fixed by peptide polarity (+y = peptide C-terminus). The conserved
diagonal docking then places the VDJ chain (TRB/TRD, Vβ) on the peptide-C side (+y) and the
VJ chain (TRA/TRG, Vα) on the peptide-N side (−y) — consistent with the CDR footprint
CDR1α·CDR2α·CDR3α·CDR3β·CDR2β·CDR1β laid out N→C. A genuinely reverse-docked TCR lands with the
α/β sides mirrored. We report it; we never force-flip, because the orientation is meaningful.
- tcren.docking.exceptions.detect_reverse_dock(structure, rotation, translation, margin=2.0)[source]#
Apply the canonical transform and check the TCR α/β handedness.
Canonical: VDJ (TRB/TRD) at
+y(peptide-C side) and VJ (TRA/TRG) at−y. ReturnsTruewhen the VJ chain is on the+yside of the VDJ chain by more thanmarginÅ (reverse dock),Falsefor a canonical dock, andNonewhen a TCR side is missing.- Parameters:
structure (Structure)
rotation (ndarray)
translation (ndarray)
margin (float)
- Return type:
bool | None
Potentials#
tcren.potential.model module#
Pairwise residue-level statistical potentials.
A Potential is a long-form table of pairwise amino-acid energies keyed on
(residue.aa.from, residue.aa.to). The “from” side is conventionally the TCR
residue and the “to” side the antigen (peptide) residue, matching the orientation of
the legacy R pipeline. Potentials can be loaded from the two CSV layouts shipped with
the project (wide and long) and exported to a dense matrix for fast scoring.
- tcren.potential.model.AA20: tuple[str, ...] = ('L', 'F', 'I', 'M', 'V', 'W', 'Y', 'C', 'H', 'A', 'G', 'P', 'T', 'S', 'Q', 'N', 'D', 'E', 'R', 'K')#
20 standard amino acids (one-letter), TCRen ordering used in the paper.
- tcren.potential.model.AA21: tuple[str, ...] = ('A', 'I', 'L', 'V', 'R', 'H', 'K', 'C', 'M', 'S', 'T', 'D', 'E', 'N', 'Q', 'G', 'P', 'Y', 'F', 'W', '-')#
21 amino acids plus the gap symbol.
- Type:
Alphabet of the alignment-matrix variant
- class tcren.potential.model.PotentialDecomposition(name, mean, one_body, pair, index)[source]#
Bases:
objectA potential split as
e(a, b) = mean + H(a) + H(b) + J(a, b).- Variables:
name (str) – Name of the potential this came from.
mean (float) – The grand mean of the matrix.
one_body (numpy.ndarray) –
H, indexed likeindex; the per-residue part.pair (numpy.ndarray) –
J, double-centred, so every row and column sums to zero.index (dict[str, int]) – Amino-acid → row/column index.
- Parameters:
name (str)
mean (float)
one_body (ndarray)
pair (ndarray)
index (dict[str, int])
- name: str#
- mean: float#
- one_body: ndarray#
- pair: ndarray#
- index: dict[str, int]#
- class tcren.potential.model.HydrophobicityFit(name, c0, c1, c2, q, index, r2, eigenvalue_share)[source]#
Bases:
objectA potential approximated as
C0 + C1 (q_a + q_b) + C2 q_a q_b.- Variables:
name (str) – Name of the potential this came from.
q (numpy.ndarray) – One value per residue, from the leading eigenvector; orders by hydrophobicity.
index (dict[str, int]) – Amino-acid → index into
q.r2 (float) – Fraction of the matrix variance the three-parameter form reproduces.
eigenvalue_share (float) –
|lambda_1| / sum |lambda|, i.e. how nearly rank-one the matrix is to begin with.
- Parameters:
name (str)
c0 (float)
c1 (float)
c2 (float)
q (ndarray)
index (dict[str, int])
r2 (float)
eigenvalue_share (float)
- name: str#
- c0: float#
- c1: float#
- c2: float#
- q: ndarray#
- index: dict[str, int]#
- r2: float#
- class tcren.potential.model.Potential(name, matrix, alphabet)[source]#
Bases:
objectA pairwise amino-acid potential in long form.
- Variables:
name (str) – Identifier of the potential (e.g.
"TCRen","MJ","Keskin").matrix (polars.dataframe.frame.DataFrame) – Long-form table with columns
residue.aa.from,residue.aa.to,value.alphabet (tuple[str, ...]) – Amino-acid symbols present on each axis.
- Parameters:
name (str)
matrix (DataFrame)
alphabet (tuple[str, ...])
- name: str#
- matrix: DataFrame#
- alphabet: tuple[str, ...]#
- value(aa_from, aa_to)[source]#
Return the energy for an ordered residue pair.
- Parameters:
aa_from (str) – One-letter code of the “from” (TCR) residue.
aa_to (str) – One-letter code of the “to” (antigen) residue.
- Returns:
The pairwise energy.
- Raises:
KeyError – If the pair is absent from the potential.
- Return type:
float
- scale()[source]#
The potential’s own energy scale: the standard deviation over its defined pairs.
Two potentials Boltzmann-inverted from different contact statistics are not on a common scale, so summing energies read off them weights whichever has the wider matrix. Measured over the shipped matrices: TCRen2 0.4880, MJ 0.3270, Keskin 1.3181 – so an unweighted \(\Phi_{\mathrm{TCR:pep}} + \Phi_{\mathrm{TCR:MHC}} + \Phi_{\mathrm{pep:MHC}}\) is 2.70x more sensitive to a presentation contact than to a recognition one when the presentation interfaces are scored with Keskin.
Dividing each interface energy by its potential’s scale makes the three terms commensurate. The coefficient is a property of the matrix alone – no cohort, no label, no fit.
Diagonal and off-diagonal entries are pooled and each unordered pair counted once, since the matrix is symmetric in use.
- Return type:
float
- offset()[source]#
The potential’s mean over its defined pairs (see
scale()).An additive offset multiplied by a contact count is a contact count, not an energy, so a potential with a large one – Keskin’s mean is -3.5630 and every entry is negative – makes its interface energy read mostly as interface size. Subtracting it leaves the identity preference, which is what the other channels do not already carry.
- Return type:
float
- as_matrix()[source]#
Return a dense
(n, n)matrix and an amino-acid → index map.Rows are indexed by
residue.aa.from, columns byresidue.aa.to. Missing pairs are filled withnan. The dense form is cached (the table is immutable), so repeated scoring/energy calls over one potential rebuild it once. Callers treat the returned array as read-only.- Return type:
tuple[ndarray, dict[str, int]]
- decompose()[source]#
Split the potential into a one-body part and a genuinely pairwise part.
A contact energy is not purely an interaction. Burying any residue against any partner costs or gains something that depends only on that residue – its transfer propensity – and only what is left after removing those one-body terms is an interaction between the two identities. Miyazawa and Jernigan make this split explicitly; here it is taken directly off the matrix, which needs no solvent reference and works for any potential:
e(a, b) = mean + H(a) + H(b) + J(a, b)
with
H(a)the row mean ofaless the grand mean, andJthe double-centred remainder, whose every row and column sums to zero. The split is exact and unique.Why it matters for scoring: an additive per-position model can already absorb
meanand bothHterms, because they depend on one residue each.Jis the only part that cannot be written as a sum over positions, so it is the only part a per-position model is actually missing.- Returns:
- Raises:
ValueError – If the dense matrix is not square and symmetric, since the split is only defined for an undirected potential (TCRen is directed and must not be decomposed this way).
- Return type:
- components()[source]#
The three additive parts of
decompose(), each as a scorablePotential.decompose()splits the matrix ase(a,b) = mean + H(a) + H(b) + J(a,b). Because an interface score is a sum over contacts, that split carries straight through to the score:component
matrix
what its interface sum equals
"size"the grand mean everywhere
mean x (number of contacts)"comp"H(a) + H(b)a degree-weighted composition term
"pair"J(a, b)the interaction, one-body parts gone
So scoring a structure with each part in turn says which of three very different things a potential is reading on that interface: how big it is, what it is made of, or which residue faces which. That distinction is not cosmetic – a matrix with no positive entries has a large negative mean, so its interface sum is dominated by the contact count, and a result obtained with one can be an interface-area effect wearing a chemical name. The three parts sum back to the original exactly, which the unit tests assert.
- Returns:
{"size": ..., "comp": ..., "pair": ...}, each named<this potential>_<part>.- Raises:
ValueError – If the potential is not symmetric (see
decompose()).- Return type:
dict[str, Potential]
- hydrophobicity_fit()[source]#
Fit
e(a,b) = C0 + C1 (q_a + q_b) + C2 q_a q_b– one number per residue.Where the one-body term comes from, for a matrix that does not ship one. Miyazawa and Jernigan derive their own one-body terms from residue–solvent contact energies, which the bundled matrices do not carry, so that route is unavailable here. Li, Tang and Wingreen showed it is not needed: the MJ matrix is dominated by a single eigenvalue, and reconstructing it from the leading eigenvector
qgives the form above, withqordering the residues by hydrophobicity.The consequence is worth stating plainly, because it limits what any MJ-based score can express. Not only is the one-body part a function of
q; so is the interaction, which is justC2 q_a q_b. A potential of that shape knows how hydrophobic each residue is and nothing else – it cannot prefer one specific pair of side chains over another pair of equal hydrophobicity.Reference: Li H, Tang C, Wingreen NS. Nature of driving force for protein folding: a result from analyzing the statistical potential. Phys Rev Lett. 1997;79:765. arXiv:cond-mat/9512111.
- Returns:
A
HydrophobicityFit. Check itsr2before relying on it; the form is an approximation, not an identity, unlikedecompose().- Raises:
ValueError – If the matrix is not square and symmetric.
- Return type:
- to_csv(path)[source]#
Write the potential to a long-form CSV (
from, to, value).- Parameters:
path (str | Path)
- Return type:
None
- classmethod from_csv(path, name=None, value_col=None)[source]#
Load a potential from a CSV, auto-detecting wide vs long layout.
Two layouts are supported:
wide —
residue.aa.from, residue.aa.to, <name>(e.g.TCRen_potential.csvwith aTCRenvalue column).long —
residue.aa.from, residue.aa.to, potential, value(e.g.MJ_Keskin_potentials.csv); load a single named potential from it.
- Parameters:
path (str | Path) – Path to the CSV file.
name (str | None) – Which potential to select (long layout) or the name to assign (wide layout). Defaults to the value-column name (wide) and is required when a long file holds more than one potential.
value_col (str | None) – Override the value column name for the wide layout.
- Returns:
The loaded
Potential.- Return type:
- tcren.potential.model.tcren()[source]#
Not part of the TCRen2 derivation
The 2022 matrix, kept for reproducing published results. TCRen2 is tcren.potential.tcren2() and is the default since 2.11.0; the two correlate at r = 0.867 with a maximum absolute difference of 0.943 and are not interchangeable.
The shipped matrix comes from
tcren derive-potential --structure-dir Native2026 --balance bothand nothing else.Load the bundled classic TCRen potential (cached; treat as read-only).
- Return type:
- tcren.potential.model.tcren2()[source]#
Load the bundled TCRen2 potential (cached; treat as read-only).
The redundancy-balanced derivation over the 362 fully annotated αβ
Native2026crystals, the default TCR:peptide potential since 2.11.0, and the matrix the TCRen2 manuscript reports. It is not interchangeable withtcren(): the two correlate at Pearson r = 0.867 with a maximum absolute difference of 0.943 over a range of 2.95, so scores computed under one cannot be compared with scores under the other.- Return type:
- tcren.potential.model.mj()[source]#
Load the bundled Miyazawa–Jernigan potential (cached; treat as read-only).
Identified 2026-08-29: this is AAindex3 ``MIYS990106``, Miyazawa & Jernigan 1999 – not 1985 and not 1996, which is what the “upstream table not recorded” warning here used to say. All 400 cells match the AAindex record exactly (
identify(mj())returns("MIYS990106", 0.0)), and the next-closest entry in the whole of AAindex3 is off by 0.65, so the identification is unique. Every score in the package is built on this file and it is left byte-for-byte untouched; what changed is that it can now be cited.It takes both signs with a mean of −0.079, so it is a contact-pair matrix with the one-body transfer term removed;
mj1996()andkeskin()are raw contact energies (mean ≈ −3.3) andbetancourt()is the other pair-form matrix. Compare like with like, and seePotential.components()for why the distinction changes what a comparison measures.Reference: Miyazawa S, Jernigan RL. Self-consistent estimation of inter-residue protein contact energies based on an equilibrium mixture approximation of residues. Proteins. 1999;34(1):49-68. doi:10.1002/(SICI)1097-0134(19990101)34:1<49::AID-PROT5>3.0.CO;2-L.
- Return type:
- tcren.potential.model.mj1996()[source]#
Miyazawa–Jernigan 1996 inter-residue contact energies,
e_ij, in RT units.The 20x20 attractive contact energies of Table 3, re-evaluated by the authors on 1168 structures. Every entry is negative, from
-7.37(Leu–Leu) to-0.12, and Ala–Ala is-2.72; a raw contact matrix looks like this, and the bundledmj()matrix does not, which is how the two are told apart.Provenance is recorded because the older bundled matrix has none: the numbers here were transcribed from a published copy of Table 3 (AAindex accession MIYS960101) and checked against a second independent copy, agreeing on the alphabet order
CMFILVWYAGTSNQDEHRKP, on Ala–Ala, and on the full range. They correlate with the bundledMJmatrix atr = 0.89, so the two are related but not the same quantity, and the bundled one is not the double-centred pair part of this one (r = 0.51). What the bundled matrix actually is remains unresolved.The companion repulsive packing-density term of the same paper is not included; it is a function of coordination number rather than of a residue pair, so it does not fit the
Potentialshape and nothing here uses it.Reference: Miyazawa S, Jernigan RL. Residue-residue potentials with a favorable contact pair term and an unfavorable high packing density term, for simulation and threading. J Mol Biol. 1996;256(3):623-644. doi:10.1006/jmbi.1996.0114.
- Return type:
- tcren.potential.model.mj_partition_energy()[source]#
Miyazawa–Jernigan effective partition energies, one value per residue.
The one-body term of the MJ framework: the energy of transferring a residue from water into the protein interior, which is what a contact energy carries in addition to any interaction between two identities. A pairwise matrix cannot supply this on its own, so it is bundled separately rather than derived.
Larger is more hydrophobic: Phe 4.37, Met 4.22, Ile 4.17 at one end, Lys 1.23, Asp 1.67, Asn 1.70 at the other. Note the sign convention is opposite to a contact energy, where lower is more favourable.
Provenance: AAindex accession MIYS850101, retrieved from two endpoints of the AAindex database that returned identical values. As an independent check, this scale correlates at
r = +0.98with the hydrophobicity axis recovered byPotential.hydrophobicity_fit()frommj1996(), which was transcribed from a different source entirely.Reference: Miyazawa S, Jernigan RL. Estimation of effective interresidue contact energies from protein crystal structures: quasi-chemical approximation. Macromolecules. 1985;18:534-552.
- Returns:
Amino acid one-letter code → partition energy. The mapping is cached; copy it before mutating.
- Return type:
dict[str, float]
- tcren.potential.model.keskin()[source]#
Load the bundled Keskin contact potential (cached; treat as read-only).
Identified 2026-08-29 as AAindex3 ``KESO980101``, “Quasichemical transfer energy derived from interfacial regions”, matching all 400 cells exactly with the next-closest AAindex3 entry off by 2.77. That is the solvent-mediated form; the companion
KESO980102is the residue-mediated one, also available throughaaindex().Every entry is negative, from
-7.23to-0.50, so this is a raw contact matrix in the same reference state asmj1996()and not in the pair-contact reference state of the bundledmj()(mixed sign, mean-0.08). Compare it againstmj1996; comparing it againstmjcompares two different reference states as well as two different derivations.Reference: Keskin O, Bahar I, Badretdinov AY, Ptitsyn OB, Jernigan RL. Empirical solvent-mediated potentials hold for both intra-molecular and inter-molecular inter-residue interactions. Protein Sci. 1998;7(12):2578-2586. doi:10.1002/pro.5560071211.
- Return type:
- tcren.potential.model.betancourt()[source]#
Betancourt–Thirumalai contact energies, the
Bmatrix, in RT units.Miyazawa–Jernigan re-referenced with Thr as the reference solvent, which is why every Thr entry is exactly
0.00; the remaining 190 cross terms and 19 self terms run-1.34(Cys–Cys) to+0.66. Mixed sign with a mean near zero, so it is a pair-contact matrix in the same reference state as the bundledmj(), and that is the matrix to compare it against. The authors report it gives “hydrophobicities that are in very good agreement with experiment”, and it is the potential Schueler-Furman et al. found generalises an MJ-based peptide–MHC groove score across alleles where MJ itself worked only for hydrophobic-pocket alleles.Provenance: parsed from AAindex3 accession
BETM990101(“Modified version of the Miyazawa-Jernigan transfer energy”), lower-triangular overARNDCQEGHILKMFPSTWYV, never retyped. Three properties are asserted at build time: the Thr row is zero, the matrix is symmetric, and all 400 cells are present.Reference: Betancourt MR, Thirumalai D. Pair potentials for protein folding: choice of reference states and sensitivity of predicted native states to variations in the interaction schemes. Protein Sci. 1999;8(2):361-369. doi:10.1110/ps.8.2.361.
- Return type:
tcren.potential.derive module#
Derivation of the TCRen statistical potential from observed contact maps.
This is a direct port of the R derivations in code_paper/2_TCRen_derivation.Rmd
(variant="classic") and tcren_am/tcren_am.Rmd (variant="am"). The classic
variant reproduces TCRen_potential.csv; the alignment-matrix variant reproduces
tcren_am/tcren.txt.
- tcren.potential.derive.symmetrize_counts(counts)[source]#
Fold a directed aa-pair count table onto its transpose:
N + Nᵀ.TCRen counts are directed —
fromis a TCR residue andtoa peptide residue — soN[a,b]andN[b,a]are different observations and the derived matrix is asymmetric. Adding the transpose treats each contact as an unordered pair, which is the convention Miyazawa–Jernigan uses. Diagonal cells double, as they must: a C–C contact is one unordered pair observed from both sides.Symmetrising here — on the raw counts, before the log-odds — is not the same as averaging the finished potential. The marginals (
total.from/total.to) are recomputed from the folded counts, so the expected term of the log-odds changes too; averaging the energies afterwards leaves the asymmetric background in place. On the Native2026 derivation set the two disagree by 0.29 on average (max 0.82), so the distinction is not cosmetic.Cysteine. The classic directed derivation drops
from == "C"because free Cys is essentially absent from CDR loops — on Native2026 only 4 of 8062 contacts (0.05 %) have a TCR-side Cys, against 32 (0.40 %) on the peptide side. Folding grafts those peptide-side observations onto the Cys row instead of discarding the column, so the symmetric matrix keeps a full 20×20 alphabet at no cost: the row that would have been dropped for having no data inherits the data the other axis did have.- Parameters:
counts (DataFrame) – Long table with
residue.aa.from,residue.aa.toandcount.- Returns:
The folded table, with one row per unordered pair-cell (still stored in both orientations, so it is a full symmetric matrix).
- Return type:
DataFrame
Example
>>> import polars as pl >>> c = pl.DataFrame({"residue.aa.from": ["A"], "residue.aa.to": ["W"], "count": [3.0]}) >>> out = symmetrize_counts(c) >>> sorted((r["residue.aa.from"], r["residue.aa.to"], r["count"]) for r in out.iter_rows(named=True)) [('A', 'W', 3.0), ('W', 'A', 3.0)]
- tcren.potential.derive.derive_tcren(contacts, include=None, exclude=None, pseudocount=1, variant='classic', beta=44.0, drop_cys=None, weights=None, weight_col=None, symmetric=False)[source]#
Derive a TCRen potential from a table of residue contacts.
- Parameters:
contacts (DataFrame) – Long table of TCR↔peptide contacts with at least
residue.aa.from,residue.aa.toand (for filtering)pdb.id.include (list[str] | None) – If given, keep only contacts whose
pdb.idis in this list.exclude (list[str] | None) – If given, drop contacts whose
pdb.idis in this list.pseudocount (int) – Added to every amino-acid pair count (default 1).
variant (str) –
"classic"(natural-log log-odds over 20 aa, Cys dropped from the “from” axis) or"am"(log2/betaover 21 symbols including a gap, Cys retained).beta (float) – Temperature divisor used by the
"am"variant.drop_cys (bool | None) – Override the per-variant default for dropping
from == "C"rows. Forced toFalsewhensymmetricis set (dropping one axis would un-symmetrise the result).weights (dict[str, float] | None) – Optional per-structure weights
{pdb.id: weight}. When given, each structure’s contributions to the aa-pair counts are multiplied by its weight (rows whosepdb.idis absent from the map default to weight1.0); this down-weights redundancy while keeping all data (seetcren.potential.redundancy.cluster_weights()).None(default) is unweighted and byte-identical to the legacy derivation.weight_col (str | None) – Name of a per-contact weight column in
contacts, multiplied with the per-structureweights. This is how a contact is down-weighted rather than dropped: excluding backbone-only pairs outright removes 46 % of the observations and empties 69 of 380 cells, whereas giving them a fractional vote keeps every cell populated. A contact whose two residues are merely co-located, and will only sample an interacting geometry some of the time, is exactly a fractional observation.symmetric (bool) – Fold the raw counts onto their transpose (
symmetrize_counts()) before the log-odds, yielding a symmetricvalue[a,b] == value[b,a]potential over an unordered amino-acid pair — the same convention as the bundled Miyazawa–Jernigan matrix, and therefore directly comparable to it. DefaultFalsekeeps the directed TCR→peptide potential, which is the shippedTCRen_potential.csv.smooth_beta – Substitution-matrix pseudocount weight (
tcren.potential.smoothing.smooth_counts()), applied to the pair counts before the log-odds. A cell holdingsmooth_betaobservations is pulled halfway to the prior its chemically similar cells imply; a well-observed cell is left alone. This is aimed at the rare residues – tryptophan, cysteine, methionine – whose cells are otherwise set by the flat pseudocount.0.0(default) is off and byte-identical to the unsmoothed derivation.smooth_matrix – Substitution matrix behind that prior, and behind the imputation.
impute_min_count – Rebuild cells holding fewer than this many observations from their nearest substitutable neighbours (
tcren.potential.smoothing.impute_thin_cells()), leaving every other cell untouched.0disables it. Applied aftersmooth_betawhen both are given, so the imputation sees the smoothed counts. NOT USED FOR TCRen2.impute_donors – How many nearest donor cells that imputation averages over.
prior – A
(20, 20)array summing to 1 that redistributes thepseudocountmass over the grid instead of spreading it uniformly – seetcren.potential.smoothing.composition_prior(). The total mass added is unchanged, sopseudocountmeans the same thing either way.Nonekeeps the flat prior. NOT USED FOR TCRen2.
- Returns:
The derived
Potential. For"am"the long matrix additionally carries acountcolumn.- Return type:
- tcren.potential.derive.derive_tcren_loo(contacts, pdb_ids, **kwargs)[source]#
Not part of the TCRen2 derivation
Leave-one-out derivation, for testing how much any single structure moves the matrix. Diagnostic, not a production path.
The shipped matrix comes from
tcren derive-potential --structure-dir Native2026 --balance bothand nothing else.Leave-one-out TCRen: derive once per structure, excluding it each time.
- Parameters:
contacts (DataFrame) – Contact table (see
derive_tcren()).pdb_ids (list[str]) – Structures to leave out one at a time (also the inclusion set).
kwargs – Forwarded to
derive_tcren().
- Returns:
Long table
residue.aa.from, residue.aa.to, TCRen.LOO, pdb.idstacking the per-structure potentials.- Return type:
DataFrame
- tcren.potential.derive.derive_tcren_by_type(contacts, *, min_count=30, **kwargs)[source]#
Derive one potential per contact type, plus the occupancy report that says whether to trust it.
The review’s suggestion: a contact potential scores a residue pair by identity alone, so it gives the same energy to a Lys–Asp salt bridge and a Lys–Asp pair that merely drifts within 5 Å. Conditioning the counts on
tcren.contact_typesseparates them. The review also names the risk, and it is the real one: splitting a fixed set of contacts across eight types multiplies the sparsity of a 20×20 matrix by eight.So this returns the report alongside the potentials rather than only the matrices. Read the report first:
n_contactsper type, andfrac_cells_ge_min— the share of the 400 cells that reachmin_countobservations. A type where that is near zero has a matrix made mostly of pseudocount, whatever its numbers look like.Measured on Canonical2026 (8002 typed TCR:peptide contacts, 370 structures): the concern is the correct one. No type reaches 5% cell occupancy at
min_count=30.polar, the largest bucket at 3221 contacts, populates 4.75% of cells with a median of 6.5 observations each;salt_bridge(136 contacts) reaches 11 cells of 400;stacking13. Correlation with the pooled matrix tracks the count and nothing else — polar 0.57, hydrophobic 0.28, cation_pi 0.03 — which is what noise looks like, not distinct chemistry. (The pipeline itself is fine: the pooled re-derivation reproduces the shippedTCRen_potential.csvat r = +0.85.)On a set this size, use the type to filter contacts instead (
tcren.contact_types.type_weights()); this function is here so the decision can be re-taken against a larger set rather than argued about.- Parameters:
contacts (DataFrame) – a contact table carrying
contact.type— fromtcren.paper.helpers.contact_table()withcontact_types=True, ortcren.contact_types.residue_pair_types().min_count (int) – observations a cell needs before it counts as populated in the report.
**kwargs – passed through to
derive_tcren().
- Returns:
(potentials, report)— a{contact_type: Potential}mapping and a polars frame withcontact.type,n_contacts,n_cells_observed,frac_cells_ge_min,median_count.- Raises:
ValueError – if
contactshas nocontact.typecolumn.- Return type:
tuple[dict[str, Potential], DataFrame]
tcren.potential.redundancy module#
Non-redundancy clustering for TCRen derivation inputs.
The TCRen potential is derived from a non-redundant set of αβ TCR–pMHC structures: near-duplicate complexes (same/similar CDR3α + CDR3β + peptide) are collapsed to a single representative so the contact statistics are not dominated by repeated entries.
This is a verbatim lift of the nonredundant/alphabeta helpers from
notebooks/natcompsci2022/01_nonred_and_derivation.ipynb: a complete-linkage
hierarchical clustering on the summed Damerau–Levenshtein distance of the
CDR3α+CDR3β+peptide strings, cut at distance t (default 6.0), keeping one
representative per cluster.
- tcren.potential.redundancy.nonredundant_ids(markup, t=6.0, fields=('cdr3a', 'cdr3b', 'peptide'), linkage_method='complete')[source]#
Not part of the TCRen2 derivation
Excludes cluster members outright. TCRen2 down-weights instead, which keeps every structure’s data.
The shipped matrix comes from
tcren derive-potential --structure-dir Native2026 --balance bothand nothing else.Non-redundant representative
pdb.idfor each cluster of similar complexes.Clusters structures by the summed Damerau–Levenshtein distance over
fields(CDR3α + CDR3β + peptide) using hierarchical clustering, then returns one representativepdb.idper cluster (the first in row order).- Parameters:
markup (DataFrame) – Per-structure table with a
pdb.idcolumn and thefieldscolumns.t (float | None) – Distance cutoff for
fcluster(criterion="distance");Noneturns redundancy filtering off and returns everypdb.idunchanged.fields (Sequence[str]) – Sequence columns whose per-pair distances are summed (default
cdr3a,cdr3b,peptide).linkage_method (str) – Linkage method for
scipy.cluster.hierarchy.linkage(default"complete").
- Returns:
Representative
pdb.idvalues, one per cluster, in first-seen order.- Return type:
list[str]
- tcren.potential.redundancy.cluster_weights(markup, t=6.0, fields=('cdr3a', 'cdr3b', 'peptide'), linkage_method='complete')[source]#
Not part of the TCRen2 derivation
Down-weights by sequence-distance clusters rather than exact identity. Needs a threshold, and conflates the epitope and receptor axes that –balance separates.
The shipped matrix comes from
tcren derive-potential --structure-dir Native2026 --balance bothand nothing else.Inverse-cluster-size (Henikoff-style) weight for each
pdb.id.Clusters structures exactly as
nonredundant_ids()(same distance, same linkage), then assigns every structure the weight1 / cluster_size. A unique structure gets weight1.0; each member of a size-kredundancy cluster gets1/k, so the cluster contributes a total weight of1to the derivation. Feed the result totcren.potential.derive.derive_tcren()’sweightsargument to down-weight redundancy while keeping every structure’s data.- Parameters:
markup (DataFrame) – Per-structure table with a
pdb.idcolumn and thefieldscolumns.t (float) – Distance cutoff for
fcluster(criterion="distance").fields (Sequence[str]) – Sequence columns whose per-pair distances are summed.
linkage_method (str) – Linkage method for
scipy.cluster.hierarchy.linkage.
- Returns:
Mapping
{pdb.id: 1 / cluster_size}.- Return type:
dict[str, float]
- tcren.potential.redundancy.balanced_weights(markup, axes=(('peptide',), ('cdr3a', 'cdr3b')))[source]#
Henikoff-style weight balancing several redundancy axes at once.
The PDB is biased on more than one axis: some epitopes are crystallized many times and so are some receptors, and correcting only one leaves the other. For structure
iwithn_a(i)structures sharing its value on axisa,\[w_i = \frac{1}{|A|} \sum_{a \in A} \frac{1}{n_a(i)}\]the mean of the per-axis inverse counts. The mean, rather than the product, is what keeps novelty on either axis: a previously unseen receptor against a nine-times crystallized epitope scores
(1/9 + 1/1)/2 = 0.556, not1/9, because it is a genuinely new recognition event. A structure unique on every axis gets1.0; a true re-solve, duplicated on all of them, gets1/n.With a single axis this reduces exactly to inverse frequency on that axis, which is what
epitope_weights()is. Overall scale cancels in the log-odds derivation, so the result needs no normalization.Complementary to
cluster_weights(), which clusters the joint object by sequence distance and so also catches near-duplicates such as point mutants, at the cost of a threshold and of conflating the axes.- Parameters:
markup (DataFrame) – Per-structure table with
pdb.idand every column named inaxes.axes (Sequence[Sequence[str]]) – One tuple of column names per axis; the columns in a tuple are matched jointly, so
("cdr3a", "cdr3b")keys on the receptor as a whole.
- Returns:
Mapping
{pdb.id: weight}over structures with no null on any axis.- Return type:
dict[str, float]
- tcren.potential.redundancy.epitope_weights(markup, field='peptide')[source]#
Not part of the TCRen2 derivation
TCRen2 balances the epitope AND receptor axes; this single-axis alias is what the manuscript’s earlier matrix used. Receptor redundancy is the larger of the two on Native2026 (223 structures share a receptor against 212 an epitope).
The shipped matrix comes from
tcren derive-potential --structure-dir Native2026 --balance bothand nothing else.One-epitope-one-vote weight for each
pdb.id.Every structure carrying a given peptide gets weight
1 / n, wherenis the number of structures in the set with that peptide, so each distinct epitope contributes a total weight of1to the derivation however often it was crystallized. A peptide seen once gets weight1.0.The single-axis case of
balanced_weights(). It corrects epitope bias only; inNative2026receptor redundancy is comparable (226 distinct receptors against 230 distinct epitopes over 374 structures), so consider balancing both axes.- Parameters:
markup (DataFrame) – Per-structure table with
pdb.idandfieldcolumns.field (str) – Column holding the peptide sequence.
- Returns:
Mapping
{pdb.id: 1 / n_structures_sharing_that_peptide}.- Return type:
dict[str, float]
- tcren.potential.redundancy.alphabeta_ids(contacts)[source]#
pdb.idof complexes whose receptor contacts are exclusively TRA/TRB (αβ TCRs).- Parameters:
contacts (DataFrame) – Contact table with
pdb.idandchain.type.fromcolumns.- Returns:
The
pdb.idvalues whose set ofchain.type.fromis a subset of{"TRA", "TRB"}(i.e. no γδ chains).- Return type:
list[str]
tcren.potential.aaindex module#
Every published residue–residue contact matrix in AAindex3, as Potential objects.
AAindex3 is GenomeNet’s section of statistical protein contact potentials: 47 matrices over the
20 amino acids, each transcribed by its curators from a published table. The whole flat file is
bundled (data/aaindex3.txt, 80 kB) rather than a hand-picked subset converted to our own
format, for three reasons: the provenance is then the upstream record itself, a reader can diff the
bundled file against a fresh download, and adding a matrix to a comparison costs a string rather
than a transcription.
Two of tcren’s own bundled potentials were identified against this file rather than guessed:
mj() is MIYS990106 and keskin() is KESO980101, both matching 400 of 400 cells
exactly (see identify()). The MJ one had carried an “upstream table unknown” warning since
2026-08-11 and is Miyazawa–Jernigan 1999, not 1985 and not 1996.
Not every entry is a pairwise energy. catalogue() reports kind for each:
energyA pairwise contact energy. The 42 entries a scoring pipeline can use.
countObserved contact counts, not energies (
TANS760102,MIYS960103).distanceSide-chain centre separations in angstroms (
BONM030104–BONM030106).
and symmetric for whether the matrix equals its transpose; the six ZHAC* entries are
environment-dependent (row secondary structure vs column secondary structure) and three of those
are asymmetric by construction, so they are directed potentials and must not be decomposed.
Example
>>> from tcren.potential import aaindex, catalogue
>>> catalogue().filter(pl.col("kind") == "energy").height
42
>>> aaindex("MOOG990101").name
'MOOG990101'
- tcren.potential.aaindex.NON_ENERGY: dict[str, str] = {'BONM030104': 'distance', 'BONM030105': 'distance', 'BONM030106': 'distance', 'MIYS960103': 'count', 'TANS760102': 'count'}#
Entries whose cells are not an energy, so a scoring call must not reach for them.
- class tcren.potential.aaindex.AAindexEntry(accession, description, authors, title, journal, pmid, rows, cols, matrix, kind)[source]#
Bases:
objectOne parsed AAindex3 record.
- Variables:
accession (str) – The
Hfield, e.g."MIYS990106".description (str) – The
Dfield, one line of prose.authors (str) – The
Afield, verbatim.title (str) – The
Tfield, verbatim (AAindex truncates long titles).journal (str) – The
Jfield, verbatim.pmid (str) – The
Rfield with thePMID:prefix stripped, or"". AAindex sometimes cites the paper that tabulated a matrix rather than the one that derived it –MIYS850102carries Bastolla 2001 – so verify before citing.rows (str) – Amino-acid symbols down the rows.
cols (str) – Amino-acid symbols across the columns.
matrix (numpy.ndarray) – Dense
(20, 20)array, missing cellsnan.kind (str) –
"energy","count"or"distance".
- Parameters:
accession (str)
description (str)
authors (str)
title (str)
journal (str)
pmid (str)
rows (str)
cols (str)
matrix (ndarray)
kind (str)
- accession: str#
- description: str#
- authors: str#
- title: str#
- journal: str#
- pmid: str#
- rows: str#
- cols: str#
- matrix: ndarray#
- kind: str#
- property symmetric: bool#
Whether the matrix equals its transpose (so the one-body split is defined).
- property n_missing: int#
Cells AAindex records as
-orNA; four entries have 39 of them.
- tcren.potential.aaindex.parse_aaindex3(text)[source]#
Parse an AAindex3 flat file into one
AAindexEntryper accession.The
Mblock is lower-triangular including the diagonal for a symmetric entry and a full rectangle for a directed one; both forms appear in the file and both are handled. A cell of-orNAbecomesnanrather than an error, because four entries genuinely omit one residue’s whole row and column.- Parameters:
text (str) – The contents of an
aaindex3flat file.- Returns:
Accession -> entry.
- Raises:
ValueError – If a record’s
Mblock matches neither shape, which would mean the upstream format changed and silently mis-parsing it would corrupt every downstream score.- Return type:
dict[str, AAindexEntry]
- tcren.potential.aaindex.aaindex(accession)[source]#
Load one AAindex3 matrix as a
Potential.- Parameters:
accession (str) – e.g.
"MOOG990101". Case-insensitive.- Returns:
The potential, named for the accession.
- Raises:
KeyError – If the accession is not in AAindex3.
ValueError – If the entry is a contact count or a distance table, which are in the file but are not energies – scoring a contact map with one is a silent category error, so it is refused rather than allowed through.
- Return type:
- tcren.potential.aaindex.entry(accession)[source]#
The parsed record itself, including the non-energy tables
aaindex()refuses.- Parameters:
accession (str)
- Return type:
- tcren.potential.aaindex.catalogue()[source]#
Every bundled AAindex3 entry, one row each, with what a caller needs to choose between them.
Columns:
accession,kind,symmetric,n_missing,mean,min,max,description,authors,journal,pmid.meanis the column to read for reference state: a matrix with mean near zero is a pair-contact form with the one-body transfer term removed, one with a large negative mean is a raw contact energy that still carries it. Comparing across the two answers a different question from comparing within (seePotential.components()).- Return type:
DataFrame
- tcren.potential.aaindex.identify(potential, tol=1e-09)[source]#
Which AAindex3 entries a potential matches, best first, as
(accession, max |delta|).Written for a matrix whose upstream table was never recorded: run it and the answer is either an exact match or a shortlist.
mj()andkeskin()were identified this way, at max|delta| = 0over all 400 cells, with every other candidate off by at least 0.66.- Parameters:
potential (Potential) – The matrix to identify.
tol (float) – Report matches at or below this max absolute difference first; everything else follows in order, so a near-miss is visible rather than silently dropped.
- Returns:
(accession, max_abs_delta)for every entry sharing the alphabet, ascending by delta.- Return type:
list[tuple[str, float]]
The score set#
The five read-outs and what each answers are in Assessing a modelled complex; the machinery behind them is here.
tcren.score.transform module#
Descriptor -> a coordinate a Gaussian can live on.
Every descriptor gets ONE transform, chosen from its unit and its operator, and the choice is
monotone and variance-stabilising throughout. This is the first stage of tcren.score: the
descriptors tcren.recognition.recognition_table() emits live on nine different kinds of
scale – bounded fractions, Poisson tallies, angles, energies in kT – and a Gaussian cannot be
fitted across them until each is mapped to something with a comparable spread.
Not a rank or quantile transform, and that is measured rather than argued. Mapping each marginal onto a uniform through the hold-out binder CDF took the per-cohort median ROC-AUC from 0.630 to 0.543 on the six templated cohorts and from 0.613 to 0.507 on the sixteen non-templated ones. The reason is structural: the signal this pass is built on is that binders occupy a NARROW range on certain axes while non-binders scatter, and flattening a marginal to uniform deletes exactly that. Anything that equalises spread is the wrong tool here.
The unit string (DETAIL[name][0]) is the primary selector; OPERATOR overrides it where
the unit is too coarse. Two cases where it is:
ratio holds six Pearson correlations on [-1,1] beside four unbounded means of products, and
count holds true Poisson tallies beside Hill numbers, which are exp(entropy) and want a log.
- tcren.score.transform.BANNED = ('pitch',)#
pitch reads the generator’s confidence rather than the interface and is banned as a feature by the library itself. The five involves_tcr = False columns are constant within an epitope x allele cohort, so a receptor-ranking model reading one reaches the cohort label without reading an interface – excluded for the receptor tasks, restored for CPL where the peptide is what varies and Phi_pep_mhc is a legitimate signal.
- tcren.score.transform.DETERMINED = ('fp_chi_r7', 'fp_chi_r8', 'D1_cell', 'J_cell', 'offset', 'n_loop_contacts', 'neg_energy', 'S_tot', 'aniso', 'couple_total', 'crossing', 'sc_gap_index', 'dPhi_tcr_soft', 'n_contacts_tm', 'n_contacts_tp')#
Exactly determined by other emitted columns, so each contributes a null direction to any covariance built over them. Every entry is also flagged in
tcren.recognition.STATUS, and all fifteen were verified over 21,939 corpus structures at max relative residual 1.7e-11. With them removed the catalogue is FULL RANK – the list is closed, not partial.
- tcren.score.transform.kind(name)[source]#
The transform class for one descriptor.
- Parameters:
name (str)
- Return type:
str
- class tcren.score.transform.Transformer(names, lam=<factory>, loc=<factory>, scale=<factory>)[source]#
Bases:
objectFit the per-column parameters on a reference set, then apply anywhere.
Only
yeocarries a fitted parameter (the Yeo-Johnson lambda, and the mean/sd after it); every other class is a fixed function. Yeo-Johnson rather than Box-Cox because sc_gap_mean (median -1.7 A over 60 crystals), shift_u, shift_w and m_face_* are legitimately negative and Box-Cox is undefined there.- Parameters:
names (list[str])
lam (dict[str, float])
loc (dict[str, float])
scale (dict[str, float])
- names: list[str]#
- lam: dict[str, float]#
- loc: dict[str, float]#
- scale: dict[str, float]#
- out_names(names=None)[source]#
Coordinate names, one per descriptor except a circular one, which yields cos and sin.
- Parameters:
names (list[str] | None)
- Return type:
list[str]
- transform_names(names=None)#
alias, for callers that read it as “what will transform(X, names=…) produce”
- Parameters:
names (list[str] | None)
- Return type:
list[str]
- CLIP = 10.0#
transformed coordinates are clipped to +-CLIP reference standard deviations. A power transform fitted on one population extrapolates on another, and the extrapolation is a power: one CPL structure (KMFLYQEEVE, clone mel8) has a standoff height of -2.51 A where the hold-out minimum is +6.59 A, and Yeo-Johnson’s negative branch turns that into
|z|= 5.1e4 while the 99.9th percentile of the same cohort is 8.5. That structure IS maximally anomalous and should score as such – the clip preserves that – but at 5e4 it dominates every sum it enters and takes an out-of-fold R^2 to -1.1e4. Ten reference sd is far outside anything real; the difference between 10 and 5e4 is arithmetic, not biology.
- transform(X, *, names=None, count_clipped=False)[source]#
Transformed and standardized against the fitted reference. Non-finite stays non-finite.
names scores a SUBSET of the fitted descriptors – the columns of X, in order. The parameters are per descriptor, so a subset is exact rather than an approximation; it is how a feature table that omits a whole family is still scored, by marginalization downstream.
- Parameters:
X (ndarray)
names (list[str] | None)
count_clipped (bool)
tcren.score.model module#
One joint Gaussian per class; every read-out is a projection of it.
The whole model is (mu_c, Sigma_c) for c in {non-binder, binder}, estimated once on the hold-out in the transformed descriptor space. Everything the author asked for is then a linear algebra operation on that one object, with no further fitting:
posterior P(1|x) = pi_1 N(x; mu_1, S_1) / sum_c pi_c N(x; mu_c, S_c)
any marginal, exact P(1|x_S): the same formula on mu_{c,S} and the sub-block S_{c,SS}. “Keep only geometry and dPhi_pep_mhc” is a sub-block, not a re-fit.
PCA at any width the covariance in PCA coordinates is W’ S_c W exactly, so truncating to m components is a projection of the same object – again no re-fit.
ipTM prediction E[ipTM | x] = mu_ipTM + S_{ipTM,x} S_xx^-1 (x - mu_x), and the residual R = ipTM - E[ipTM | x] is the confidence-misbehaviour channel.
one-class anomaly A(x) = sum_{k<K} (u_k’(x - mu_1))^2 / lam_k over the stiffest binder directions – needs no negatives at all, so it is a different tier of fitting from the rest.
There is no Jacobian in any of this. The author’s note asked to back-transform
P(binder|PCA) -> Jacobian * P(binder|descriptors). For a POSTERIOR the Jacobian cancels: PCA is
affine, |det dz/dx| is a constant independent of x, and it appears identically in numerator and
denominator. P(c|z) == P(c|x) exactly whenever W is square and invertible, and
tests/unit/test_score.py asserts it to 1e-9 rather than taking it on faith. The Jacobian is real for
the DENSITY; what happens under truncation is not a Jacobian but a marginalization, which for a
Gaussian is the sub-block above.
Shrinkage is not optional here. A full covariance over 145 coordinates is 10,585 parameters and the negative arm has 1,155 rows once benchmark structures are held out. Sigma is shrunk toward a scaled identity with a Ledoit-Wolf intensity; without it the smallest eigenvalues are estimation noise and the Mahalanobis form divides by them.
- class tcren.score.model.Joint(names, mu=<factory>, cov=<factory>, prior=<factory>, alpha=<factory>, lam1=None, U1=None)[source]#
Bases:
objectTwo Gaussians in one transformed coordinate system, and the read-outs they induce.
- Parameters:
names (list[str])
mu (dict[int, ndarray])
cov (dict[int, ndarray])
prior (dict[int, float])
alpha (dict[int, float])
lam1 (ndarray | None)
U1 (ndarray | None)
- names: list[str]#
- mu: dict[int, ndarray]#
- cov: dict[int, ndarray]#
- prior: dict[int, float]#
- alpha: dict[int, float]#
- lam1: ndarray | None = None#
- U1: ndarray | None = None#
- fit(X, w, shrink=True)[source]#
shrink=False keeps the raw covariance, which only the artefact test wants.
Ledoit-Wolf floors the smallest binder direction at s.d. 0.0797, and the directions the crystal test flags as the generator’s own regularity all sit below 0.05 – so a shrunk model cannot see the artefact band at all, and measuring it needs the raw one.
- Parameters:
X (dict[int, ndarray])
w (dict[int, ndarray])
shrink (bool)
- Return type:
- log_odds(X, subset=None)[source]#
log P(1|x_S) - log P(0|x_S), exact for any subset S by sub-blocking Sigma.
X carries exactly the columns subset names, in that order, when subset is given.
- Parameters:
X (ndarray)
subset (list[str] | None)
- Return type:
ndarray
- anomaly(X, k=None)[source]#
Partial Mahalanobis to the binder Gaussian on its k stiffest directions.
One-class: the negatives are never read. This is the tier-1 read-out – the same standing as the shipped q_score/t_score, which estimate a reference covariance and no label.
- Parameters:
X (ndarray)
k (int | None)
- Return type:
ndarray
- anomaly_on(X, names, k=None)[source]#
anomaly()restricted to the coordinates names, which X’s columns carry.The eigenbasis is recomputed on the sub-block rather than sliced out of the full one: the stiff directions of a marginal are not the stiff directions of the joint restricted, and using the latter would score a structure against directions its table cannot supply.
- Parameters:
X (ndarray)
names (list[str])
k (int | None)
- Return type:
ndarray
- predict(X, target, given=None, cls=1)[source]#
E[target | given] under class cls – the Gaussian conditional mean.
With target=”iptm” this is what the structure says the generator’s confidence should have been; the residual against the reported value is the QC channel.
- Parameters:
X (ndarray)
target (str)
given (list[str] | None)
cls (int)
- Return type:
ndarray
- project(m)[source]#
The same model in the top-m PCA coordinates of the pooled within-class scatter.
W’ Sigma_c W is exact, so this is a projection of the fitted object and not a second fit. At m = p with W orthonormal it is a rotation, and the posterior is unchanged – the identity tests/unit/test_score.py asserts.
- Parameters:
m (int)
- Return type:
tuple[ndarray, Joint]
tcren.score.fit module#
Fitting the frozen hold-out model, and the reason this file is shipped rather than kept aside.
P_native was withdrawn from this project because its coefficients were frozen against a
training set that no longer existed, which made it the one part of the package a reader could not
reproduce. The repair is not to stop fitting – a two-class covariance is what reads a
variance break, and a variance break is what separates binders on the hardest stratum – but to
ship the fitter, the manifest and the frozen output together, so that
tcren fit-holdout –features <table> –manifest <csv> -o holdout_model.npz
reproduces tcren.score.MODEL_FILE from inputs that are named and public.
What is estimated, and on what: a mean and a covariance per class over the transformed descriptor coordinates, on out-of-panel structures only, weighted 1/n_epitope so that one deeply sampled epitope does not set the shape of the binder manifold. ipTM enters as one further coordinate of the binder Gaussian alone, because every hold-out positive carries it and one whole negative arm does not – a two-class joint over it would learn a property of the deposit rather than of the interface.
- tcren.score.fit.epitope_weights(epitopes)[source]#
1/n per epitope, normalised: even coverage without discarding a structure.
Subsampling to an even epitope count reaches the same first and second moments and throws rows away; the whole construction here is a covariance, so thinning it is the one thing not to do.
- Return type:
ndarray
- tcren.score.fit.fit_holdout(features, manifest, *, out=None)[source]#
Fit the frozen model.
featuresandmanifestare polars frames keyed onpdb.id.manifestneeds the id column,y(1 binder / 0 non-binder) andepitope; aniptmcolumn is used for the confidence coordinate if present and skipped if not.- Parameters:
out (Path | None)
- Return type:
dict
Fit-free cohort scores#
tcren.cohort module#
Cohort-relative recognition scores — the recommended, fit-free screening layer.
These carry no trained coefficients — no logistic, no fit, no training set — so they cannot leak, cannot go stale, and there is nothing to re-derive. The fitted composites that stood beside them were removed in 2.26.0; their coefficients were frozen against training sets that no longer exist, which made them the one part of the package a reader could not reproduce.
q_score()generalises across cohorts: a logistic trained on one cohort learns that cohort’s epitope composition and does not transfer, whereasQhas nothing to transfer. With ipTM it reproduces the headline synergy fit-free.strain_z()grades pose forcedness (crystal < AF-real < AF-decoy) reproducibly.
They are cohort-relative by default: each standardizes a feature over the set being ranked.
For a candidate set, score the whole batch together (tcren recognize over a directory). For a
single structure, or a small/heterogeneous user set where the batch is not a fair reference, pass
reference=native_reference() (with features=Q_FEATURES_GEOM): the descriptors are then
standardized against the shipped Native2026 crystal manifold, so Q is defined for one structure and
transfers across inputs. The descriptors are counts and bounded ratios (mildly non-normal), so
method="rank" gives a robust, assumption-free percentile standardization; on the benchmarks it
agrees with the default z to ρ≈0.98. The division of labour is scores in tcren, evaluation
(ROC/PR/CI) downstream.
All functions take the table tcren features emits (a mapping of column name to sequence, a
polars/pandas frame, or a dict of arrays) and return one value per row.
Where the line is drawn. Every score the TCRen2 manuscript reports is computed here or in
tcren.footprint, tcren.mechanics, tcren.ddg and tcren.potential — a
benchmark script that recomputes one of them by hand is a bug, not a shortcut. What stays outside
the library is evaluation: ROC/PR/AUC, bootstrap intervals, macro-averaging over cohorts, and any
protocol that consumes a binder label (leave-one-epitope-out anchoring, an in-sample GLM against a
generator’s confidence). Those need the labels this library is built to do without, so they live in
the benchmark repo next to the data that carries them.
Sign convention: every term is oriented so that higher = more binder-like for
q_score(), and higher = more forced/strained for strain_z().
Note
The hand-written combination rules this module once exposed were removed in 2.12.0, and the
fitted cohort posterior that replaced them was itself discarded in 2.26.0. Use
q_score() for the single-structure interface-quality score, and
tcren.reliability.s_score() for the composition.
- tcren.cohort.zscore(x, reference=None, method='z')[source]#
NaN-aware standardization.
referencecalibrates against another cohort.Passing
referenceis what makesstrain_z()crystal-calibrated: the mean and sd come from the crystallographic ensemble, so the score reads ~0 on crystals by construction and grows as a pose departs from the natural manifold. Without it, a cohort of uniformly forced poses would standardize to zero mean and the shift would be invisible.- Parameters:
x – values to standardize.
reference – cohort defining the location/scale; defaults to
xitself (cohort-relative).method –
"z"(mean/sd, the default) or"rank"— the percentile of eachxagainst the reference, mapped to[-1, 1](2·percentile − 1)."rank"is scale-free and makes no normality assumption, so it is the robust choice for the bounded/count descriptors ofQ(chain balance, H-bond and contact counts are not normal). On the benchmarkszandrankagree to Spearman ρ≈0.98 and differ by <0.005 AUROC, sozis kept as the default; userankwhen a heavy-tailed user descriptor could distort the mean/sd.
- Return type:
ndarray
- tcren.cohort.q_score(table, reference=None, features=('burial', 'n_pep_contacted', 'chain_balance', 'n_hbond'), method='z', decorrelate=True, signs=None)[source]#
Interface-quality score
Q— fit-free, single-structure-capable; the default binder score.The default is the directional, decorrelated one-class score over
k = len(features)terms\[Q(x) \;=\; z(x)^{\top}\, C^{-1}\, \mathbf{1}, \qquad z(x)_k = \frac{d_k(x)-\mu_k}{\sigma_k},\]where each descriptor is standardized against the native crystal reference (
reference, defaultnative_reference()— its \(\mu_k,\sigma_k\)), \(C\) is the native descriptor correlation matrix, and \(\mathbf 1\) is the biophysical every-descriptor-higher-is-better direction. Whitening by \(C^{-1}\) stops correlated descriptors from double-counting. The score carries no fitted coefficient (only the native covariance is estimated; \(\mathbf 1\) is fixed), needs no negative set, is calibrated on natives so it transfers across inputs, and is defined for a single structure. It reduces to the equal-weight mean when the descriptors are uncorrelated (\(C=I\)). See the manuscript Methods (S Scores).decorrelate=Falserecovers the legacy equal-weight mean \(Q=\frac1k\sum_k z(d_k)\).- Parameters:
table – the
tcren recognize --fulltable (dict / pandas / polars).reference – cohort defining \(\mu,\sigma,C\).
Noneusesnative_reference()whendecorrelate(so the covariance is defined for any input, incl. one structure); withdecorrelate=Falseit means cohort-relative (thetableitself).features – the
kdescriptors. DefaultQ_FEATURES_GEOM(k=4, geometry only) — the validated default. Adding thepp_comboenergy term (k=5,Q_FEATURES) degrades ranking on generated poses, because that term inverts on forced ones (ledger C42).method – per-descriptor standardization,
"z"(default) or"rank"— seezscore().decorrelate – whiten by the native covariance and project onto \(\mathbf 1\) (default); else the equal-weight mean.
signs – per-descriptor orientation replacing \(\mathbf 1\), for a block whose terms are not all “higher = more native” — the topology block’s footprint fraction runs the other way. Length must match
features.Nonekeeps \(\mathbf 1\).
- Return type:
ndarray
- tcren.cohort.phi_score(table, reference=None, terms=('Phi_tcr_pep', 'Phi_tcr_mhc'))[source]#
Binder-oriented TCRen contact energy
F = z(-(Phi_tcr_pep + Phi_tcr_mhc))— the chemistry channel.The standardized, sign-flipped sum of the
PHI_TERMScontact energies, so higher = more binder-like and it is on the same z-scale asq_score(). UnlikeQ(interface geometry),Freads the actual contact chemistry — and unlikeQit is pose-conditional: it works on well-modelled poses and inverts on forced ones (benchmark ledger C27/C42). Do not use it unconditioned on pose quality; seePHI_TERMS.Cohort-relative (standardized over the ranked set); pass
referenceto standardize against another cohort (seezscore()).- Return type:
ndarray
- tcren.cohort.q_coupled(q, energy, r=None)[source]#
Parameter-free binder score: interface geometry and coupling-weighted contact energy.
Deprecated since version 2.12: No longer a component of any recommended score. Nothing here changes: this function returns exactly what it always has, and the numbers it produces stand (TCRvdb macro ROC 0.802 / PR 0.817). What changed is that the footprint-shape channel makes the gate unnecessary — the energy is one input among four rather than a term that has to be disarmed.
\[S(x) \;=\; \tfrac14\Big[1+\operatorname{erf}\tfrac{z(Q(x))}{\sqrt2}\Big] \Big[1+\operatorname{erf}\tfrac{r\,z(\Delta\Phi(x))}{\sqrt2}\Big], \qquad r=\operatorname{corr}(Q,\Delta\Phi)\]Each bracket is \(2\times\) a Gaussian tail probability, written with
erfrather than the normal-CDF symbol so nothing collides with the potential \(\Phi\). Three biophysical statements, no free parameter — \(z\) is standardization and \(r\) is measured, not chosen.Binding needs both. A complex forms only if there is an interface and the residues in it are favourable. Each factor is the one-class probability that the candidate is native-like on that channel, and the product is the conjunction of two pieces of evidence — the smooth AND, with no threshold and no softness constant.
The energy is admitted in proportion to its coupling. Under joint normality \(\mathbb E[z(Q)\mid z(\Delta\Phi)] = r\,z(\Delta\Phi)\), so \(r\,z(\Delta\Phi)\) is exactly the part of the energy that is evidence about interface nativeness. Nothing is discarded and nothing is over-trusted.
A forced pose disarms itself. \(r<0\) on a fabricated cohort (
coupling()), which flips the energy’s sign automatically; \(r\approx0\) shrinks the factor to \(\Phi(0)=\tfrac12\), a constant, leaving the geometry alone. The failure mode that makes raw \(\Phi\) inverting and dangerous (ledger C27) is handled by the same \(r\) that measures it.
On TCRvdb this reaches macro ROC 0.799 / PR 0.817 / precision-at-10 %-recall 0.949, ahead of every TCRmodel2 confidence (best 0.795 / 0.800 / 0.916) with no generative term, and it is balanced across the two epitopes rather than trading one for the other.
- Parameters:
q – interface-quality scores for the cohort, e.g.
q_score()output.energy – binder-oriented referenced contact energy for the same rows — for receptor ranking use the TCR-referenced \(\Delta_{\mathrm{TCR}}\Phi\) (the peptide is fixed there, so the peptide reference carries no signal); for peptide ranking use \(\Delta_{\mathrm{pep}}\Phi\) (
tcren.reference_delta()).r – coupling weight.
None(default) measures it from the cohort withcoupling(), which is what every published number uses. Pass a scalar or a per-row array to supply it from outside — a predicted \(\hat C\) from a single structure is defined atn = 1, where the cohort estimator is not (atn = 2it returns \(|\hat r| = 1\) by construction, with the wrong sign in 43.4 % of GLCTLVAML draws).
- Returns:
Scores in \((0,1)\); higher is more binder-like. Cohort-relative — rank within the set you scored, do not compare across cohorts.
- Return type:
ndarray
- tcren.cohort.coupling(q, energy)[source]#
Interface–energy coupling \(r=\mathrm{corr}(Q,\,\Delta\Phi)\) over a cohort — the label-free forced-pose diagnostic.
Deprecated since version 2.12: No longer a component of any recommended score; keep it as a diagnostic you report, not a weight you apply. What it measures is real and worth knowing — on the heavily crystallised GLCTLVAML cohort it reads −0.2617 and the referenced energy ranks binders at AUROC 0.338 [0.250, 0.433], entirely below chance, while on the sparsely templated YLQPRTFLL it reads +0.4784 and the same energy reads 0.776 [0.728, 0.820].
In a genuine complex the two channels are physically tied: a larger, better-packed interface holds more contacts, so favourable contact energy and good interface geometry rise together and \(r>0\). A structure generator that manufactures a pose optimises contacts without the interface, breaking the tie — the two channels decouple or run opposite, and \(r<0\).
So the sign and size of \(r\) say how far the energy of this cohort can be trusted, using no labels and no reference set. It is the weight
q_coupled()admits the energy with.- Parameters:
q – interface-quality scores (e.g.
q_score()output) for the cohort.energy – binder-oriented referenced contact energy for the same rows.
- Returns:
Pearson \(r\) over the rows where both are finite;
0.0if fewer than three remain (an uninformative cohort contributes no energy evidence rather than a spurious weight).- Return type:
float
- tcren.cohort.strain_z(table, reference=None)[source]#
Crystal-calibrated interface strain; higher = more forced. The recommended forced-pose score.
Directional mean-z of
STRAIN_TERMSwith fixed physical signs. Pass the crystal cohort asreferenceto reproduce the provenance gradient (crystal +0.02 < generated-real +0.40 < generated-decoy +0.81); without it the score is only relative within the input set.Unfitted — no logistic, no coefficients, just signed standardization — so it carries no training set and is fully reproducible. It grades forced-ness continuously, which is what pairs with
q_score()to catch the forced poses where the contact energy inverts.- Return type:
ndarray
- tcren.cohort.native_reference()[source]#
The interface-geometry descriptors over the 374 Native2026 crystal complexes, shipped so a single user structure (or any small cohort) can be standardized against the natural interface manifold instead of against itself — the deployment path for generic input:
from tcren import cohort q = cohort.q_score(user_table, reference=cohort.native_reference(), features=cohort.Q_FEATURES_GEOM)
Use
Q_FEATURES_GEOM(the four geometry terms) for one structure: the fifth termpp_combois a within-cohort z-contrast and is undefined for a single row. Returns a dict of column arrays (burial, n_pep_contacted, chain_balance, n_hbond, Phi_cdr12, Phi_cdr3a) usable as thereferenceargument. Provenance:tcren recognize --fullover the Native2026 set.- Return type:
dict
- tcren.cohort.Q_FEATURES = ('burial', 'n_pep_contacted', 'chain_balance', 'n_hbond', 'pp_combo')#
The five interface-quality descriptors, equal-weighted in
q_score(). Each is oriented positive-is-better as given.pp_combois the CDR1/2-vs-CDR3alpha TCRen contrast — the one energy term robust to the forced-pose inversion (benchmark ledger C27), since it is a contrast rather than an absolute contact energy. Per-term macro AUROC on TCRvdb: burial 0.73, n_hbond 0.69, pp_combo 0.66, n_pep_contacted 0.62, chain_balance 0.61; the terms are near-independent (mean absolute Spearman 0.20).
- tcren.cohort.Q_FEATURES_CORE = ('burial', 'chain_balance', 'n_hbond', 'pp_combo')#
it is the weakest term and removing it raises macro AUROC 0.795 -> 0.801 on TCRvdb (benchmark ledger, energy memo). Pass
features=Q_FEATURES_COREtoq_score()for the simpler, marginally better score.- Type:
The four load-bearing descriptors.
n_pep_contactedis dropped
- tcren.cohort.Q_FEATURES_GEOM = ('burial', 'n_pep_contacted', 'chain_balance', 'n_hbond')#
The four geometry-only descriptors —
Q_FEATURESwithout thepp_comboenergy contrast. This isQ_geom, the AF-orthogonal channel that survives the forced-pose regime where the contact energy inverts (benchmark ledger C27/C42):z(ipTM) + z(q_score(..., features=Q_FEATURES_GEOM))beats raw-AF ipTM on well-modelled (“template-covered”) epitopes on both ROC and PR, while the energy term is used only conditioned on pose quality. Pass toq_score().
- tcren.cohort.PHI_TERMS = ('Phi_tcr_pep', 'Phi_tcr_mhc')#
The TCRen contact-energy terms summed into the binder-oriented
phi_score().Phi_tcr_pepis the TCR:peptide TCRen energy,Phi_tcr_mhcthe TCR:MHC energy; both are emitted bytcren recognize. They are raw energies (lower = tighter), sophi_score()negates the sum to make higher = more binder-like. This term is pose-conditional — it reads real binding chemistry on well-modelled (crystal-templated) poses and inverts on forced ones (benchmark ledger C27/C42): on the forced GLCTLVAML TCRvdb pose-Phi_tcr_pepranks binders at AUROC 0.36 (backwards), on the clean YLQPRTFLL pose at 0.59. Use it only conditioned on pose quality — gate withstrain_z(), or readz(Q)-z(F)on forced poses andz(Q)+z(F)on clean ones.
- tcren.cohort.STRAIN_TERMS = (('cdr3b_topep', 1.0), ('cdr3b_reach', 1.0), ('extent_per_ct', 1.0), ('chain_balance', -1.0))#
Crystal-calibrated interface-strain terms with their physical signs. A forced pose reaches further from the peptide with a thinner, less balanced interface.
tcren.binder package#
Interface-sanity flag for a modelled complex.
The fitted binder score this package once carried was removed in 2.26.0 – its coefficients were frozen against a training set that no longer exists. What remains is the pre-energy check that a TCR:pMHC interface is a plausible dock at all, which is a rule over contact count and docking geometry rather than a model.
- tcren.binder.is_real_interface(n_contacts, scanning_angle, pitch_angle)[source]#
False if the TCR:pMHC interface is assay noise / a failed dock.
NaN(orNone) in any input =>False(undocked). Thresholds are the ~p01-p99 range of real (binder) interfaces from the TCRvdb AF set.- Parameters:
n_contacts (float | None) – number of TCR:peptide residue-pair contacts at the interface.
scanning_angle (float | None) – TCR crossing (scanning) angle in degrees (
DockingAngles.crossing_angle).pitch_angle (float | None) – TCR incident (pitch) angle in degrees (
DockingAngles.incident_angle).
- Returns:
Trueonly ifn_contacts >= N_CONTACTS_MINandscanning_anglelies inSCANNING_RANGEandpitch_anglelies inPITCH_RANGE;Falseotherwise, including when any input is missing.- Return type:
bool
Example
>>> is_real_interface(25, 45.0, 5.0) True >>> is_real_interface(0, 45.0, 5.0) False
tcren.binder.noise module#
Interface-sanity filter: flag trivial non-interfaces as assay noise before scoring.
A TCR-pMHC “complex” can fail to be a real interface for two reasons that need no energy
model to spot: the dock collapsed (few or no TCR:peptide contacts) or the docking geometry
is out of the physiological range (a failed pose lands the TCR at an implausible crossing or
tilt). is_real_interface() rejects both from three cheap descriptors so the downstream
energy score is only ever asked about interfaces that could plausibly be binders.
The thresholds are the p01-p99 range of the 309 real (binder) interfaces from the TCRvdb
AlphaFold set, rounded inward (lower bounds up, upper bounds down): at least
N_CONTACTS_MIN TCR:peptide residue contacts, a crossing (scanning) angle in
SCANNING_RANGE and an incident (pitch) angle in PITCH_RANGE. Any missing
descriptor (NaN/None, i.e. an undocked or un-oriented complex) is treated as noise.
The derivation is models/fit_frozen.py::envelope in the benchmark repo, which reproduces all
three constants exactly and is regression-tested there. Two caveats it records: the pitch lower
bound of 0 deg is the domain floor of an unsigned angle rather than a percentile (p01 = 0.04), and
the pitch axis is derived from a cached pitch_angle column carrying AlphaFold-confidence
leakage – the scanning and contact-count axes are clean. Because that bound is a floor on an
unsigned angle, the pitch is compared as abs(pitch_angle): tcren’s own
DockingAngles.incident_angle is signed, and a downward tilt is the same tilt.
- tcren.binder.noise.is_real_interface(n_contacts, scanning_angle, pitch_angle)[source]#
False if the TCR:pMHC interface is assay noise / a failed dock.
NaN(orNone) in any input =>False(undocked). Thresholds are the ~p01-p99 range of real (binder) interfaces from the TCRvdb AF set.- Parameters:
n_contacts (float | None) – number of TCR:peptide residue-pair contacts at the interface.
scanning_angle (float | None) – TCR crossing (scanning) angle in degrees (
DockingAngles.crossing_angle).pitch_angle (float | None) – TCR incident (pitch) angle in degrees (
DockingAngles.incident_angle).
- Returns:
Trueonly ifn_contacts >= N_CONTACTS_MINandscanning_anglelies inSCANNING_RANGEandpitch_anglelies inPITCH_RANGE;Falseotherwise, including when any input is missing.- Return type:
bool
Example
>>> is_real_interface(25, 45.0, 5.0) True >>> is_real_interface(0, 45.0, 5.0) False
Epitope scoring and ranking#
tcren.scoring_rank module#
Percentile rank of a native peptide’s energy against a random pMHC background.
For a given structure, scores the native peptide together with a background of
random (or epitope-sampled) peptides of the same length and reports where the
native score falls in that distribution. Lower TCRen energy = better binder, so a
small rank_pct means the native peptide scores at least as well as only a small
fraction of the background.
- tcren.scoring_rank.background_peptides(length, n=1000, seed=0, source=None, anchors=None, pool=None)[source]#
Build a background set of
npeptides of the given length.- Parameters:
length (int) – Required peptide length.
n (int) – Number of background peptides to return.
seed (int) – Random seed for reproducibility.
source (str | None) – Optional FASTA/text file of epitopes. When given, peptides of the requested length are sampled from it (with replacement); each sampled sequence is also position-permuted so the background is a shuffled-epitope distribution rather than a verbatim copy. When
None, peptides are drawn uniformly at random over the 20 amino acids.anchors (dict[int, str] | None) – 1-based position -> fixed residue, applied after sampling. Pass the cognate peptide’s anchor residues (typically
{2: p[1], length: p[-1]}) to hold MHC presentation constant so the background varies only where the TCR reads. Without this, a “random peptide” background is dominated by peptides that would not be presented at all, which inflates the apparent discrimination. Required to reproduce the published cognate-vs-decoy benchmark.pool (list[str] | None) – Explicit candidate peptides to draw from (with replacement) instead of generating them – e.g. the native epitopes of the other structures in a cohort, which is the decoy set used by the crystal peptide-swap benchmark.
- Returns:
A list of
nupper-case peptide strings, each of lengthlength.- Return type:
list[str]
- tcren.scoring_rank.percentile_rank(contact_map, peptide, potential, *, interface='tcr_peptide', n_background=1000, seed=0, tcr_regions='all', background=None, contact_weight='residue')[source]#
Percentile rank of a peptide’s energy against a random pMHC background.
Scores
peptidetogether with a background set (supplied or generated) and returns the fraction of background peptides whose score is<=the native score. Because lower TCRen energy means a better binder, a smallerrank_pctindicates the native peptide is among the strongest binders.- Parameters:
contact_map (ContactMap) – The structure’s contact map.
peptide (str) – The native peptide to rank.
potential (Potential) – Pairwise potential to score with.
interface (Literal['tcr_peptide', 'tcr_mhc', 'peptide_mhc']) – Which interface to score over (default
"tcr_peptide").n_background (int) – Size of the generated background (ignored if
backgroundis given).seed (int) – Random seed for background generation.
tcr_regions (str) – Which TCR regions to keep on the TCR side (
"all"default,"cdr"or"cdr+fr"); passed through toscore_peptides.background (Iterable[str] | None) – Explicit background peptides; when
Nonea uniform-random background of lengthlen(peptide)is generated.contact_weight (str) –
"residue"(default) or"atomic"; passed through toscore_peptides("atomic"needs ann_atom_contactscontact map).
- Returns:
Mapping with keys
peptide,score(native energy),rank_pct(fraction of background with score<=native), andn_background.- Return type:
dict
tcren.cpl module#
Predict a combinatorial-peptide-library response matrix from one template TCR:pMHC structure.
A positional-scanning combinatorial peptide library (CPL) measures a T-cell clone’s peptide
preference one position at a time. For a peptide of length L, each of the L x 20
sublibraries fixes position i to amino acid a and leaves every other position an equimolar
1/20 mixture, so the measured cell is an ensemble mean,
R[i, a] = E[ response | x_i = a ] .
This module predicts that matrix from a single deposited or modelled complex. Position i has a
fixed set of contact partners in the template, so threading each of the twenty residues through the
same contact map and re-reading the potential costs one batched call per interface. Nothing is
re-docked, nothing moves, and nothing is fitted to any assay.
WHAT A CELL IS SCORED WITH. The assay reads activation, which needs the peptide presented and the receptor engaged: a substitution that abolishes MHC binding abolishes the response whatever the receptor would have done. Every cell therefore carries the sum of both peptide-bearing interfaces,
Phi = Phi(TCR:peptide) + Phi(peptide:MHC),
TCRen over the first and Miyazawa–Jernigan over the second by default. The two channels are statistically uncorrelated over these cells, so they add rather than duplicate. A position the receptor never touches is an anchor: its TCR term is identically zero, so scoring by the sum degrades gracefully to presentation alone, and the older “TCRen at receptor-facing positions, MJ at anchors” partition is this rule’s special case rather than a separate mode.
TWO REFERENCE STATES, BOTH USEFUL. A raw Phi carries a large per-position offset that says only
how many contacts the position makes, so a cell is meaningful only relative to the other residues
that could sit there. Two references are offered and both are exact differences of whole-peptide
energies, so the “rest of the peptide” cancels:
"wild_type"Phi(x_{i->wt}) - Phi(x_{i->a})– a mutation scan off the residue the template carries. This is the neoantigen / epitope-design question: is this substitution better than what is there? It istcren.ddg.ddg()resolved per position."equimolar"(default)mean_b Phi(x_{i->b}) - Phi(x_{i->a})– referenced to the 1/20 mixture, which is the assay’s own null: the CPL background at positioniis exactly that mixture. Use this to compare against measured CPL cells. It is also the only one of the two under which the template’s own residue is an ordinary measurement rather than a forced zero.
The two differ by a per-position constant, and that constant is not noise: it is how far the template’s residue sits above its column’s mean. Referencing to the wild type folds that between-position quantity into every cell of the column, which is the right thing for a mutation scan and the wrong thing for a comparison against an assay whose background is the mixture.
SIGN. Lower energy is a better binder throughout tcren, and both references are written as
reference - candidate, so positive means favourable: a positive wild_type value says the
substitution improves on the template residue, and a positive equimolar value says the residue is
better than the average residue at that position.
Example
>>> from tcren import ContactMap, parse_structure, response_matrix, position_scan
>>> from tcren.annotation import classify_chains
>>> from tcren.mhc import annotate_mhc
>>> s = parse_structure("3HG1.pdb", pdb_id="3HG1")
>>> classify_chains(s, organism="human"); annotate_mhc(s)
>>> rm = response_matrix(ContactMap.from_structure(s, cutoff=5.0))
>>> rm.peptide
'ELAGIGILTV'
>>> position_scan(rm, 5).head(3) # every residue at position 5
- tcren.cpl.AA20: tuple[str, ...] = ('A', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'Y')#
Column order of every matrix this module returns.
- tcren.cpl.INTERFACES: tuple[str, ...] = ('tcr_peptide', 'peptide_mhc')#
The two peptide-bearing interfaces, and the potential family each is scored with by default.
tcr_mhcis absent on purpose: a peptide substitution cannot change it, so its contribution to every cell of the matrix is identically zero.
- tcren.cpl.REFERENCES: tuple[str, ...] = ('equimolar', 'wild_type')#
Accepted values of the
referenceargument.
- class tcren.cpl.ResponseMatrix(peptide, positions, interface_class, aa, phi, phi_tcr, phi_mhc, structure_id)[source]#
Bases:
objectA predicted CPL response matrix: peptide positions x twenty amino acids.
phi[i, a]is the total interface energy of the template with amino acidaa[a]threaded atpositions[i], everything else held at the template sequence and geometry. It is a whole-complex energy, not a per-position contribution, so differences within a row are exact and differences between rows are meaningless – which is why every accessor references a row against something in that same row.Use
referenced()(ormutation_effect()/position_scan()/equimolar_effect()) rather than readingphidirectly.- Parameters:
peptide (str)
positions (tuple[int, ...])
interface_class (tuple[str, ...])
aa (tuple[str, ...])
phi (object)
phi_tcr (object)
phi_mhc (object)
structure_id (str)
- peptide: str#
the template’s own peptide sequence
- positions: tuple[int, ...]#
1-based peptide positions that contact either interface
- interface_class: tuple[str, ...]#
"receptor"or"anchor", one per row
- phi: object#
MHC
- Type:
(n_positions, 20)total energy, TCR- Type:
peptide + peptide
- phi_tcr: object#
peptide component alone
- Type:
the TCR
- phi_mhc: object#
MHC component alone
- Type:
the peptide
- structure_id: str#
the template’s
pdb_id
- row_of(position)[source]#
Row index of a 1-based peptide
position, or raise if it contacts nothing.Raising is deliberate. A position that touches neither interface has an entirely flat row, and returning that silently would report “this position tolerates everything” for what is really “this template says nothing about this position”.
- Parameters:
position (int)
- Return type:
int
- column_of(aa)[source]#
Column index of a one-letter amino acid code.
- Parameters:
aa (str)
- Return type:
int
- wild_type_at(position)[source]#
The residue the template carries at a 1-based peptide
position.- Parameters:
position (int)
- Return type:
str
- referenced(reference='equimolar')[source]#
The matrix as effects,
(n_positions, 20), positive = favourable.- Parameters:
reference (str) –
"equimolar"references each row to the mean over its twenty residues – the 1/20 mixture the assay holds the other positions at."wild_type"references it to the residue the template carries, making the matrix a mutation scan whose wild-type column is identically zero.- Returns:
reference_energy - phi, so a positive entry is a favourable residue.
- to_frame(reference=None)[source]#
Long form: one row per (position, amino acid) cell.
- Parameters:
reference (str | None) – emit only this reference’s
effectcolumn.None(default) emits both aseffect_equimolarandeffect_wild_type, which is what a side-by-side comparison against a measured matrix wants.- Returns:
Columns
structure_id,pos,wt_aa,aa,is_wt,interface_class,phi,phi_tcr,phi_mhc, and the requested effect column(s).- Return type:
DataFrame
- tcren.cpl.response_matrix(contact_map, peptide=None, *, tcr_potential=None, mhc_potential=None, tcr_regions='all', contact_weight='residue', tcr_weights=None)[source]#
Predict the CPL response matrix of a template TCR:pMHC complex.
Every peptide position that contacts either peptide-bearing interface gets a row; each of the twenty residues is threaded there in turn on the template’s own contact map and scored with both potentials, and the two interface energies are summed. See the module docstring for why both interfaces enter and what the two reference states mean.
- Parameters:
contact_map (ContactMap) – the template’s contact map, e.g.
ContactMap.from_structure(s, cutoff=5.0). The structure must have been chain-typed (tcren.annotation.classify_chains) and MHC-annotated (tcren.mhc.annotate_mhc) first, or the peptide:MHC interface comes out empty.peptide (str | None) – sequence to thread substitutions off. Defaults to the template’s own peptide, taken from the contact map. Must match the template’s peptide length.
tcr_potential (Potential | None) – potential for TCR:peptide. Default: the bundled TCRen.
mhc_potential (Potential | None) – potential for peptide:MHC. Default: Miyazawa–Jernigan, which is what the presentation interface is scored with throughout
tcren– TCRen is a directed TCR-to-peptide recognition potential and does not describe the groove.tcr_regions (str) – which TCR regions contribute on the TCR side (
"all"/"cdr"/"cdr+fr").contact_weight (str) –
"residue"(default) or"atomic"; passed through totcren.scoring.score_peptides().tcr_weights (ndarray | None) – an explicit per-contact multiplier for the TCR:peptide interface only, one value per row of
contact_map.interface("tcr_peptide")and in its row order. Its use is the same astcren.ddg.ddg()’sweights: replace the map’s hard 0/1 contact indicator with a contact probability –tcren.potts.contact_probabilities()’p_model– so every threaded substitution is scored against how often each pair actually touches rather than against one frozen snapshot of whether it did. The presentation interface is left alone, because the shipped Potts model is fitted on TCR:peptide.None(default) leaves the result byte-identical.
- Returns:
- Raises:
ValueError – if the complex has no peptide contacts at all, or if
peptidehas the wrong length. Both are silent-empty failures otherwise: an empty matrix is indistinguishable from a genuinely flat one.- Return type:
- tcren.cpl.mutation_effect(rm, position, aa, *, reference='equimolar')[source]#
Effect of ONE substitution at ONE position, positive = favourable.
- Parameters:
rm (ResponseMatrix) – a predicted
ResponseMatrix.position (int) – 1-based peptide position.
aa (str) – the one-letter residue to put there.
reference (str) –
"equimolar"(default) scores it against the 1/20 mixture at that position;"wild_type"scores it against the residue the template carries, i.e. the classical mutation-scanddG.
- Returns:
A single number. Under
"wild_type"the template’s own residue returns exactly0.0.- Return type:
float
- tcren.cpl.position_scan(rm, position, *, reference='equimolar')[source]#
Effect of EVERY substitution at one position – one column of the response matrix.
- Parameters:
rm (ResponseMatrix) – a predicted
ResponseMatrix.position (int) – 1-based peptide position.
reference (str) – as
mutation_effect().
- Returns:
Twenty rows –
pos,aa,wt_aa,is_wt,interface_class,phi,effect– sorted best residue first, so the head of the frame is what the receptor prefers at that position.- Return type:
DataFrame
- tcren.cpl.equimolar_effect(rm, position, aa=None)[source]#
Effect of replacing a residue with a random 1/20 mixture at that position.
This is the sublibrary construction read backwards. The CPL background at position
iis the equimolar mixture, so the cost of giving up a defined residue for that mixture ismean_b Phi(x_{i->b}) - Phi(x_{i->a})– exactly the
"equimolar"reference. A positive result meansaais better than the average residue at that position, so scrambling it to the mixture loses that much; a negative result means the mixture is an improvement, i.e. the template residue is disfavoured there.- Parameters:
rm (ResponseMatrix) – a predicted
ResponseMatrix.position (int) – 1-based peptide position.
aa (str | None) – the residue being given up. Defaults to the one the template carries, which is the usual question: how much does this position’s identity matter?
- Returns:
A single number in the potential’s units.
- Return type:
float
tcren.shuffle module#
Shuffled decoys: wrong-TCR-on-real-pMHC negatives for TCR-recognition models.
Given a set of co-framed (oriented) TCR-pMHC complexes, keep each complex’s pMHC (peptide + MHC) intact and graft on a different complex’s TCR — a within-MHC-class derangement, so no complex keeps its own TCR. The result is a physically-implausible recognition complex: a real, correctly-presented pMHC with the wrong TCR docked over it. Both the TCR:peptide and TCR:MHC interfaces are therefore mismatched, while the peptide:MHC interface is untouched (a clean internal control: peptide:MHC energy is invariant under the graft).
These decoys are the true negatives that a one-class “plausible complex” density lacks. A classifier trained on real (label 1) vs shuffled (label 0) learns TCR-recognition compatibility from structures alone, with no binding-assay labels — useful as a general, label-free recognition prior and as a supplementary benchmark.
Inputs MUST be canonically oriented (all superposed into the common MHC frame): run
tcren.docking.run_folder() (tcren orient) or tcren.docking.superimpose() (tcren superimpose)
first. The graft is then a direct chain replacement with no per-pair alignment — deliberately unlike
tcren.docking.graft.substitute_tcr(), which superposes the donor MHC onto the host MHC pairwise. Because
every complex already sits in the one canonical frame, dropping in the donor TCR as-is lets it keep its own
native docking angle relative to the canonical MHC, so the decoy set spans the real MHC–TCR docking-angle
variance across the whole database rather than forcing every TCR onto one host’s pose. Chains are typed by
tcren.annotation.classify_chains() + tcren.mhc.annotate_mhc(), so the graft is by chain type.
CLI: tcren shuffle -s oriented/ -o shuffled/ --n 10.
- tcren.shuffle.mhc_class(s)[source]#
MHC class (
"MHCI"/"MHCII") from the annotated MHC-chain supertype, orNoneif unknown.- Parameters:
s (Structure)
- Return type:
str | None
- tcren.shuffle.graft_tcr(pmhc_source, tcr_source, pdb_id=None)[source]#
Build a decoy: the pMHC chains of
pmhc_source+ the TCR chains oftcr_source.Both structures must already share a coordinate frame (be oriented into the canonical MHC frame). No coordinate transform is applied — chains are copied as-is, so the grafted TCR keeps its own native docking angle relative to the canonical MHC (see the module docstring for why this beats pairwise alignment). TCR chain ids that collide with a pMHC chain id are reassigned.
- Parameters:
- Returns:
The decoy
Structure.- Raises:
ValueError – if
pmhc_sourcehas no pMHC ortcr_sourcehas no TCR.- Return type:
- tcren.shuffle.make_decoys(structures, n_per=10, within_class=True, seed=0)[source]#
Yield decoy structures: each input pMHC paired with
n_perdistinct other TCRs.Within each MHC class (if
within_class) the TCR sources are a random selection excluding the pMHC’s own complex, so no decoy reproduces a real pairing. Reproducible for a givenseed.- Parameters:
structures (Iterable[Structure]) – co-framed, chain-typed + MHC-annotated TCR-pMHC structures.
n_per (int) – decoys generated per input pMHC (capped at the pool size).
within_class (bool) – only graft TCRs from complexes of the same MHC class.
seed (int) – RNG seed.
- Yields:
Decoy
Structureobjects.- Return type:
Iterator[Structure]
- tcren.shuffle.run_shuffle(struct_dir, out, n=10, seed=0, within_class=True, organism='human', compress=False)[source]#
Load a folder of oriented complexes, generate
ndecoys per pMHC, write them, return the count.- Parameters:
struct_dir (str | Path)
out (str | Path)
n (int)
seed (int)
within_class (bool)
organism (str)
compress (bool)
- Return type:
int
tcren.pipeline module#
End-to-end TCRen pipeline: structure → annotation → orientation → contacts → score.
One call takes a TCR-pMHC structure all the way through the tcren workflow:
import the structure (C-gene trimmed);
annotate chains — TCR loci/CDRs via arda, MHC allele/class/role + groove regions;
superimpose onto the canonical database (canonical Cα frame; optional);
markup + contacts — the per-residue region table and the 5 Å contact map;
score each interface with its potential: TCRen for TCR↔peptide, MJ for TCR↔MHC and peptide↔MHC, plus the total.
The interface energy is the sum of the residue-pair potential over the observed contacts of that interface (the closest-atom contact per residue pair, as everywhere in tcren).
- class tcren.pipeline.PipelineResult(pdb_id, mhc_calls, markup, contacts, scores, oriented=None, rmsd=None, extra=<factory>)[source]#
Bases:
objectEverything the pipeline produces for one structure.
extracarries the interface-sanity flag when the complex was oriented (superimpose=True):real_interface(bool—Falsemarks assay noise / a failed dock; seetcren.binder.is_real_interface()), and the raw descriptors it was computed from (n_contacts,scanning_angle,pitch_angle). Withsuperimpose=Falsethe docking angles are unavailable, soreal_interfaceisNone(scanning_angle/pitch_angleNone) whilen_contactsis still set.- Parameters:
- pdb_id: str#
- markup: DataFrame#
- contacts: DataFrame#
- scores: dict[str, float]#
- rmsd: float | None#
- extra: dict#
- tcren.pipeline.run(structure, organism='human', superimpose=True, db_dir=None, cutoff=5.0, potentials=None, tcr_regions='all', contact_weight='residue', reference_aa=None, intra_weight=0.0, typed=False)[source]#
Run the full pipeline on one structure (path or parsed
Structure).- Parameters:
structure (str | Path | Structure) – a structure file (any tcren-readable format) or an already-parsed structure.
organism (str) – organism for TCR annotation.
superimpose (bool) – also orient onto the canonical database (sets
oriented+rmsd).db_dir (str | Path | None) – canonical database for
superimpose(defaultdata/Canonical2026).cutoff (float) – contact distance threshold (Å).
potentials (dict[str, str | Potential | None] | None) – optional per-interface potential override mapping an interface name (
"tcr_peptide","tcr_mhc","peptide_mhc") to aPotential, a bundled name ("tcren2"/"karnaukhov2022"/"mj"/"keskin"), a CSV path, orNone.None(or a missing entry) keeps the default family for that interface, so the default output is unchanged.tcr_regions (str) – which TCR regions to keep on the TCR side of the TCR-containing interfaces (
"all"default = no filter = legacy behaviour;"cdr"or"cdr+fr"to restrict).contact_weight (str) –
"residue"(default, legacy) weights each contacting residue pair by 1 on all three interfaces;"atomic"weights each pair by itsn_atom_contactsheavy-atom-pair count (the contact map is then built withcount_atoms=True). Applies totcr_peptide,tcr_mhcandpeptide_mhcalike.reference_aa (str | None) – if set (typically
"A"), also report the reference-normalised energiesdelta_<interface>anddelta_total— each interface’stcren.ddg.reference_delta(), i.e. its energy minus the energy of a poly-reference_aapeptide threaded onto the same contact map. Off by default, so the defaultscoresdict is unchanged.typed (bool) – the structure’s chains are already typed, so skip
classify_chains(). Chain typing costs onemmseqs easy-searchper structure and a caller scoring a whole cohort should have typed it in one batched call (tcren.paper.iter_annotated_set()) rather than paying that per structure. Only meaningful whenstructureis an already-parsedStructure.intra_weight (float) – weight of the intra-peptide term. Non-zero adds
scores["peptide_internal"]— the peptide’s contact energy with itself (tcren.intra_peptide_energy()), which every interface sum omits — and foldsintra_weight *that energy intoscores["total"].0.0(default) computes nothing and leavesscoresunchanged. Its potential is MJ unlesspotentials["peptide_internal"]overrides it.
- Returns:
A
PipelineResultwith the markup, contacts, per-interface scores and (if requested) the canonical-frame oriented structure.- Return type:
- tcren.pipeline.score_row(result)[source]#
Flatten a
PipelineResultto a one-row scores dict (for a CSV table).The
d_*reference-normalised columns are present only when the pipeline was run withreference_aaset, andPhi_pep_intonly when it was run with a non-zerointra_weight.- Parameters:
result (PipelineResult)
- Return type:
dict
tcren.oracle module#
One-call facade over the tcren pipeline for the paper notebooks.
summarize_structure() turns a single TCR-pMHC structure into a bundle of
ready-to-tabulate polars frames by composing the existing milestones:
S1+S2 —
tcren.pipeline.run()(annotate → orient → contacts → per-interface scores), giving thescores,markupandcontactsframes;S3 —
tcren.scoring_rank.percentile_rank()of the structure’s native peptide against a random pMHC background, giving therankframe;S4 —
tcren.ddg.alanine_scan()of the native peptide, giving theddgframe.
Nothing is re-derived here: the facade only orchestrates the milestone functions and
collects their outputs. The scores frame is therefore byte-identical to what
run produces for the same structure and arguments.
- tcren.oracle.summarize_structure(structure, *, organism='human', superimpose=True, potentials=None, tcr_regions='all', background=1000, seed=0, alanine=False, contact_weight='residue')[source]#
Summarise one TCR-pMHC structure into a bundle of tables.
Composes the tcren milestones on a single structure: the full pipeline (S1+S2) for per-interface energies, region markup and contacts; the percentile rank of the native peptide against a random background (S3); and, optionally, the per-position alanine scan (S4).
- Parameters:
structure (str | Path | Structure) – A structure file (any tcren-readable format) or a parsed
Structure.organism (str) – Organism for TCR annotation.
superimpose (bool) – Also orient onto the canonical database (adds
rmsdtoscores).potentials (dict[str, str | Potential | None] | None) – Optional per-interface potential override, forwarded to
tcren.pipeline.run(). The TCR↔peptide potential is also used for therankandddgframes.tcr_regions (str) – Which TCR regions to keep on the TCR side (
"all"default,"cdr"or"cdr+fr"); forwarded to every milestone.background (int) – Number of random background peptides for the percentile rank (S3).
seed (int) – Random seed for the background generation.
alanine (bool) – When
True, compute the per-position alanine scan (S4) for theddgframe. WhenFalse(default) the scan is skipped andddgis an empty frame with the same schema.contact_weight (str) –
"residue"(default, legacy: each contacting residue pair contributespotential[a, b] x 1) or"atomic"(each pair contributespotential[a, b] x n_atom_contacts). Applies to all three interfaces of thescoresframe and to therank/ddgframes."residue"keeps every output byte-identical to the legacy facade.
- Returns:
scores— one row of per-interface energies (andrmsdif superimposed), identical totcren.pipeline.run()’s scores.rank— one row: the native peptide’s energy and itsrank_pctagainst the background.ddg— the alanine scan (columnspos/wt_aa/ddG); empty unlessalanine=True.markup— the per-residue region-markup table.contacts— the annotated residue-contact table.
- Return type:
Mapping with five polars frames
Peptide substitution and refinement#
tcren.refine package#
Peptide substitution + potential-guided refinement.
substitute_peptide() threads a new sequence onto the peptide backbone; refine_peptide()
runs a knowledge-based rigid-body Monte-Carlo refinement of the peptide pose via the compiled
tcren._refine kernel. The refinement energy is the DOPE atom-level distance-dependent
statistical potential (Shen & Sali, Protein Science 2006), used here independently of the
TCRen/MJ potentials tcren scores epitopes with — so the pose is not optimised against the same
quantity it is later scored with. This is a lightweight, knowledge-based refine, NOT physics
relaxation (that is Rosetta FlexPepDock, as a subprocess).
- tcren.refine.substitute_peptide(structure, new_peptide, chain_type='PEPTIDE')[source]#
Return a copy of
structurewith the peptide chain threaded tonew_peptide.The peptide backbone (and Cβ) is preserved; side-chain atoms beyond Cβ are dropped (and Cβ too for any position mutated to glycine, while a position mutated from glycine has one built by
virtual_cb()).new_peptidemust equal the peptide length and use the 20 standard one-letter amino acids. Region markup is carried over onto the new residues, so the result can go straight into a contact map and be scored.For
new_peptideall-alanine this is exact, which is what makes a structural poly-alanine reference possible (tcren.ddg.reference_delta()with a structure). For other targets the result is a Cβ stub: correct in position, short in reach.
- tcren.refine.refine_peptide(structure, *, shell=12.0, restraint_w=0.5, n_steps=2000, trans_sigma=0.2, rot_sigma=0.05, temp0=1.0, temp1=0.05, seed=0)[source]#
Rigid-body refine the peptide pose against its TCR+MHC partners;
(structure, energy).The energy is the DOPE atom-level distance-dependent statistical potential summed over all peptide$leftrightarrow$partner heavy-atom pairs within DOPE’s range (its short-range bins are repulsive, so it provides its own clash term), plus a harmonic restraint to the input pose (
restraint_w) that keeps the search local. Only partner atoms withinshellÅ of the peptide are considered. The structure must be chain-typed (peptide = chain ofchain_type == 'PEPTIDE'). Requires the compiled_refineext + the bundled DOPE table.- Parameters:
structure (Structure)
shell (float)
restraint_w (float)
n_steps (int)
trans_sigma (float)
rot_sigma (float)
temp0 (float)
temp1 (float)
seed (int)
- tcren.refine.interface_energy(structure, *, shell=12.0)[source]#
DOPE interaction energy across the peptide↔partner interface (lower = more favourable).
The structure must be chain-typed (peptide =
chain_type == 'PEPTIDE'). Only partner heavy atoms withinshellÅ of the peptide are considered (beyond DOPE’s range they contribute nothing). Requires the compiledtcren._relaxext + the bundled DOPE table.- Parameters:
structure (Structure)
shell (float)
- Return type:
float
- tcren.refine.predict_anchors(peptide, structure=None)[source]#
Predict peptide anchors, preferring the structure’s MHC-class call over the length heuristic.
Pass a chain-typed, MHC-annotated
structure(seetcren.mhc.annotate_mhc()) to use its real class assignment; otherwise the class is inferred from peptide length.- Parameters:
peptide (str)
structure (Structure | None)
- Return type:
- tcren.refine.native_peptide(structure)[source]#
One-letter sequence of the structure’s PEPTIDE chain (raises if absent).
- Parameters:
structure (Structure)
- Return type:
str
- class tcren.refine.Decomposition(peptide, mhc_class, anchors, tcr_facing, presentation)[source]#
Bases:
objectAnchor vs TCR-facing split of a peptide.
- Parameters:
peptide (str)
mhc_class (str)
anchors (tuple[int, ...])
tcr_facing (str)
presentation (str)
- peptide: str#
- mhc_class: str#
- anchors: tuple[int, ...]#
- tcr_facing: str#
- presentation: str#
- tcren.refine.peptide_rmsd(model, reference, anchors=())[source]#
Peptide backbone / Cα / anchor-Cα RMSD of
modelvsreferencein the MHC-groove frame.Both structures must be chain-typed and MHC-annotated.
modelis superposed ontoreferenceby their shared groove Cα; the peptide RMSDs are then computed over atoms (and residues) present in both.anchorsare 0-based peptide residue indices for the anchor-Cα RMSD.- Parameters:
- Return type:
- class tcren.refine.PeptideRMSD(backbone_rmsd, ca_rmsd, anchor_ca_rmsd, n_backbone, n_ca, groove_rmsd)[source]#
Bases:
objectPeptide RMSD of a model against a reference, after MHC-groove superposition.
- Parameters:
backbone_rmsd (float)
ca_rmsd (float)
anchor_ca_rmsd (float)
n_backbone (int)
n_ca (int)
groove_rmsd (float)
- backbone_rmsd: float#
- ca_rmsd: float#
- anchor_ca_rmsd: float#
- n_backbone: int#
- n_ca: int#
- groove_rmsd: float#
- tcren.refine.check_register(model, reference=None, *, anchor_rmsd_cut=2.0, tolerance=0.4, top=8)[source]#
Assess whether
model’s peptide is clashing and/or in the wrong register.- Parameters:
model (Structure) – The generated complex to check (chain-typed; peptide
chain_type == 'PEPTIDE').reference (Structure | None) – A correctly-registered pose of the same complex (crystal / trusted model). When given, the anchor-Cα RMSD in the MHC-groove frame decides
wrong_register.anchor_rmsd_cut (float) – Anchor-Cα RMSD (Å) above which the register is called wrong.
tolerance (float) – vdW-overlap tolerance passed to
tcren.clashes.interface_clashes().top (int) – Worst clashing pairs to keep in the report.
- Returns:
- Return type:
- tcren.refine.fix_register(model, template, *, engine='ccd', seed=0, **engine_kwargs)[source]#
Correct a wrong-register model by re-threading its peptide onto
template’s register.The model’s peptide sequence is threaded onto the
template’s correctly-registered backbone (backbone + Cβ preserved) and re-refined by the chosen open-source engine, so the returned complex carries the model’s peptide in the template’s groove pose. This is the FlexPepDock-functional correction path (tcren.refine.model_peptide()); no PyRosetta.- Parameters:
model (Structure) – The wrong-register complex (source of the peptide sequence).
template (Structure) – A correctly-registered complex of equal peptide length (source of the pose), e.g. the crystal structure of the same clone.
engine (str) – Modelling engine (
ccd/doperun out of the box;openmm/promod3optional).seed (int) – RNG seed for engines that sample.
**engine_kwargs – Forwarded to the engine (e.g.
n_stepsfordope).
- Returns:
A
tcren.refine.engines.base.ModelResultwith the corrected structure, engine score, and anchors used.- Raises:
ValueError – If the model and template peptides differ in length (equal length is required for backbone-preserving substitution).
- class tcren.refine.RegisterReport(clashes, anchors, backbone_rmsd, anchor_rmsd, groove_rmsd, wrong_register, reason)[source]#
Bases:
objectRegister / forced-pose diagnostic for a generated TCR:pMHC complex.
- Variables:
clashes (tcren.clashes.ClashReport) – Interface steric-clash burden of the model (always computed).
anchors (tuple[int, ...]) – 0-based peptide anchor indices used for the anchor RMSD.
backbone_rmsd (float) – Peptide backbone RMSD vs the reference (Å);
nanwithout a reference.anchor_rmsd (float) – Anchor-Cα RMSD vs the reference (Å) — the register signal;
nanwithout one.groove_rmsd (float) – MHC-groove superposition residual (Å);
nanwithout a reference.wrong_register (bool | None) –
True/Falsewhen a reference is given (anchor RMSD over the cut),Nonewhen it cannot be determined (no reference).reason (str) – Human-readable explanation of the verdict.
- Parameters:
clashes (ClashReport)
anchors (tuple[int, ...])
backbone_rmsd (float)
anchor_rmsd (float)
groove_rmsd (float)
wrong_register (bool | None)
reason (str)
- clashes: ClashReport#
- anchors: tuple[int, ...]#
- backbone_rmsd: float#
- anchor_rmsd: float#
- groove_rmsd: float#
- wrong_register: bool | None#
- reason: str#
- property suspect: bool#
True if the pose is a wrong register, or (absent a reference) carries a severe clash.
tcren.refine.substitute module#
Backbone-preserving peptide substitution.
score_peptides scores a candidate peptide virtually (it re-indexes the potential matrix over
the native contact map — no atoms move). When you want to actually re-dock / refine a candidate you
first need its coordinates: substitute_peptide() threads an equal-length sequence onto the
existing peptide backbone, keeping N/Cα/C/O(+Cβ) and dropping the old side-chain atoms beyond Cβ
(a refiner / rotamer repack rebuilds them). Pure data-model manipulation; returns a new structure.
Alanine is the case this gets exactly right, and it matters. Alanine’s heavy atoms are N, Cα,
C, O and Cβ and no others, so a substitution to alanine needs no rotamer, no relaxation and no
choice: truncating at Cβ is the alanine. Every other target is left as a Cβ stub whose reach is
therefore under-stated, and needs a side-chain builder
(tcren.rotamers.repack() rotates existing atoms but cannot create them; ProMod3’s
ReconstructSidechains, wired up in tcren.refine.engines.promod3_engine, can).
Glycine has no Cβ to keep, so mutating a glycine to anything else needs one built.
virtual_cb() places it from N, Cα and C by ideal L-amino-acid geometry; measured against
1,679 real crystallographic Cβ atoms the construction lands a median 0.09 Å away (99th percentile
0.31 Å), which is far inside the 5 Å contact definition it feeds.
- tcren.refine.substitute.virtual_cb(n, ca, c)[source]#
Cβ position from the backbone, by ideal L-amino-acid geometry.
The standard construction: with
b = Cα − Nandc = C − Cα, the Cβ sits along a fixed combination ofb,cand their cross product, which fixes both the ~1.53 Å bond length and the tetrahedral chirality. Used only where there is no Cβ to keep — i.e. mutating away from glycine.- Parameters:
n (ndarray)
ca (ndarray)
c (ndarray)
- Return type:
ndarray
- tcren.refine.substitute.substitute_residues(structure, mutations)[source]#
Return a copy of
structurewith the named residues re-typed, in 3D.The general primitive behind every structural substitution: it moves no backbone, drops side-chain atoms past Cβ on the residues it touches, and leaves every other residue and every other chain byte-identical. Any chain may be targeted — this is what makes a receptor-side alanine scan possible, where
substitute_peptide()threads the peptide chain alone.- Parameters:
structure (Structure) – The structure to mutate.
mutations (Mapping[tuple[str, int], str]) –
(chain_id, residue.seq_index) -> one-letter target. An empty mapping returns the structure unchanged.
- Returns:
A new
Structure. Region markup on every touched chain is re-pointed at the rewritten residues, so the result goes straight into a contact map.- Raises:
ValueError – if a chain id or residue index is absent, or a code is non-standard.
- Return type:
- tcren.refine.substitute.substitute_peptide(structure, new_peptide, chain_type='PEPTIDE')[source]#
Return a copy of
structurewith the peptide chain threaded tonew_peptide.The peptide backbone (and Cβ) is preserved; side-chain atoms beyond Cβ are dropped (and Cβ too for any position mutated to glycine, while a position mutated from glycine has one built by
virtual_cb()).new_peptidemust equal the peptide length and use the 20 standard one-letter amino acids. Region markup is carried over onto the new residues, so the result can go straight into a contact map and be scored.For
new_peptideall-alanine this is exact, which is what makes a structural poly-alanine reference possible (tcren.ddg.reference_delta()with a structure). For other targets the result is a Cβ stub: correct in position, short in reach.
tcren.refine.register module#
Peptide register / forced-pose detection and correction.
A generated (AlphaFold / TCRmodel) TCR:pMHC complex can seat the peptide in the wrong register — anchors not in their MHC pockets, the peptide shifted along the groove — which corrupts the TCR-facing contacts the score reads. On the CPL benchmark this is the ila1 anomaly: the forced AF pose drives the raw TCRen ranking below chance (ROC 0.35), and re-seating the peptide on the correctly registered crystal recovers it (≈0.77).
check_register() flags such poses. It always reports the interface clash burden (via
tcren.clashes.interface_clashes()); when a correctly-registered reference (a crystal, or any
trusted pose of the same complex) is supplied it also measures the anchor-Cα RMSD in the MHC-groove
frame (tcren.refine.peptide_rmsd()) — the reliable register signal, since a mis-registered
peptide’s anchors land far from where they should sit. Note a heavy clash burden alone does not prove
a register error: AlphaFold peptide-swap models are routinely clashy, so register needs the reference.
fix_register() corrects a wrong-register model by re-threading its peptide sequence onto the
correctly-registered template backbone and re-refining through the open-source modelling path
(tcren.refine.model_peptide()) — the FlexPepDock-functional replacement, no PyRosetta.
- class tcren.refine.register.RegisterReport(clashes, anchors, backbone_rmsd, anchor_rmsd, groove_rmsd, wrong_register, reason)[source]#
Bases:
objectRegister / forced-pose diagnostic for a generated TCR:pMHC complex.
- Variables:
clashes (tcren.clashes.ClashReport) – Interface steric-clash burden of the model (always computed).
anchors (tuple[int, ...]) – 0-based peptide anchor indices used for the anchor RMSD.
backbone_rmsd (float) – Peptide backbone RMSD vs the reference (Å);
nanwithout a reference.anchor_rmsd (float) – Anchor-Cα RMSD vs the reference (Å) — the register signal;
nanwithout one.groove_rmsd (float) – MHC-groove superposition residual (Å);
nanwithout a reference.wrong_register (bool | None) –
True/Falsewhen a reference is given (anchor RMSD over the cut),Nonewhen it cannot be determined (no reference).reason (str) – Human-readable explanation of the verdict.
- Parameters:
clashes (ClashReport)
anchors (tuple[int, ...])
backbone_rmsd (float)
anchor_rmsd (float)
groove_rmsd (float)
wrong_register (bool | None)
reason (str)
- clashes: ClashReport#
- anchors: tuple[int, ...]#
- backbone_rmsd: float#
- anchor_rmsd: float#
- groove_rmsd: float#
- wrong_register: bool | None#
- reason: str#
- property suspect: bool#
True if the pose is a wrong register, or (absent a reference) carries a severe clash.
- tcren.refine.register.check_register(model, reference=None, *, anchor_rmsd_cut=2.0, tolerance=0.4, top=8)[source]#
Assess whether
model’s peptide is clashing and/or in the wrong register.- Parameters:
model (Structure) – The generated complex to check (chain-typed; peptide
chain_type == 'PEPTIDE').reference (Structure | None) – A correctly-registered pose of the same complex (crystal / trusted model). When given, the anchor-Cα RMSD in the MHC-groove frame decides
wrong_register.anchor_rmsd_cut (float) – Anchor-Cα RMSD (Å) above which the register is called wrong.
tolerance (float) – vdW-overlap tolerance passed to
tcren.clashes.interface_clashes().top (int) – Worst clashing pairs to keep in the report.
- Returns:
- Return type:
- tcren.refine.register.fix_register(model, template, *, engine='ccd', seed=0, **engine_kwargs)[source]#
Correct a wrong-register model by re-threading its peptide onto
template’s register.The model’s peptide sequence is threaded onto the
template’s correctly-registered backbone (backbone + Cβ preserved) and re-refined by the chosen open-source engine, so the returned complex carries the model’s peptide in the template’s groove pose. This is the FlexPepDock-functional correction path (tcren.refine.model_peptide()); no PyRosetta.- Parameters:
model (Structure) – The wrong-register complex (source of the peptide sequence).
template (Structure) – A correctly-registered complex of equal peptide length (source of the pose), e.g. the crystal structure of the same clone.
engine (str) – Modelling engine (
ccd/doperun out of the box;openmm/promod3optional).seed (int) – RNG seed for engines that sample.
**engine_kwargs – Forwarded to the engine (e.g.
n_stepsfordope).
- Returns:
A
tcren.refine.engines.base.ModelResultwith the corrected structure, engine score, and anchors used.- Raises:
ValueError – If the model and template peptides differ in length (equal length is required for backbone-preserving substitution).
tcren.refine.anchors module#
Peptide anchor prediction (which residues bury into the MHC groove).
Class I uses fixed positions counted from both termini (P2 + the C-terminal PΩ); class II has no fixed register, so a one-pass heuristic slides a 9-mer window and scores each register by a P1-hydrophobic / P4,P6,P9-not-Pro/Gly rule (a cheap proxy for GibbsCluster/NNAlign register inference), then reports P1/P4/P6/P9 of the best core. The anchor residues are the groove-facing (“presentation”) side; the rest of the peptide is TCR-facing.
This logic is portable and depends only on the standard library. It is lifted from the antigenomics
mhcmatch/seqtree decomposition primitives (which themselves carry no tcren dependency at
runtime); predict_anchors() adds a thin tcren wrapper that prefers the structure’s own MHC-class
call (from tcren.mhc) over the length heuristic when a typed structure is available.
- tcren.refine.anchors.infer_class(peptide)[source]#
Heuristic MHC class from peptide length (≤ 11 → class I, else class II).
- Parameters:
peptide (str)
- Return type:
str
- tcren.refine.anchors.anchor_indices(peptide, mhc_class)[source]#
0-based anchor positions: class-I P2/PΩ, class-II core P1/P4/P6/P9.
mhc_classin {MHCI, MHCII}.- Parameters:
peptide (str)
mhc_class (str)
- Return type:
tuple[int, …]
- class tcren.refine.anchors.Decomposition(peptide, mhc_class, anchors, tcr_facing, presentation)[source]#
Bases:
objectAnchor vs TCR-facing split of a peptide.
- Parameters:
peptide (str)
mhc_class (str)
anchors (tuple[int, ...])
tcr_facing (str)
presentation (str)
- peptide: str#
- mhc_class: str#
- anchors: tuple[int, ...]#
- tcr_facing: str#
- presentation: str#
- tcren.refine.anchors.decompose(peptide, mhc_class=None)[source]#
Split
peptideinto anchor (groove-facing) and TCR-facing parts.- Parameters:
peptide (str)
mhc_class (str | None)
- Return type:
- tcren.refine.anchors.predict_anchors(peptide, structure=None)[source]#
Predict peptide anchors, preferring the structure’s MHC-class call over the length heuristic.
Pass a chain-typed, MHC-annotated
structure(seetcren.mhc.annotate_mhc()) to use its real class assignment; otherwise the class is inferred from peptide length.- Parameters:
peptide (str)
structure (Structure | None)
- Return type:
tcren.refine.rmsd module#
Peptide RMSD between two poses of the same complex.
The benchmark question is “how close is a re-modelled peptide to its native crystal pose?” We answer it
the way the rest of the codebase measures geometry: superpose the two complexes on the conserved MHC
groove Cα (tcren.docking.align._matched_anchors() + Biopython’s SVDSuperimposer), then compute
the peptide backbone RMSD in that common MHC frame. Superposing on the MHC — not on the peptide —
means the peptide RMSD reflects how well the pose sits in the groove, which is the quantity that
matters for downstream TCR-contact scoring.
- class tcren.refine.rmsd.PeptideRMSD(backbone_rmsd, ca_rmsd, anchor_ca_rmsd, n_backbone, n_ca, groove_rmsd)[source]#
Bases:
objectPeptide RMSD of a model against a reference, after MHC-groove superposition.
- Parameters:
backbone_rmsd (float)
ca_rmsd (float)
anchor_ca_rmsd (float)
n_backbone (int)
n_ca (int)
groove_rmsd (float)
- backbone_rmsd: float#
- ca_rmsd: float#
- anchor_ca_rmsd: float#
- n_backbone: int#
- n_ca: int#
- groove_rmsd: float#
- tcren.refine.rmsd.peptide_rmsd(model, reference, anchors=())[source]#
Peptide backbone / Cα / anchor-Cα RMSD of
modelvsreferencein the MHC-groove frame.Both structures must be chain-typed and MHC-annotated.
modelis superposed ontoreferenceby their shared groove Cα; the peptide RMSDs are then computed over atoms (and residues) present in both.anchorsare 0-based peptide residue indices for the anchor-Cα RMSD.- Parameters:
- Return type:
tcren.refine.interface module#
Interface interaction energy — the ΔΔG core (native, DOPE, no PyRosetta).
interface_energy(structure) sums the DOPE atom-level statistical potential over peptide↔partner
heavy-atom pairs — the interaction energy across the TCR/MHC:peptide interface. Because only the cross
(peptide↔partner) terms are summed, it is already E_bound − E_separated for the interaction, i.e.
the interface ΔG contribution of the peptide. This is the quantity a sampling ΔΔG differences between a
mutant and the native (each first repacked/relaxed); see the _relax kernel roadmap in CPP_REWRITE.md.
Reuses the bundled DOPE table + atom-class map from tcren.refine (_dope), and the compiled
tcren._relax kernel.
- tcren.refine.interface.interface_energy(structure, *, shell=12.0)[source]#
DOPE interaction energy across the peptide↔partner interface (lower = more favourable).
The structure must be chain-typed (peptide =
chain_type == 'PEPTIDE'). Only partner heavy atoms withinshellÅ of the peptide are considered (beyond DOPE’s range they contribute nothing). Requires the compiledtcren._relaxext + the bundled DOPE table.- Parameters:
structure (Structure)
shell (float)
- Return type:
float
tcren.refine.model module#
Open-source peptide (re)modelling: substitute → predict anchors → model in the groove.
model_peptide() is the single entry point that replaces the licensed FlexPepDock/MODELLER path.
It threads a candidate peptide onto the groove backbone (tcren.refine.substitute_peptide()),
predicts which residues anchor into the MHC (tcren.refine.anchors.predict_anchors()), and hands
off to a modelling engine (dope and ccd run out of the box; openmm and promod3 are
optional). The returned ModelResult carries the refined structure,
an engine score, and the anchor set used.
- tcren.refine.model.model_peptide(structure, new_peptide=None, *, engine='ccd', seed=0, **engine_kwargs)[source]#
Model
new_peptideintostructure’s groove with the chosen engine.- Parameters:
structure (Structure) – A chain-typed, MHC-annotated complex (peptide chain
chain_type == 'PEPTIDE').new_peptide (str | None) – Candidate one-letter sequence (equal length to the native peptide).
Nonere-models the native sequence in place — the self-reconstruction case used by the benchmark.engine (str) – One of
ENGINES(dope,ccd,openmm,promod3).seed (int) – RNG seed (engines that sample).
**engine_kwargs – Forwarded to the engine’s
run(e.g.perturb/anchor_targetsforccd,n_stepsfordope).
- Returns:
A
ModelResultwith the refined structure, engine score, and anchors used.- Return type:
- class tcren.refine.model.ModelResult(structure, energy, engine, anchors, iterations=0, info=<factory>)[source]#
Bases:
objectOutcome of (re)modelling a peptide into the groove.
- Parameters:
structure (Structure)
energy (float)
engine (str)
anchors (tuple[int, ...])
iterations (int)
info (dict)
- energy: float#
- engine: str#
- anchors: tuple[int, ...]#
- iterations: int#
- info: dict#
tcren.refine.oracle_flexpep module#
FlexPepDock oracle — the accuracy ceiling the open engines are measured against.
Rosetta FlexPepDock is the reference-standard peptide refiner. It is a protocol inside Rosetta, so there are two ways to reach it:
PyRosetta API (preferred here) —
pyrosetta.rosetta.protocols.flexpep_docking. Installed viapyrosetta-installer(academic license). No external binary needed.External binary — a licensed
FlexPepDockingexecutable via$ROSETTA_BIN/rosetta_bin=.
Either way this is an oracle, never a shipped tcren dependency: the RMSD FlexPepDock achieves from a
displaced start is the lower bound on error the license-free engines (ccd/dope/openmm) should approach
(see CPP_REWRITE.md). Refinement is slow (tens of seconds to minutes per structure), so the oracle
column in fold_benchmark.py is meant for a subset, not the full sweep.
KNOWN ISSUE (2026-07): via the PyRosetta path below, FlexPepDockingProtocol().apply(pose) is a
no-op on our 5-chain TCR-pMHC complexes — the dumped pose equals the input (peptide RMSD Δ=0.000
vs the un-refined baseline across every test pair). FlexPepDock refine needs the receptor↔peptide jump /
FoldTree set up (the command-line app infers it from the last-chain-peptide convention; bare protocol
construction here does not). The oracle is therefore currently NON-FUNCTIONAL and its numbers must NOT
be reported as FlexPepDock accuracy. Fix by configuring the FlexPepDock FoldTree + -flexPepDocking
flags (or use the external binary path) before trusting the column.
- tcren.refine.oracle_flexpep.flexpep_available(rosetta_bin=None)[source]#
True if FlexPepDock is reachable (PyRosetta installed, or a binary resolvable).
- Parameters:
rosetta_bin (str | None)
- Return type:
bool
- tcren.refine.oracle_flexpep.flexpep_refine(structure, *, rosetta_bin=None, seed=0, nstruct=1, extra_flags=())[source]#
Refine the peptide pose with FlexPepDock; return the refined
Structure.Uses the PyRosetta API if installed, else a resolvable
FlexPepDockingbinary. The structure should carry full chains (imported withkeep_c_gene=True). RaisesRuntimeErrorif neither route is available.
tcren.refine.engines.base module#
Common types for peptide-modelling engines.
Every engine takes a chain-typed Structure whose peptide has already
been threaded to the candidate sequence (via tcren.refine.substitute_peptide()) plus the
predicted anchors, and returns a ModelResult: the refined structure, an engine-specific
energy/score, and bookkeeping. Engines that need an unavailable dependency raise
EngineUnavailable at call time (never at import), so import tcren always succeeds.
Bases:
RuntimeErrorRaised when an engine’s backend (OpenMM, ProMod3, …) is not installed.
- class tcren.refine.engines.base.ModelResult(structure, energy, engine, anchors, iterations=0, info=<factory>)[source]#
Bases:
objectOutcome of (re)modelling a peptide into the groove.
- Parameters:
structure (Structure)
energy (float)
engine (str)
anchors (tuple[int, ...])
iterations (int)
info (dict)
- energy: float#
- engine: str#
- anchors: tuple[int, ...]#
- iterations: int#
- info: dict#
- class tcren.refine.engines.base.Engine(*args, **kwargs)[source]#
Bases:
ProtocolA peptide-modelling backend.
- name: str#
- available()[source]#
True if the backend can run in this environment (cheap import probe, no side effects).
- Return type:
bool
- run(structure, decomp, *, seed=0)[source]#
Model the (already substituted) peptide; return the refined structure + score.
- Parameters:
structure (Structure)
decomp (Decomposition)
seed (int)
- Return type:
tcren.refine.engines.dope module#
DOPE rigid-body Monte-Carlo engine (wraps the existing tcren._refine kernel).
This is the engine that already ships: a knowledge-based rigid-body refine of the peptide pose against
its TCR+MHC partners under the DOPE statistical potential. It does not use the anchors (the whole
peptide moves as a rigid body), but it is the always-available baseline every other engine is compared
against. See tcren.refine.refine_peptide().
tcren.refine.engines.ccd module#
CCD anchor-restrained closure engine (wraps the tcren._fold C++ kernel).
Cyclic Coordinate Descent drives the peptide’s anchor Cα onto target positions (predicted MHC-groove
pocket centroids) while the rest of the backbone follows as a kinematic linkage. This is the
license-free geometric path: no Rosetta, no MODELLER, only the bundled stdlib-only _fold kernel.
Draft scope (ponytail: marked so the simplification reads as intent, not ignorance):
Operates on the Cα trace with consecutive-Cα rotatable bonds, and writes the closed pose back by rigid per-residue translation (each residue’s atoms shift by its Cα displacement). The kernel preserves Cα–Cα distances exactly, but because adjacent residues receive different translations the inter-residue peptide-bond geometry (C(i)–N(i+1) ≈ 1.33 Å, φ/ψ) is only approximate — intra- residue geometry is intact, the chain as a whole is a Cα-trace model, NOT a physically valid all-atom backbone. The output therefore MUST be followed by an energy refine (DOPE / OpenMM) to regularise the peptide bonds; the upgrade path is a full N–Cα–C kinematic chain + rotamer repack (OpenMM/ProMod3 do this). Do not treat the ccd output, on its own, as a finished structure.
Anchor targets must be supplied. In the self-reconstruction benchmark they are the native anchor Cα; predicting pocket centroids de novo (groove-pocket geometry) is the open piece flagged in STATUS.md and left to the scoring/orient layer.
- class tcren.refine.engines.ccd.CcdEngine[source]#
Bases:
object- name = 'ccd'#
- run(structure, decomp, *, seed=0, anchor_targets=None, perturb=0.0, max_iter=1000, tol=0.08)[source]#
- Parameters:
structure (Structure)
decomp (Decomposition)
seed (int)
anchor_targets (ndarray | None)
perturb (float)
max_iter (int)
tol (float)
- Return type:
tcren.refine.engines.openmm_engine module#
OpenMM anchor-restrained relaxation engine (physics, MIT-licensed, optional).
The open replacement for FlexPepDock’s physics relaxation: build an OpenMM system for the complex,
freeze the receptor (TCR + MHC) by zeroing masses, pin the anchor Cα to their target pockets with
harmonic restraints (the physics analog of MODELLER’s forms.gaussian distance restraints), and run
local energy minimisation. PDBFixer rebuilds the side chains that substitute_peptide() stripped,
so the output is a full-atom relaxed peptide (an improvement over the backbone-only threading).
Optional dependency: conda install -c conda-forge openmm pdbfixer. Raises
EngineUnavailable (never an ImportError at module import) when absent.
This engine is a reference/accuracy oracle for the native C++ rewrite (see CPP_REWRITE.md): its
relaxed pose and relative energies validate the compact _relax minimiser that will replace it in the
dependency-free path — OpenMM’s force field itself is not reimplemented.
- class tcren.refine.engines.openmm_engine.OpenMMEngine[source]#
Bases:
object- name = 'openmm'#
- run(structure, decomp, *, seed=0, anchor_targets=None, restraint_k=500.0, forcefield='amber14-all.xml', max_iter=0, tolerance=5.0)[source]#
- Parameters:
structure (Structure)
decomp (Decomposition)
seed (int)
anchor_targets (ndarray | None)
restraint_k (float)
forcefield (str)
max_iter (int)
tolerance (float)
- Return type:
tcren.refine.engines.promod3_engine module#
ProMod3 side-chain reconstruction engine (Apache-2.0, optional).
ProMod3 is the open (Apache-2.0, license-key-free) replacement for MODELLER. Its cleanest, most
directly useful capability for a groove peptide whose backbone is already placed (threaded or
CCD-closed) is rotamer side-chain reconstruction (modelling.ReconstructSidechains): it rebuilds
the side chains that substitute_peptide() strips, packed against the real receptor context with
ProMod3’s rotamer library — the SCWRL/packer role. Full backbone loop building (promod3.loop
CCD/KIC + fragments, the MODELLER-loopmodel analog) is the documented upgrade; repack is the piece that
works via a stable API and complements the geometric (ccd) and physics (openmm) engines.
Optional dependency: conda install -c bioconda openstructure promod3. Raises
EngineUnavailable (never an ImportError at import) when absent. Reference oracle for the native
C++ rewrite (see CPP_REWRITE.md): validates a future native rotamer packer.
Data paths, metadata and provenance#
tcren.paths module#
Filesystem locations for tcren’s reference data.
The library’s runtime dataset lives under tcren_home() — the source checkout when
tcren is run from one, $TCREN_HOME when set, and a user cache directory otherwise. It holds
the canonical Native2026 structure set (HF isalgo/tcren_structures, gitignored),
PDB_date.tsv and the built MHC allele reference. Structures are fetched lazily; nothing here
is bundled into the installed package, except Canonical2026’s orient_metadata.json, which
rides in tcren/data/ so an installed superimpose can describe the database it fetched.
- tcren.paths.tcren_home()[source]#
Root of tcren’s on-disk reference data.
$TCREN_HOMEwhen set; otherwise the source checkout, recognised by itspyproject.toml. An installed wheel has no checkout above it —parents[2]is thensite-packages’ parent — so it falls back to a user cache directory, which is both writable and stable across upgrades.- Return type:
Path
- tcren.paths.data_dir()[source]#
Root of the runtime dataset:
$TCREN_DATA_DIRordata/undertcren_home().- Return type:
Path
- tcren.paths.native_dir()[source]#
Directory holding the canonical
Native2026structures (data/Native2026).- Return type:
Path
- tcren.paths.reference_structure_path(pdb_id)[source]#
Resolve a canonical reference structure by id (plain/gzipped PDB/mmCIF).
Looks under
data/Native2026first; if absent (e.g. a pip-installed library with no repodata/), lazily downloads it from the HF dataset into the HF cache. This makes orienting a new, non-canonical structure work out of the box for both the library and CLI.Raises
FileNotFoundErrorif it is neither local nor fetchable.- Parameters:
pdb_id (str)
- Return type:
Path
tcren.metadata module#
Per-structure metadata that travels with a structure set.
A structure set is a directory (or archive) of TCR-pMHC complexes. Everything a descriptor cannot
be computed from — the binding label, the epitope and allele, and above all the generator’s own
confidence (iptm, plddt, ranking_confidence) — lives beside the structures in a
metadata.tsv, keyed by the same id tcren features writes into complex.id.
The rule this enforces: a set that ships models ships their confidences. Without it every analysis has to rediscover where the confidences went, and they went somewhere different for each set — which is how a benchmark ends up joined on a hash that is not unique.
Layout, beside the structures:
<set>/
metadata.tsv id + whatever is known
1ao7.pdb.gz
...
id matches tcren.structure.structure_stem() — the file stem with structure suffixes
removed — so a table produced by tcren features joins on complex.id with no massaging.
Reserved column names, all optional except id:
|
structure stem; the join key. Required. |
|
binding label, 1/0, when the set carries one |
|
peptide sequence |
|
allele, e.g. |
|
AlphaFold/TCRmodel2 interface pTM |
|
mean pLDDT |
|
pTM |
|
the generator’s own ranking scalar |
|
free text: what produced this structure |
Any further column is passed through untouched.
- tcren.metadata.find_metadata(structures)[source]#
The
metadata.tsvgoverningstructures, orNone.structuresmay be the set directory, a file inside it, or an archive; the search walks up from the path so that pointing at one structure still finds its set’s table.- Parameters:
structures (str | Path)
- Return type:
Path | None
- tcren.metadata.read_metadata(structures)[source]#
Read the set’s metadata table, or
Noneif it has none.- Raises:
ValueError – if the file exists but has no
idcolumn, which would make it unjoinable.- Parameters:
structures (str | Path)
- Return type:
DataFrame | None
- tcren.metadata.join_metadata(table, structures, *, on='complex.id', columns=None)[source]#
Left-join a set’s metadata onto a feature table.
- Parameters:
table (DataFrame) – a
tcren featuresoutput, or anything keyed byon.structures (str | Path) – the structure set, so its
metadata.tsvcan be found.on (str) – the key column in
table. Defaultcomplex.id.columns (tuple[str, ...] | None) – restrict to these metadata columns (
idis always kept).Nonetakes all.
- Returns:
tablewith the metadata columns appended. Unchanged if the set has no metadata, so this is safe to call unconditionally.- Return type:
DataFrame
tcren.provenance module#
Provenance stamps for generated tables, and the check that a table is not stale.
The failure this exists to prevent: a feature or score table written by an older tcren is read back months later, a number is quoted from it, and nothing anywhere says the descriptor that produced it has since been renamed, redefined or removed. The number is then wrong and irreproducible, and the only symptom is that it does not match a fresh run.
stamp() writes a sidecar JSON beside every table a command emits. check() reads it back
and raises unless the installed package would produce the same columns from the same registry.
The registry digest is the load-bearing part: it changes whenever any descriptor is added, dropped,
or has its units or definition edited, so a table computed under a different catalogue cannot pass
silently.
- tcren.provenance.registry_digest()[source]#
SHA-256 over the descriptor catalogue: names, families, invariance classes, units.
Definitions are included, so editing what a descriptor means invalidates every table that carries it even when the number would not change. That is deliberate – a redefined column is a different quantity under the same name, which is the case a value comparison cannot catch.
- Return type:
str
- tcren.provenance.sidecar_path(table)[source]#
Where the stamp for
tablelives: the table’s path with.provenance.jsonappended.- Parameters:
table (str | Path)
- Return type:
Path
- tcren.provenance.stamp(table, *, command, columns=None, extra=None)[source]#
Write the sidecar for
tableand return its path.- Parameters:
table (str | Path) – the file just written.
command (str) – the invocation that produced it, verbatim enough to re-run.
columns – the table’s column names, or
Noneto omit them.extra (dict | None) – anything else worth recording (structure count, potentials, cutoff).
- Return type:
Path
- tcren.provenance.read(table)[source]#
The stamp for
table, orNoneif it has none.- Parameters:
table (str | Path)
- Return type:
dict | None
- tcren.provenance.check(table, *, require=True)[source]#
Raise
StaleTableErrorunlesstablewas written by this catalogue.- Parameters:
table (str | Path) – the table about to be read.
require (bool) – raise when the stamp is missing entirely.
True(default) is right for anything whose numbers will be reported – an unstamped table is one written before stamping existed, which is exactly the era this guard is aimed at.
- Returns:
The stamp.
- Return type:
dict
Analysis#
tcren.analysis module#
Dataset-level analyses for the TCRen contact statistics and potentials.
Helpers for the analysis notebook / benchmarks, following the TCRen manuscript logic:
potential heatmaps and comparisons, the distribution of TCR↔peptide contacts per
structure and per region, and how contacts distribute over peptide / CDR3 positions as a
function of peptide / CDR3 length. They take the manuscript contact + summary tables as
explicit paths (the committed oracle lives under tests/assets/oracle/).
- tcren.analysis.load_interface_contacts(contact_maps, summary)[source]#
Load and enrich the manuscript TCR↔peptide contact table.
Adds, per contact:
peptide_pos(0-based peptide position =residue.index.to),peptide_len,cdr3_len(CDR3α/β length for the contacting TCR chain),cdr3_rel_pos(residue index relative to the chain’s first contacting CDR3 residue — a relative position, since the committed table carries no region start), and thenonredflag.- Parameters:
contact_maps (str | Path)
summary (str | Path)
- Return type:
DataFrame
- tcren.analysis.contacts_per_structure(df, nonred_only=True)[source]#
Number of TCR↔peptide contacts per structure (with TRA/TRB split).
- Parameters:
df (DataFrame)
nonred_only (bool)
- Return type:
DataFrame
- tcren.analysis.region_contact_counts(df, nonred_only=True)[source]#
Total contacts by TCR region (CDR1/2/3, FR) and chain (TRA/TRB).
- Parameters:
df (DataFrame)
nonred_only (bool)
- Return type:
DataFrame
- tcren.analysis.position_distribution(df, side='peptide', nonred_only=True)[source]#
Contact counts by position, stratified by chain/molecule length.
- Parameters:
df (DataFrame) – enriched contacts (see
load_interface_contacts()).side (str) –
"peptide"(peptide position vs peptide length) or"cdr3a"/"cdr3b"(relative CDR3 position vs CDR3 length, for that TCR chain).nonred_only (bool) – restrict to non-redundant structures.
- Returns:
length, position, n_contacts.- Return type:
Long counts
- tcren.analysis.potential_long(potential)[source]#
Heatmap-ready long form of a potential:
residue.aa.from, residue.aa.to, value.- Parameters:
potential (Potential)
- Return type:
DataFrame
2D projection and visualization#
tcren.project2d.frame module#
Project an oriented TCR-peptide-MHC interface onto a single 2D plane.
The “optimal plane” is the MHC groove plane. Two routes produce it:
native (default) — orient the structure onto a canonical reference via
tcren.docking.align.align_to_native(); the canonical groove plane is xy and its normal is z, so projecting is just dropping z. The transform is applied only to the extracted Cα coordinates (apply_transformwould clear region annotations).pca (fallback) — fit a plane to the groove-floor Cα by SVD when no native database is available. The normal is the lowest-variance axis; z is sign-oriented toward the peptide.
The residues projected are the CDR1-3 loops (TRA/TRB), the peptide, and the MHC groove helices/floor — the residues that line the interface.
- class tcren.project2d.frame.ProjectionResult(keys, coords3d, frame, reference_id=None, rmsd=None)[source]#
Bases:
object2D projection of selected interface residues onto the groove plane.
- Parameters:
keys (list[tuple[str, int]])
coords3d (ndarray)
frame (Literal['native', 'pca'])
reference_id (str | None)
rmsd (float | None)
- keys: list[tuple[str, int]]#
- coords3d: ndarray#
- frame: Literal['native', 'pca']#
- reference_id: str | None#
- rmsd: float | None#
- property uv: ndarray#
In-plane (u, v) coordinates.
- property height: ndarray#
Signed distance above the groove plane (z).
- tcren.project2d.frame.project_structure(structure, reference_id=None, force_pca=False)[source]#
Project the interface residues of an annotated structure onto the groove plane.
- Parameters:
structure (Structure) – A chain-typed, MHC-annotated structure.
reference_id (str | None) – Canonical reference complex id (defaults per MHC class).
force_pca (bool) – Skip the native frame and fit the plane by PCA.
- Returns:
- Return type:
tcren.project2d.tables module#
Canonical polars tables for complementarity maps: residue markup + contacts.
Both tables are tidy and keyed for joins on (structure_id, structure_chain, aa_index)
where aa_index is the 0-based position in the chain’s protein sequence (the legacy
mir residue.index); residue_index is the author/PDB numbering (a display label).
The contacts table wraps the parity-preserving all_atom_contacts(), so it is
compatible with the original TCRen contact calculation.
- tcren.project2d.tables.residue_markup_table(structure, projection=None)[source]#
Per-residue markup with chain/region annotation and groove-frame coordinates.
- Parameters:
structure (Structure) – A chain-typed, annotated structure.
projection (ProjectionResult | None) – Optional
ProjectionResult; its groove-frame coordinates are joined on(chain_id, seq_index)(x/y/zand in-planeu/v/height).
- Returns:
structure_id, structure_chain, complex_chain, complex_region, residue_index, aa_index, aa_len, aa, x, y, z, u, v, height.- Return type:
Columns
- tcren.project2d.tables.ca_contacts_table(structure, threshold=8.0)[source]#
Inter-chain Cα–Cα “chain contacts” within
thresholdÅ (bold edges in the map).Columns:
structure_id, structure_chain_1, structure_chain_2, aa_index_1, aa_index_2, ca_dist. Complements the all-atomcontacts_table()(which gives the dashed inter-residue edges).- Parameters:
structure (Structure)
threshold (float)
- Return type:
DataFrame
- tcren.project2d.tables.classify_contact(aa1, aa2, atom1, atom2, dist)[source]#
Classify a residue–residue contact from its closest atom pair.
Delegates to
tcren.contact_typesso the map and the contact tables agree. There used to be two independent classifiers with the same vocabulary and different rules: this one had no distance bound onaromaticorhydrophobicat all, so every C–C pair out to the 5 Å map cutoff read as hydrophobic.Returns one of
salt_bridge,hydrogen_bond,cation_pi,aromatic,hydrophobic,polar,other.stackingcannot appear here: it needs ring geometry, which this signature does not carry.- Parameters:
aa1 (str)
aa2 (str)
atom1 (str)
atom2 (str)
dist (float)
- Return type:
str
- tcren.project2d.tables.contacts_table(structure, threshold=5.0)[source]#
Inter-chain residue contacts within
thresholdÅ, classified by bond type.- Parameters:
structure (Structure) – The structure.
threshold (float) – Distance cutoff in Å (
3 ≤ threshold ≤ 6, default 5 — the original TCRen contact distance).
- Returns:
structure_id, structure_chain_1, structure_chain_2, residue_index_1, residue_index_2, aa_index_1, aa_index_2, min_dist, contact_type, backbone_1, backbone_2. One row per unordered inter-chain residue pair (TCRen-compatible).- Return type:
Columns
- tcren.project2d.tables.region_pair_contacts(structure, kind='closest', cutoff=None)[source]#
Inter-chain residue contacts annotated with the region pair they bridge.
kindselects the contact definition:"closest"(closest heavy-atom pair,all_atom_contacts(), default 5 Å — the original TCRen definition, and the only kind that carries acontact_typebond classification),"cb"(Cβ representative atom, default 8 Å), or"ca"(Cα representative atom, default 12 Å). Region labels are ordered canonically per row so a pair is direction-independent (cdr3``↔``peptide==peptide``↔``cdr3).Returns columns:
structure_id, complex_chain_1, region_1, complex_chain_2, region_2, aa_index_1, aa_index_2, min_distplus, forkind="closest",contact_type.- Parameters:
structure (Structure)
kind (str)
cutoff (float | None)
- Return type:
DataFrame
- tcren.project2d.tables.region_pair_summary(structure, kind='closest', cutoff=None)[source]#
Per region-pair contact counts (+ bond-type breakdown for
kind="closest").Aggregates
region_pair_contacts()to one row per(region_1, region_2)pair withn_contacts. Forkind="closest"it adds a column per bond type (onen_<type>pertcren.contact_types.TYPES_V2entry barstacking), so hydrogen bonds (and the rest) are reported for every region pair in the complex, not just the MHC interface.- Parameters:
structure (Structure)
kind (str)
cutoff (float | None)
- Return type:
DataFrame
tcren.project2d.pockets module#
Approximate MHC peptide-binding pocket (A–F) markers along the peptide track.
The six class-I pockets (A–F) line the groove from the peptide N-terminus (A ≈ P1) to its C-terminus (F ≈ P-Ω). Without explicit pocket geometry we place A–F evenly along the projected peptide Cα track — a labelling aid for the 2D map and 3D view, not a structural pocket definition.
tcren.viz.svg2d module#
Direct SVG builder for 2D complementarity maps.
Renders projected interface residues as squares (AA + number), Cα–Cα “chain contacts” as
bold lines, and closest-atom inter-residue contacts as dashed lines. Every element carries
its data as data-* attributes plus a <title> tooltip, so the SVG is both a figure
and a queryable, metadata-bearing artifact. Pure string building — no dependencies.
- tcren.viz.svg2d.render_complementarity_map(markup, contacts=None, ca_contacts=None, pockets=None, show_chains=None, draw_backbone=True, width=900, height=700, margin=60.0)[source]#
Render a complementarity map to an SVG string.
- Parameters:
markup (DataFrame) – residue markup table (needs
u,v; non-null rows are drawn).contacts (DataFrame | None) – closest-atom inter-residue contacts (dashed) with
structure_chain_1/2,aa_index_1/2,min_dist,contact_type.ca_contacts (DataFrame | None) – Cα–Cα chain contacts (bold) with
structure_chain_1/2,aa_index_1/2,ca_dist.pockets (DataFrame | None) – optional A–F pocket markers with
pocket,u,v.show_chains (list[str] | None) – if given, only draw residues whose
complex_chainis in this set (e.g.["tra", "trb", "peptide"]to hide MHC). Contacts/edges are kept only between shown residues.draw_backbone (bool) – connect consecutive residues within each chain (sequence-adjacent
aa_index) with a thin backbone trace.width (int) – canvas geometry.
height (int) – canvas geometry.
margin (float) – canvas geometry.
- Returns:
SVG markup (string).
- Return type:
str
tcren.viz.surface2d module#
Direct SVG builder for pMHC surface-topology maps (tcren.surface).
One <rect> per grid cell, coloured by the channel being shown, with the cell’s height,
chemistry and owning region carried as data-* attributes and a <title> tooltip — same
contract as tcren.viz.svg2d: the SVG is a figure and a queryable artifact. Pure string
building, no dependencies.
Two ramps, chosen by what the channel means rather than by taste. h is a magnitude above the
groove floor, so it gets a sequential viridis-like ramp. phobic and charge have a
meaningful zero (neutral / uncharged), so they get a diverging ramp centred on zero, not on
the data range — a range-fitted diverging ramp paints the least-hydrophobic cell of an all-greasy
surface as if it were hydrophilic.
- tcren.viz.surface2d.SIGNED_CHANNELS = ('phobic', 'charge')#
Channels whose zero is meaningful, so the ramp is centred rather than range-fitted.
- tcren.viz.surface2d.render_surface_map(smap, channel='h', *, width=520, height=760, margin=56.0, outline_source=True, vmin=None, vmax=None, title=None)[source]#
Render a
tcren.surface.SurfaceMapchannel to an SVG string.- Parameters:
smap – the map to draw.
channel (str) – which channel —
"h","phobic"or"charge".width (int) – canvas geometry in px. The default is portrait because the groove axis (y, peptide N→C) is the long one.
height (int) – canvas geometry in px. The default is portrait because the groove axis (y, peptide N→C) is the long one.
margin (float) – canvas geometry in px. The default is portrait because the groove axis (y, peptide N→C) is the long one.
outline_source (bool) – stroke each cell in its source region’s colour (peptide vs the two helices), so the peptide’s footprint is legible on top of the value ramp.
vmin (float | None) – fix the colour range instead of taking the data’s 2nd/98th percentile. Pass the same values across a set of maps to make the figures directly comparable.
vmax (float | None) – fix the colour range instead of taking the data’s 2nd/98th percentile. Pass the same values across a set of maps to make the figures directly comparable.
title (str | None) – caption; defaults to
"<structure id> <peptide>".
- Returns:
SVG markup (string).
- Raises:
KeyError – if
channelis not one of the map’s channels.- Return type:
str
tcren.viz.pocket3d module#
Interactive 3D peptide-pocket view with CDR1-3 overlay (py3Dmol / 3Dmol.js).
Renders the MHC groove (cartoon + optional translucent surface, histo.fyi style), the
peptide as sticks, and the TCR CDR1-3 loops as Cα traces coloured by the shared palette,
all in the canonical (oriented) frame. py3Dmol is imported lazily so the rest of the
package has no hard 3D dependency.
- tcren.viz.pocket3d.view_pocket_cdr(structure, reference_id=None, surface=True, width=700, height=500)[source]#
Build a py3Dmol view of the groove + peptide + CDR loops (oriented frame).
- Parameters:
structure (Structure) – a chain-typed, MHC-annotated structure.
reference_id (str | None) – native reference for orientation (identity frame if unavailable).
surface (bool) – draw a translucent groove surface.
width (int) – viewer size.
height (int) – viewer size.
- Returns:
A
py3Dmol.view(call.show()in a notebook).
tcren.viz.pymol module#
Publication-ready PyMOL renders of canonically-oriented TCR–pMHC complexes.
A render of an oriented structure is only interpretable if the reader can tell which way the canonical frame points, so the centrepiece here is the axis gizmo — a thin, arrow-headed triad in a corner of the image, turning with the camera, naming what each direction means.
The frame it draws. tcren.docking.frame puts every structure into one frame by PCA of the
reference complex, and CANONICAL_AXES is that frame written out in words:
axis |
definition in code |
figure label |
equivalent in the literature |
|---|---|---|---|
|
PC3, the thin axis |
|
|
|
PC2, +y to peptide C-term |
|
|
|
PC1, +z toward the TCR |
|
|
The principal-component numbers differ from the docking-geometry literature (SwiftTCR, TCR3d)
because those fit the MHC groove alone while tcren.docking.frame fits the whole complex, in
which the MHC→TCR direction carries the most variance. The three directions are the same three;
only their ranking differs. Naming them for what they are is the point — pMHC→TCR is readable
in a figure, z is not.
How the gizmo is placed. In its own render pass, not by projecting a world position into the
corner. PyMOL’s orthoscopic viewport does not span the world height that field_of_view and the
camera distance imply — measured on a real scene it is out by about a quarter — so a gizmo placed
by that arithmetic lands off-frame. Rendering the triad alone on a transparent background and
compositing it at pixel coordinates makes its size and position exact by construction, and leaves
nothing to reverse-engineer. It also guarantees the molecule can never occlude it.
Why the geometry lives here. PyMOL runs under its own interpreter and cannot import tcren,
so the scripts this module emits are lists of literals and every decision behind them is ordinary
testable Python.
Example
>>> from tcren.viz.pymol import render, overlay_scene
>>> render(overlay_scene(["1ao7"], "data/Canonical2026"), "overlay.png")
- class tcren.viz.pymol.Axis(letter, label, short, definition)[source]#
Bases:
objectOne canonical axis: its letter, what it means, and how to label it in a figure.
- Parameters:
letter (str)
label (str)
short (str)
definition (str)
- letter: str#
- label: str#
- short: str#
- definition: str#
- tcren.viz.pymol.CANONICAL_AXES: tuple[Axis, Axis, Axis] = (Axis(letter='x', label='groove width', short='width', definition='PC3, the thin axis; across the cleft, α1↔α2 helix separation'), Axis(letter='y', label='peptide N→C', short='N→C', definition='PC2, the groove/peptide axis, signed toward the peptide C-terminus'), Axis(letter='z', label='pMHC→TCR', short='TCR', definition='PC1, the MHC→TCR long axis, signed toward the TCR; the MHC sits at −z'))#
The canonical frame of
tcren.docking.frame, in figure-ready words.shortis for the corner gizmo, where anything longer than a few characters collides with the arrows.
- tcren.viz.pymol.CORNERS: dict[str, tuple[int, int]] = {'bottom-left': (-1, -1), 'bottom-right': (1, -1), 'top-left': (-1, 1), 'top-right': (1, 1)}#
Corner anchors, as the sign of (x, y) in image space with y up.
- tcren.viz.pymol.PALETTES: dict[str, tuple[tuple[float, float, float], ...]] = {'mono': ((0.2, 0.2, 0.2), (0.2, 0.2, 0.2), (0.2, 0.2, 0.2)), 'okabe-ito': ((0.835, 0.369, 0.0), (0.0, 0.62, 0.451), (0.0, 0.447, 0.698))}#
mono— one restrained grey for all three arrows, told apart by their labels. The default, because the structure already spends colour on chains and an orientation gizmo that competes with the molecule makes a worse figure.okabe-itois the colourblind-safe triad for when the axes themselves are the subject.
- tcren.viz.pymol.CHAIN_COLOURS: dict[str, tuple[str, str]] = {'A': ('Vα', 'marine'), 'B': ('Vβ', 'orange'), 'C': ('peptide', 'yellow'), 'D': ('MHC α', 'grey70'), 'E': ('MHC β / β2m', 'grey60')}#
Chain roles after
tcren orient, and the colours used throughout these scenes.
- tcren.viz.pymol.gizmo_cgo(*, arm=10.0, radius=0.3, head_length=0.3, head_radius=2.8, palette='mono')[source]#
The axis triad as CGO, at the world origin, in world units.
Absolute size does not matter: the triad is rendered on its own and scaled to the gizmo tile, so only the proportions here are visible. What they control is how the figure reads — a thin shaft with a distinct arrowhead, rather than the fat default axes that dominate a panel.
- Parameters:
arm (float) – Arrow length.
radius (float) – Shaft radius. Thin relative to
armis the intent.head_length (float) – Arrowhead length as a fraction of
arm.head_radius (float) – Arrowhead base radius as a multiple of
radius.palette (str) – A key of
PALETTES.
- Returns:
(cgo, tips)— the flat CGO float list, and the three arrow tips, where labels go.- Raises:
ValueError – If
paletteis unknown, or a size is non-positive.- Return type:
tuple[list[float], list[tuple[float, float, float]]]
Example
>>> cgo, tips = gizmo_cgo() >>> tips[1] (0.0, 10.0, 0.0)
- tcren.viz.pymol.label_points(rotation, tips, *, offset=1.42, head_on=0.25)[source]#
Where each axis label goes, pushed clear of its arrow in screen space.
Anchoring a label to its tip in world coordinates fails exactly where it matters: an axis pointing at the viewer foreshortens to a dot, and its label lands on top of the origin and the other two labels. Pushing outward along the axis’s projected direction keeps every label clear whatever the camera does, and an axis too head-on to have a projected direction is pushed down-left instead, where it reads as belonging to the dot at the origin.
- Parameters:
rotation – The nine floats of the camera rotation, column-major as PyMOL stores them.
tips – The three arrow tips from
gizmo_cgo().offset (float) – How far past the tip to sit, as a multiple of the arm length.
head_on (float) – Projected length below which an axis counts as pointing at the viewer.
- Returns:
Three world-space points, rounded for embedding in a script.
Example
>>> pts = label_points([1,0,0, 0,1,0, 0,0,1], [(10,0,0), (0,10,0), (0,0,10)]) >>> pts[0][0] > 10 # pushed out past the tip True
- tcren.viz.pymol.gizmo_scene(rotation, *, axes=(Axis(letter='x', label='groove width', short='width', definition='PC3, the thin axis; across the cleft, α1↔α2 helix separation'), Axis(letter='y', label='peptide N→C', short='N→C', definition='PC2, the groove/peptide axis, signed toward the peptide C-terminus'), Axis(letter='z', label='pMHC→TCR', short='TCR', definition='PC1, the MHC→TCR long axis, signed toward the TCR; the MHC sits at −z')), short_labels=True, label_size=26.0, label_colour='gray20', label_offset=1.42, **cgo_kwargs)[source]#
A PyMOL scene holding only the triad, seen under
rotation.- Parameters:
rotation (list[float]) – The first nine floats of the main scene’s
cmd.get_view()— the same camera rotation, so the triad reports the orientation the molecule is actually drawn in.axes (tuple[Axis, Axis, Axis]) – The axes to draw, in x, y, z order.
short_labels (bool) – Use
Axis.short(fits a small tile) rather thanAxis.label.label_size (float) – PyMOL
label_size. Large, because the tile is later scaled down.label_colour (str) – Any PyMOL colour name.
label_offset (float) – Where the label sits along the arm, as a multiple of its length.
**cgo_kwargs – Forwarded to
gizmo_cgo().
- Returns:
A PyMOL script body. Labels are pseudoatoms because CGO has no text primitive; they ray-trace like any other label.
- Raises:
ValueError – If
rotationis not nine floats.- Return type:
str
- tcren.viz.pymol.probe_rotation(scene, *, pymol_bin=None)[source]#
Run
sceneand report the camera rotation it ends up with.The gizmo must be drawn under the same rotation as the molecule, and that is not known until the scene has loaded, turned and zoomed —
cmd.zoomleaves the rotation alone butcmd.turnandcmd.orientdo not, so reading it back beats assuming it.- Parameters:
scene (str) – A PyMOL script body that loads structures and sets the view.
pymol_bin (str | None) – Override the
pymolexecutable.
- Returns:
The first nine floats of
cmd.get_view().- Return type:
list[float]
- tcren.viz.pymol.render(scene, png, *, size=(1200, 1200), dpi=300, gizmo=True, corner='bottom-left', gizmo_scale=0.19, gizmo_margin=0.025, pymol_bin=None, **gizmo_kwargs)[source]#
Ray-trace
scenetopng, with the canonical-frame gizmo in a corner.- Parameters:
scene (str) – A PyMOL script body: load structures, style them, set and zoom the view.
png (str | Path) – Output path.
size (tuple[int, int]) –
(width, height)in pixels. 1200 px at 300 dpi is a 4-inch figure panel.dpi (int) – Written into the PNG so a document places it at a known physical size.
gizmo (bool) – Draw the axis triad. Turn it off where the frame is not the point.
corner (str) – Which corner the gizmo goes in; a key of
CORNERS.gizmo_scale (float) – Gizmo tile width as a fraction of the image width.
gizmo_margin (float) – Gizmo inset from the edges, as a fraction of the image width.
pymol_bin (str | None) – Override the
pymolexecutable.**gizmo_kwargs – Forwarded to
gizmo_scene()/gizmo_cgo().
- Returns:
The path written.
- Return type:
Path
Example
>>> render('cmd.load("1ao7.pdb")\ncmd.show("cartoon")', "fig.png")
- tcren.viz.pymol.composite(base_png, tile_png, out_png, *, corner='bottom-left', scale=0.19, margin=0.025)[source]#
Paste the gizmo tile into a corner of the render, preserving alpha.
- Parameters:
base_png – The molecule render.
tile_png – The gizmo render, transparent outside the arrows.
out_png – Where to write. May be
base_png.corner (str) – A key of
CORNERS.scale (float) – Tile width as a fraction of the base image width.
margin (float) – Inset from the edges, as a fraction of the base image width.
- Returns:
The path written.
- Raises:
ValueError – If
corneris unknown orscale/marginleave no room.
- tcren.viz.pymol.overlay_scene(ids, canon_dir, *, limit=8, transparency=0.55)[source]#
Superpose a set of oriented structures, seen side-on.
- Parameters:
ids – PDB ids present in
canon_diras<id>.pdb.gz.canon_dir – The canonical (oriented) structure directory.
limit (int) – Draw at most this many; past roughly eight the overlay stops being readable.
transparency (float) – Cartoon transparency, so the spread of the ensemble shows through.
- Returns:
A PyMOL scene body for
render().- Return type:
str
- tcren.viz.pymol.groove_scene(pid, canon_dir, *, surface=False)[source]#
One complex from above the groove: peptide as sticks in the MHC cleft.
The layout histo.fyi uses for its structure pages — a pale MHC with the peptide threaded along the cleft, which is the most legible way to show what is presented.
- Parameters:
pid – PDB id.
canon_dir – The canonical (oriented) structure directory.
surface (bool) – Add a translucent molecular surface over the MHC ribbon.
- Returns:
A PyMOL scene body for
render().- Return type:
str
- tcren.viz.pymol.interface_scene(pid, canon_dir, cdr_resi)[source]#
The recognition interface: peptide plus the CDR loops that touch it.
- Parameters:
pid – PDB id.
canon_dir – The canonical (oriented) structure directory.
cdr_resi –
{"TRA": [...], "TRB": [...]}PDB residue numbers, as from annotating the pre-orientation structure — orientation preserves numbering.
- Returns:
A PyMOL scene body for
render().- Return type:
str
- tcren.viz.pymol.residue_importance(structure, *, interface='tcr_peptide', cutoff=5.0, potential=None, tcr_regions='all')[source]#
Per-residue share of the interface, on both the physics and the geometry axis.
The interface energy Φ is a sum over residue–residue contacts, so it decomposes exactly: each residue’s share is the sum of
φ(aa_i, aa_j)over the contacts it makes across the interface. That is the quantity to colour a figure by — it says which residues the score is actually made of, where the total only says how large it is.Two columns come back because they answer different questions.
phiis the energy share, negative being favourable;n_contactsis the geometric share, how much of the interface the residue physically occupies. A residue can be large on one and small on the other, and that difference is usually the interesting part.- Parameters:
structure – An annotated structure (chains typed, MHC called).
interface (str) – Which interface to decompose —
tcr_peptide,tcr_mhcorpeptide_mhc.cutoff (float) – Heavy-atom contact distance (Å).
potential – A
tcren.potential.Potential; defaults to the bundled TCRen fortcr_peptideand MJ for the MHC interfaces, matchingtcren.pipeline.tcr_regions (str) – Which TCR regions count —
all,cdrorcdr+fr.
- Returns:
chain.id,residue.index,residue.aa,region.type,n_contacts,phi. One row per residue that touches the interface, either side of it, sorted most-favourable first.- Return type:
A polars frame
Example
>>> residue_importance(s).head(3)
- tcren.viz.pymol.importance_scene(pid, canon_dir, importance, *, by='phi', regions=('CDR3', 'PEPTIDE'), spectrum='blue_white_red')[source]#
Colour the recognition interface by each residue’s share of it.
The residues that carry the score are shown as sticks on a colour ramp; everything else stays pale so the eye goes to the ramp. Values ride in on the B-factor column, which is what PyMOL’s
spectrumreads.The ramp is centred on zero when colouring by
phi, so blue and red mean favourable and unfavourable rather than merely “less” and “more” — a ramp fitted to the observed range would paint the least-favourable residue red even in an interface where every contact is stabilising.- Parameters:
pid – PDB id.
canon_dir – The canonical (oriented) structure directory.
importance – The frame from
residue_importance().by (str) – Which column to colour by.
phi(energy share) andn_contacts(geometric share) come fromresidue_importance(); any other numeric column of the frame works too, which is how a predicted engagement fromtcren.potts.contact_map()is coloured onto the peptide.regions – Which region types to draw as coloured sticks.
PEPTIDEmatches the peptide chain, whose residues carry no CDR region label.spectrum (str) – Any PyMOL spectrum name.
- Returns:
A PyMOL scene body for
render().- Raises:
ValueError – If
byis not a column ofimportance.- Return type:
str
- tcren.viz.pymol.groove_importance_scene(pid, canon_dir, importance, *, by='p_expected', spectrum='yellow_green_blue', surface=False)[source]#
What is presented, coloured by how engaged each residue of it is.
groove_scene()’s framing – the peptide threaded along the cleft, seen from above, with the groove helices and the sheet floor kept as furniture – with the peptide coloured by a per-residue value instead of by element. The value rides in on the B-factor column, which is what PyMOL’sspectrumreads.This is the picture of
tcren.potts.contact_map()withby="position": how engaged the contact model expects each peptide residue to be, before any residue identity is scored. Unlikeimportance_scene()it zooms the groove rather than the coloured selection, so several complexes rendered this way are directly comparable.- Parameters:
pid – PDB id.
canon_dir – The canonical (oriented) structure directory.
importance – Rows carrying
chain.id,residue.indexandby.by (str) – The column to colour by. Ramped over its own range, which is right for a one-sided quantity;
importance_scene()centres on zero for the signed energy share.spectrum (str) – Any PyMOL spectrum name.
surface (bool) – Add a translucent molecular surface over the MHC ribbon.
- Returns:
A PyMOL scene body for
render().- Raises:
ValueError – If
importanceis empty orbyis not one of its columns.- Return type:
str
tcren.viz.palette module#
Colorblind-safe palette for complementarity maps (Okabe-Ito), shared by 2D and 3D.
Hue encodes the complex chain (tra/trb/peptide/mhca/mhcb); CDR loops get distinct shades.
Reference data and reproduction#
tcren.recent module#
Fetch recent TCR-pMHC structures from the RCSB PDB into data/pdb_recent (gitignored).
Two entry points, both gzipping validated mmCIF into the destination:
fetch_ids()— download specific PDB ids (e.g. the Native2026 set) straight from RCSB.discover_similar()— RCSB full-text search for TCR:peptide:MHC entries (optionally released after a date / excluding ids we already have), to surface new structures.
Robustness notes baked in per the PDB’s current state:
IDs may be longer than 4 characters (extended
pdb_0000XXXXaccessions) — handled.The PDB is deprecating split ``.pdb`` files for large structures, so we always pull mmCIF (
.cif.gz), which tcren reads natively.
Every kept structure is annotated (batched, one mmseqs pass) and must have all 5 required
chains — MHCα, β2m or MHCβ, peptide, and the two TCR chains (TRA/TRB or TRG/TRD) — else it
is dropped. huggingface_hub/network are not involved; this uses requests + the RCSB APIs.
- tcren.recent.recent_dir()[source]#
The gitignored destination for fetched structures (
data/pdb_recent).- Return type:
Path
- tcren.recent.discover_similar(after_date=None, limit=200, timeout=30.0)[source]#
RCSB full-text search for TCR:peptide:MHC structures; return candidate PDB ids.
after_date(YYYY-MM-DD) restricts to entries released on/after it (find new structures). This is the keyword-driven discovery step; the returned ids are downloaded and then strictly validated by_has_required_chains()(an agent can further curate the keyword set or the returned list before a fetch).- Parameters:
after_date (str | None)
limit (int)
timeout (float)
- Return type:
list[str]
- tcren.recent.fetch_ids(ids, dest=None, organism='human')[source]#
Download
idsfrom RCSB intodestas.cif.gz, keep only complete complexes.Incomplete (missing one of the 5 required chains) or unparseable downloads are removed. Returns a summary dict
{requested, downloaded, complete, kept}.- Parameters:
ids (list[str])
dest (Path | None)
organism (str)
- Return type:
dict
tcren.paper.bootstrap module#
Bootstrap data for the Nat Comput Sci reproduction notebooks.
Fetches the structure sets from the Hugging Face dataset isalgo/tcren_structures and
the pinned vdjdb release into the shared notebooks/data/ dir, and stages the paper’s
non-structure data there plus the legacy (mir/R) text outputs used as the regression
oracle under notebooks/natcompsci2022/results_legacy/. Network access uses requests
only (no huggingface_hub dependency). All copied csv/tsv/txt are gzipped; the
downloaded HF structures are gitignored.
- tcren.paper.bootstrap.fetch_hf_structures(data_dir, folders=('Native2022', 'Native2026', 'PolyV2022', 'Bobisse', 'Bigot'), force=False, timeout=120.0)[source]#
Download the HF structure folders into
<data_dir>/structures/(gitignored).Uses
huggingface_hub.snapshot_downloadwhen available (robust LFS download with resume + retries); otherwise falls back to a retryingrequestsloop. Neither is a hard package dependency — this is an optional reproduction tool.- Parameters:
data_dir (Path)
folders (tuple[str, ...])
force (bool)
timeout (float)
- Return type:
dict[str, int]
- tcren.paper.bootstrap.fetch_vdjdb(data_dir, date='2022-03-30', timeout=300.0)[source]#
Download the pinned vdjdb release and gzip its slim table into
<data_dir>/vdjdb/.- Parameters:
data_dir (Path)
date (str)
timeout (float)
- Return type:
Path
- tcren.paper.bootstrap.fetch_pdb_dates(data_dir, pdb_ids, timeout=30.0)[source]#
Fetch PDB initial-release dates from RCSB into
<data_dir>/PDB_date.csv.gz.RCSB release dates are external published metadata (needed for the holdout date-split). Queried per entry via the RCSB Data API; cached, so re-runs only fetch missing ids.
- Parameters:
data_dir (Path)
pdb_ids (list[str])
timeout (float)
- Return type:
Path
- tcren.paper.bootstrap.copy_external_inputs(data_dir, repo_data=None)[source]#
Stage the allowed externally published inputs (gzipped) into
<data_dir>/.These are the only non-structure inputs the reproduced pipeline may consume: the MJ/Keskin potentials, the Birnbaum set, IEDB, and the Bobisse/Bigot candidate lists.
- Parameters:
data_dir (Path)
repo_data (Path | None)
- Return type:
int
- tcren.paper.bootstrap.copy_legacy_results(paper_dir=PosixPath('/home/runner/work/tcren/tcren/notebooks/natcompsci2022'), repo_data=None)[source]#
Stage the legacy mir/R outputs into
data_legacy/(gzipped) — comparison only.Holds the 2022 baselines the new tcren results are measured against: the published TCRen matrix, mir contacts, the old non-redundancy
summary, the legacy MHC annotation, the mir annotation oracle, papersource_data/and the other-tools (TITAN/ERGO-II) outputs. Never consumed as a pipeline input.- Parameters:
paper_dir (Path)
repo_data (Path | None)
- Return type:
int
- tcren.paper.bootstrap.bootstrap(paper_dir=PosixPath('/home/runner/work/tcren/tcren/notebooks/natcompsci2022'), data_dir=PosixPath('/home/runner/work/tcren/tcren/notebooks/data'), structures=True, canonical=False, **_legacy_flags)[source]#
Fetch the HF structure sets into
data_dir(notebooks/databy default).Each set lands in its own folder directly under
data_dir(e.g.notebooks/data/ Native2026/) — nostructures/wrapper — and is gitignored. The non-structure inputs (vdjdb, Birnbaum, MJ/Keskin, IEDB, epitope lists, PDB dates) and the legacy comparison baselines are already committed under<paper_dir>/data_legacy/, so they are not re-fetched here. Passcanonical=Trueto also fetch the re-orientedCanonical2026set.- Parameters:
paper_dir (Path)
data_dir (Path)
structures (bool)
canonical (bool)
- Return type:
dict
tcren.paper.helpers module#
Helpers for the Nat Comput Sci 2022 reproduction notebooks.
contact_table replaces the legacy mir extract_contact_map (it returns the same
TCR↔peptide contact columns the R analyses consume, computed through the tcren pipeline).
compare is the small regression utility behind 07_compare_legacy.ipynb.
- tcren.paper.helpers.contact_table(structure, cutoff=5.0, count_atoms=False, contact_types=False)[source]#
TCR↔peptide contact table for an annotated structure (the mir-replacement).
The structure must already be chain-typed (
classify_chains) and MHC-annotated (annotate_mhc). Returns the columns the R benchmarks use:pdb.id, chain.type.from, region.type.from, residue.index.from, residue.index.to, pos.from, pos.to, residue.aa.from, residue.aa.to.When
count_atomsis set, an extran_atom_contactscolumn (the heavy-atom-pair count per residue pair) is carried through for atomic-weighted scoring. DefaultFalsekeeps the schema byte-identical to the legacy output.contact_typesaddscontact.typefromtcren.contact_types.residue_pair_types(). Without it a cached contact table cannot be typed after the fact —atom.from,atom.toanddistare all dropped here — which is what blocked a type-aware potential derivation.- Parameters:
structure (Structure)
cutoff (float)
count_atoms (bool)
contact_types (bool)
- Return type:
DataFrame
- tcren.paper.helpers.compare(old_path, new_path, keys, value_cols=None, tol=1e-06)[source]#
Compare two tables on
keysand report row-set + max numeric differences.Returns
{rows_old, rows_new, matched, only_old, only_new, max_abs_diff, status}wherestatusis"pass"when the key sets agree and every shared numeric column differs by ≤tol.- Parameters:
old_path (str | Path)
new_path (str | Path)
keys (list[str])
value_cols (list[str] | None)
tol (float)
- Return type:
dict
Command line#
tcren.cli module#
Command-line interface for tcren.
Commands are grouped in tcren --help:
- Scoring & prediction
tcren score— end-to-end candidate-epitope scoring (drop-in forrun_TCRen.R).tcren rank— percentile-rank a peptide’s energy against a random pMHC background.tcren ddg— ΔΔG of peptide mutations (fast virtual-matrix path; alanine scan / neoantigens).tcren assess— the score set on a folder of models: pose, binder, the five channels.tcren energy— DOPE atom-level interface interaction energy (the ΔΔGe_nativescorer).tcren mechanics— interface mechanics (stiffness / rupture / coupling) — the koff proxies.tcren scoring— per-interface contact energies Φ (--deltafor ΔΦ,--geometryfor Q).tcren surface— pMHC surface topology: height/hydropathy/charge maps + epitope comparison.tcren cpl— combinatorial-peptide-library response matrix from one template structure.tcren recognize— every interface descriptor, and Q/T/S from a feature table.
- Annotation & contacts
tcren annotate— chain typing + region markup (TCR CDR/FR, MHC groove, peptide;--pseudo).tcren contacts— annotated residue-pair contact table for an interface.
- Orientation & refinement
tcren superimpose— orient structure(s) onto the canonical database by MHC.tcren refine— potential-guided peptide-pose refinement (DOPE MC; optional--substitute).tcren substitute-tcr— graft a donor TCR onto a host pMHC (a chimeric complex).
- Reference data & potentials
tcren orient— build a canonical database from native complexes.tcren shuffle— wrong-TCR-on-real-pMHC decoys, the negatives for a recognition model.tcren derive-potential— derive a TCRen potential from a contact-map table.tcren potts— the contact map as a Boltzmann field:fita coupled model over the residue pairs that could have contacted,scorea structure’s map (energy, log Z, likelihood),contactsfor per-residue-pair contact probabilities,mapto close those onto a loop x peptide-position frequency map or a per-residue importance profile.tcren fetch-data/fetch-recent— fetch reference sets / recent RCSB TCR-pMHC entries.tcren build-mhc-ref— build the IMGT/HLA + mouse MHC allele reference.
- Info
tcren info— version + dependency check.tcren paper …— Nat Comput Sci 2022 reproduction helpers.
- tcren.cli.paper_bootstrap(structures=<typer.models.OptionInfo object>, canonical=<typer.models.OptionInfo object>)[source]#
Fetch HF structure sets into notebooks/data/<Set>/ (gitignored; non-structure inputs are already committed under natcompsci2022/data_legacy/).
- Parameters:
structures (bool)
canonical (bool)
- Return type:
None
- tcren.cli.annotate(structures=<typer.models.OptionInfo object>, out=<typer.models.OptionInfo object>, regions=<typer.models.OptionInfo object>, pseudo=<typer.models.OptionInfo object>, organism=<typer.models.OptionInfo object>)[source]#
Annotate chains and emit a per-residue region-markup table.
Covers TCR (CDR/FR), MHC groove (helices/floor) and peptide in one pass —
--regionsrestricts the output to one chain class.--pseudoadditionally marks the NetMHCpan MHC pseudosequence residues (regionMPS). MHC groove +MPSrequire MHC annotation, which runs automatically when needed.- Parameters:
structures (Path)
out (Path)
regions (str)
pseudo (bool)
organism (str)
- Return type:
None
- tcren.cli.contacts(structures=<typer.models.OptionInfo object>, out=<typer.models.OptionInfo object>, cutoff=<typer.models.OptionInfo object>, interface=<typer.models.OptionInfo object>, regions=<typer.models.OptionInfo object>, organism=<typer.models.OptionInfo object>)[source]#
Compute and emit an annotated contact table.
- Parameters:
structures (Path)
out (Path)
cutoff (float)
interface (str)
regions (str)
organism (str)
- Return type:
None
- tcren.cli.orient(structures=<typer.models.OptionInfo object>, out=<typer.models.OptionInfo object>, metadata=<typer.models.OptionInfo object>, organism=<typer.models.OptionInfo object>, reference_id=<typer.models.OptionInfo object>, force_pca=<typer.models.OptionInfo object>, threads=<typer.models.OptionInfo object>, mmcif=<typer.models.OptionInfo object>, compress=<typer.models.OptionInfo object>)[source]#
Build a canonical database: orient native TCR-pMHC complexes into the common MHC frame.
Derives the per-class canonical frame and writes every complex into it (A–E chains). This is how the bundled
Canonical2026set is produced; usesuperimposeto bring a new structure into an existing canonical database.- Parameters:
structures (Path)
out (Path)
metadata (Path)
organism (str)
reference_id (str)
force_pca (bool)
threads (int)
mmcif (bool)
compress (bool)
- Return type:
None
- tcren.cli.shuffle(structures=<typer.models.OptionInfo object>, out=<typer.models.OptionInfo object>, n=<typer.models.OptionInfo object>, seed=<typer.models.OptionInfo object>, within_class=<typer.models.OptionInfo object>, organism=<typer.models.OptionInfo object>, compress=<typer.models.OptionInfo object>)[source]#
Generate wrong-TCR-on-real-pMHC decoys (a Shuffled set) for recognition models.
Keeps each oriented complex’s pMHC intact and grafts on
ndifferent complexes’ TCRs (a within-MHC-class derangement, so no decoy reproduces a real pairing). Real (label 1) vs these decoys (label 0) trains a label-free TCR-recognition classifier. Inputs must be co-framed — runtcren orientfirst.- Parameters:
structures (Path)
out (Path)
n (int)
seed (int)
within_class (bool)
organism (str)
compress (bool)
- Return type:
None
- tcren.cli.superimpose(structures=<typer.models.OptionInfo object>, out=<typer.models.OptionInfo object>, db=<typer.models.OptionInfo object>, organism=<typer.models.OptionInfo object>, threads=<typer.models.OptionInfo object>, mmcif=<typer.models.OptionInfo object>, compress=<typer.models.OptionInfo object>)[source]#
Superimpose structure(s) onto a canonical database by MHC.
Detects each input’s MHC chains, class, and species, then superposes its conserved groove Cα onto every database structure of the same class and species and averages the transforms into one consensus placement. The database defaults to
data/Canonical2026(populated at install).-saccepts a file, directory,.tar.gz, or a shell glob.-ois an output directory, or — for a single input — a structure file whose extension must match--mmCIF/--compress. Annotation is one batched mmseqs call;-tthreads the alignment + write.- Parameters:
structures (str)
out (Path)
db (Path)
organism (str)
threads (int)
mmcif (bool)
compress (bool)
- Return type:
None
- tcren.cli.derive_potential(contact_maps=<typer.models.OptionInfo object>, out=<typer.models.OptionInfo object>, summary=<typer.models.OptionInfo object>, nonred=<typer.models.OptionInfo object>, structure_dir=<typer.models.OptionInfo object>, redundancy_t=<typer.models.OptionInfo object>, variant=<typer.models.OptionInfo object>, pseudocount=<typer.models.OptionInfo object>, balance=<typer.models.OptionInfo object>, loo=<typer.models.OptionInfo object>)[source]#
Derive a TCRen potential from observed contacts.
Provide contacts either as a precomputed
-iCSV or as a--structure-dirof PDBs (assembled viaannotate_structure_set); pass exactly one. With a structure directory,--redundancy-tadditionally restricts derivation to one representative per non-redundant cluster of αβ complexes (PDBs→contacts→cluster→derive in one call).- Parameters:
contact_maps (Path | None)
out (Path)
summary (Path | None)
nonred (bool)
structure_dir (Path | None)
redundancy_t (float | None)
variant (str)
pseudocount (int)
balance (str | None)
loo (bool)
- Return type:
None
- tcren.cli.fetch_data(canonical=<typer.models.OptionInfo object>)[source]#
Populate
data/with the reference structure sets from the HF dataset.Run once at install (
setup.shdoes this). FetchesNative2026(orientation references) and, by default,Canonical2026(the defaultsuperimposedatabase) into$TCREN_DATA_DIR/ repodata/. Skips folders already present.- Parameters:
canonical (bool)
- Return type:
None
- tcren.cli.build_mhc_ref(species=<typer.models.OptionInfo object>, force_download=<typer.models.OptionInfo object>)[source]#
Download and curate the MHC allele reference (IMGT/HLA + UniProt mouse).
- Parameters:
species (str)
force_download (bool)
- Return type:
None
- tcren.cli.fetch_recent(dest=<typer.models.OptionInfo object>, discover=<typer.models.OptionInfo object>, after=<typer.models.OptionInfo object>, organism=<typer.models.OptionInfo object>)[source]#
Download recent TCR-pMHC structures from RCSB into data/pdb_recent.
Seeds with the Native2026 ids; with –discover also full-text-searches RCSB for new entries. Each is pulled as mmCIF (.cif.gz; handles extended PDB ids), annotated, and kept only if it has all 5 required chains (MHCa + b2m/MHCb + peptide + TCR pair).
- Parameters:
dest (Path)
discover (bool)
after (str)
organism (str)
- Return type:
None
- tcren.cli.score(structures=<typer.models.OptionInfo object>, candidates=<typer.models.OptionInfo object>, potential=<typer.models.OptionInfo object>, out=<typer.models.OptionInfo object>, interface=<typer.models.OptionInfo object>, regions=<typer.models.OptionInfo object>, organism=<typer.models.OptionInfo object>, cutoff=<typer.models.OptionInfo object>, intra_weight=<typer.models.OptionInfo object>, drop_untyped=<typer.models.OptionInfo object>, position_scheme=<typer.models.OptionInfo object>, soft=<typer.models.OptionInfo object>)[source]#
Score candidate epitopes against input structures (end-to-end pipeline).
--intra-weightadds the term the interface sum omits: a candidate threaded onto the template’s peptide conformation also pays for the contacts that conformation makes it have with itself (5 Å, sequence separation >= 3, MJ). It is off by default; a class-I 9-mer is extended and makes zero to two such contacts, so it separates candidates only where the peptide is genuinely bulged or self-packed.--drop-untyped,--position-weightsand--softall reweight the same sum, and all default to off so the score is unchanged unless asked. The first uses the chemical typing to ignore pairs that are within 5 Å but make no interaction; the second says a contact under the CDR3 loops in the middle of the peptide is not worth the same as one at an anchor the TCR never touches; the third replaces the hard cutoff with a contact probability averaged over side-chain rotamers, which is what stops a single wrong χ1 from moving the energy by more than the energy itself (measured|ΔΦ|0.524 → 0.054 under a deliberately wrong rotamer).- Parameters:
structures (Path)
candidates (Path)
potential (str | None)
out (Path)
interface (str)
regions (str)
organism (str)
cutoff (float)
intra_weight (float)
drop_untyped (bool)
position_scheme (str)
soft (bool)
- Return type:
None
- tcren.cli.ddg_cmd(structures=<typer.models.OptionInfo object>, native=<typer.models.OptionInfo object>, alanine_scan=<typer.models.OptionInfo object>, side=<typer.models.OptionInfo object>, virtual=<typer.models.OptionInfo object>, mutant=<typer.models.OptionInfo object>, potential=<typer.models.OptionInfo object>, out=<typer.models.OptionInfo object>, interface=<typer.models.OptionInfo object>, mhc_potential=<typer.models.OptionInfo object>, regions=<typer.models.OptionInfo object>, organism=<typer.models.OptionInfo object>, cutoff=<typer.models.OptionInfo object>)[source]#
ΔΔG of point mutations;
ddG = E(native) - E(mutant), positive => STABILISING.--alanine-scanwalks one residue at a time, truncating it to alanine in 3D and rescoring the rebuilt contact map, so a side chain that was the only thing bridging to its partner loses those contacts.--sidechooses which side is walked:peptide(default),tcr(the contacted CDR residues) orboth.--virtualtakes the fast path instead, re-indexing the mutant on the native map with no atoms moved – peptide side only.--mutantscores specific substitutions rather than a scan.- Parameters:
structures (Path)
native (str)
alanine_scan (bool)
side (str)
virtual (bool)
mutant (list[str])
potential (str | None)
out (Path)
interface (str)
mhc_potential (str | None)
regions (str)
organism (str)
cutoff (float)
- Return type:
None
- tcren.cli.cpl_cmd(structures=<typer.models.OptionInfo object>, peptide=<typer.models.OptionInfo object>, position=<typer.models.OptionInfo object>, mutation=<typer.models.OptionInfo object>, to_mixture=<typer.models.OptionInfo object>, reference=<typer.models.OptionInfo object>, potential=<typer.models.OptionInfo object>, mhc_potential=<typer.models.OptionInfo object>, out=<typer.models.OptionInfo object>, regions=<typer.models.OptionInfo object>, organism=<typer.models.OptionInfo object>, cutoff=<typer.models.OptionInfo object>)[source]#
Predict a combinatorial-peptide-library response matrix from a template TCR:pMHC structure.
One row per (peptide position, amino acid) cell. Every cell carries BOTH peptide-bearing interfaces summed – TCRen over TCR:peptide plus Miyazawa-Jernigan over peptide:MHC – because the assay reads activation, which needs the peptide presented as well as the receptor engaged.
Two reference states are reported, and a cell is only meaningful against one of them:
effect_equimolarscores a residue against the 1/20 mixture, which is the CPL background and the right axis to compare with a measured matrix;effect_wild_typescores it against the residue the template carries, which is the mutation-scan / neoantigen question. Positive is favourable on both.Three narrower questions, all from the same matrix:
–position 5 every substitution at position 5, best first –position 5 –mutation W just that one cell –position 5 –to-mixture the cost of giving position 5 up to the 1/20 mixture
- Parameters:
structures (Path)
peptide (str)
position (int)
mutation (str)
to_mixture (bool)
reference (str)
potential (str | None)
mhc_potential (str | None)
out (Path)
regions (str)
organism (str)
cutoff (float)
- Return type:
None
- tcren.cli.rank(structures=<typer.models.OptionInfo object>, candidates=<typer.models.OptionInfo object>, potential=<typer.models.OptionInfo object>, out=<typer.models.OptionInfo object>, interface=<typer.models.OptionInfo object>, regions=<typer.models.OptionInfo object>, background=<typer.models.OptionInfo object>, background_source=<typer.models.OptionInfo object>, seed=<typer.models.OptionInfo object>, organism=<typer.models.OptionInfo object>, cutoff=<typer.models.OptionInfo object>)[source]#
Percentile-rank peptides’ TCRen energy against a random pMHC background.
For each structure, scores the supplied candidate peptides (or the structure’s own peptide when
-cis omitted) together with--backgroundrandom peptides of the same length and reportsrank_pct— the fraction of background scoring at least as well (lower energy = better binder, so a smallrank_pctmeans a strong binder).- Parameters:
structures (Path)
candidates (Path)
potential (str | None)
out (Path)
interface (str)
regions (str)
background (int)
background_source (Path)
seed (int)
organism (str)
cutoff (float)
- Return type:
None
- tcren.cli.surface(structures=<typer.models.OptionInfo object>, out=<typer.models.OptionInfo object>, organism=<typer.models.OptionInfo object>, grid=<typer.models.OptionInfo object>, scale=<typer.models.OptionInfo object>, channel=<typer.models.OptionInfo object>, side=<typer.models.OptionInfo object>, complementarity=<typer.models.OptionInfo object>, region=<typer.models.OptionInfo object>, compare=<typer.models.OptionInfo object>, cells=<typer.models.OptionInfo object>, svg=<typer.models.OptionInfo object>)[source]#
Map the pMHC surface a TCR sees — height + hydropathy + charge over the groove.
Emits, per structure, the scalars that say how featured the presented surface is:
relief(height spread over the peptide’s footprint),peak_to_valley,frac_above_ridge(how much peptide surface clears the MHC helix crests) and the mean/central hydropathy. A flat, MHC-dominated landscape — a “featureless” epitope — scores low on all of them.The groove frame is refit from each structure, so maps are comparable without prealigning the inputs:
--comparewrites the pairwise Manhattan map distance, which clusters structures of the same epitope together.--side tcrmaps the receptor’s underside in the same frame instead, and--complementaritybuilds both faces and reports how well they agree cell for cell — shape, charge and hydropathy — over the calibrated window and Z cutoff.- Parameters:
structures (Path)
out (Path)
organism (str)
grid (str)
scale (str)
channel (str)
side (str)
complementarity (Path)
region (str)
compare (Path)
cells (Path)
svg (Path)
- Return type:
None
- tcren.cli.recognize(structures=<typer.models.OptionInfo object>, features_table=<typer.models.OptionInfo object>, out=<typer.models.OptionInfo object>, organism=<typer.models.OptionInfo object>, full=<typer.models.OptionInfo object>, mechanics=<typer.models.OptionInfo object>, threads=<typer.models.OptionInfo object>, autodetect_species=<typer.models.OptionInfo object>)[source]#
Full interface descriptor table for each TCR-pMHC complex (one TSV row per PDB).
One row per structure with the complete recognition feature set (
tcren.recognition.RECOGNITION_FEATURES): docking geometry (pitch, crossing, the 6 TCRdock rigid-body params), per-interface energiesPhi_{tcr_pep,tcr_mhc,pep_mhc}and poly-alaninedPhi, CDR-loop energiesPhi_{cdr12,cdr3a,cdr3b}, contact-type tallies, ΔSASAburialand the MHC-class indicator.--fullalso emits the 18 CDR3-local frame descriptors (the FramePose strain layer) and the intra-peptide termPhi_pep_int/n_pep_int— the peptide’s contact energy with itself, which the three interface energies omit.This command emits no fitted composite.
p_real,p_real_bn,p_bind,p_forced,q_bindands_strainwere removed in 2.26.0, and the reason was reproducibility rather than performance: their coefficients were frozen against training sets nobody could reconstruct, which made them the one part of the package a reader could not regenerate. What--featuresreturns instead isQ,TandS, none of which fits anything at call time; the two-class read-outs aretcren assess, whose frozen model refits from a manifest that ships in the wheel. Output is TSV.--mechanicsappends the koff proxiestcren mechanicsreports — stiffness tensor, steered rupture, coupling residues — to these same rows. Prefer it to running the two commands: they need the identical annotated structure, so the second command repeats the parse and both mmseqs searches to produce a second table in a different format (CSV) under a different key (pdb.id) that then has to be joined. One flag costs about a sixth of the descriptor pass and returns one table.Complementary scorer on the same inputs:
tcren ddg(per-mutation alanine/neoantigen ΔΔG).Examples:
tcren features -s models/ -o feats.tsv # the descriptor pass, once tcren recognize --features feats.tsv -o scores.tsv # Q, T, S tcren recognize -s models/ -o out.tsv # descriptors only, no feature file tcren recognize -s models/ --mechanics -t 0 -o out.tsv # + the spring-network terms
- Parameters:
structures (str)
features_table (Path)
out (Path)
organism (str)
full (bool)
mechanics (bool)
threads (int)
autodetect_species (bool)
- Return type:
None
- tcren.cli.fit_holdout_cmd(features_table=<typer.models.OptionInfo object>, manifest_file=<typer.models.OptionInfo object>, out=<typer.models.OptionInfo object>)[source]#
Refit the frozen model behind tcren assess from its hold-out, and write it out.
The earlier fitted read-out in this project was withdrawn because its coefficients were frozen against a training set nobody could reconstruct. These are frozen against one that is named: the manifest ships inside the wheel, and every structure it names is deposited. Descriptors are not shipped – 8,292 rows by 147 columns is 19 MB – so the reproduction is three commands:
tcren fetch-data # the structure sets the manifest names tcren features -s <those structures> -o hold.tsv # the descriptors tcren fit-holdout –features hold.tsv -o refit.npz
and
refit.npzmatches the shipped model to a relative 1e-5, the bound the test suite asserts. It is not bit-identical across platforms: the Yeo-Johnson lambda comes from a Brent search that stops at its own tolerance of about 1.5e-8, and every array fitted through the transform inherits that. Pass your own--manifestto fit a different hold-out, then read it back withtcren assess --model.- Parameters:
features_table (Path)
manifest_file (Path)
out (Path)
- Return type:
None
- tcren.cli.scoring(structures=<typer.models.OptionInfo object>, out=<typer.models.OptionInfo object>, no_superimpose=<typer.models.OptionInfo object>, db=<typer.models.OptionInfo object>, organism=<typer.models.OptionInfo object>, cutoff=<typer.models.OptionInfo object>, tcr_peptide_potential=<typer.models.OptionInfo object>, tcr_mhc_potential=<typer.models.OptionInfo object>, peptide_mhc_potential=<typer.models.OptionInfo object>, regions=<typer.models.OptionInfo object>, contact_weight=<typer.models.OptionInfo object>, intra_weight=<typer.models.OptionInfo object>, delta=<typer.models.OptionInfo object>, reference_aa=<typer.models.OptionInfo object>, geometry=<typer.models.OptionInfo object>, skip_errors=<typer.models.OptionInfo object>, threads=<typer.models.OptionInfo object>)[source]#
Score structures: per-interface contact energies Φ (and ΔΦ, and interface geometry).
This is scoring only — it reads structures and writes numbers. The preparation steps (canonicalisation, region mapping, Cα / contact / atom-distance matrices) are separate commands:
tcren annotate,tcren superimpose,tcren contacts.Columns
Phi_tcr_pep,Phi_tcr_mhc,Phi_pep_mhcare the three interface terms Φ_TP, Φ_TM, Φ_PM;Phi_totalis their sum Φ. With--deltaeach also gets its poly-alanine-referenced counterpartdPhi_*(ΔΦ_TP, ΔΦ_TM≡0, ΔΦ_PM) anddPhi_total= ΔΦ. The names matchtcren recognize, so the two tables join onpdb.id. ΔΦ is the score to use across candidates that each carry their own generated pose, where raw Φ partly reads the pose geometry rather than the peptide sequence.--intra-weight wadds the term the three interface sums omit —Phi_pep_int, the peptide’s contact energy with itself (5 Å, sequence separation >= 3, MJ) — and foldsw x Phi_pep_intintoPhi_total. The energy is reported raw, so the term and the weight stay separable.--geometryappends the interface descriptors (buried surfaceburial, peptide coveragen_pep_contacted,chain_balance,n_hbond, dockingpitch/crossing) andQ— the directional, decorrelated interface-quality score, standardised against the native-crystal reference so it is defined for a single structure (tcren.q_score()). For the complete descriptor catalogue, usetcren features; for the scores built on it,tcren recognize(Q,T,S) andtcren assess(the score set).Each interface’s potential can be overridden with a bundled name (
tcren2,karnaukhov2022,mj,keskin) or a CSV path; an unset option keeps the default family for that interface.Examples:
tcren scoring -s complex.pdb.gz -o scores.csv tcren scoring -s a.pdb.gz -s b.pdb.gz --delta # repeat -s, or comma-separate tcren scoring -s 'models/*.pdb.gz' --delta --geometry # quote the glob tcren scoring -s models/ --delta -t 8 # a directory, 8 workers tcren scoring -s models.txt --delta # one path per line tcren scoring -s models.tar.gz --regions cdr # CDR contacts only
Chain typing and MHC annotation are done ONCE for the whole input set, in one mmseqs search each, with mmseqs threading internally – never per structure and never inside a worker pool.
-tsets the mmseqs thread count for those two searches and the worker count for what is left, which is contact-map construction and the energy sums in numpy (-t 0uses every core).- Parameters:
structures (list[str])
out (Path)
no_superimpose (bool)
db (Path)
organism (str)
cutoff (float)
tcr_peptide_potential (str)
tcr_mhc_potential (str)
peptide_mhc_potential (str)
regions (str)
contact_weight (str)
intra_weight (float)
delta (bool)
reference_aa (str)
geometry (bool)
skip_errors (bool)
threads (int)
- Return type:
None
- tcren.cli.pipeline(ctx)[source]#
Deprecated alias for
tcren scoring(this command never ran the full pipeline).- Parameters:
ctx (Context)
- Return type:
None
- tcren.cli.energy(structures=<typer.models.OptionInfo object>, out=<typer.models.OptionInfo object>, relax=<typer.models.OptionInfo object>, shell=<typer.models.OptionInfo object>, organism=<typer.models.OptionInfo object>, seed=<typer.models.OptionInfo object>)[source]#
DOPE atom-level interaction energy across the peptide↔partner interface (the
_relaxkernel).Sums the DOPE potential over peptide↔partner heavy-atom pairs — the interface ΔG contribution of the peptide (lower = more favourable). With
--relaxit also reports the energy after a rigid-body DOPE refinement (tcren.refine_peptide()) and the relaxationgap= e_native − e_relax. This is the single-structure scorer behind the ΔΔG benchmark (e_native/e_relax).- Parameters:
structures (str)
out (Path)
relax (bool)
shell (float)
organism (str)
seed (int)
- Return type:
None
- tcren.cli.mechanics(structures=<typer.models.OptionInfo object>, out=<typer.models.OptionInfo object>, cutoff=<typer.models.OptionInfo object>, weight=<typer.models.OptionInfo object>, direction=<typer.models.OptionInfo object>, break_strain=<typer.models.OptionInfo object>, organism=<typer.models.OptionInfo object>, threads=<typer.models.OptionInfo object>, autodetect_species=<typer.models.OptionInfo object>)[source]#
Interface mechanics — the koff proxies: stiffness tensor + steered rupture + coupling residues.
Treats the TCR↔pMHC contact map as a network of breakable springs and reports, per structure:
n_spring,S_tot/K_tens/K_shear/aniso(stiffness tensor),rupture_force/rupture_work(steered unbinding), andcouple_pep/couple_total(coupling residues). Validated on ATLAS: the tensile stiffness / rupture resistance track the dissociation off-rate (koff) far better than the equilibrium ΔG/Kd (Bell–Evans; the TCR is a mechanosensor).- Parameters:
structures (str)
out (Path)
cutoff (float)
weight (str)
direction (str)
break_strain (float)
organism (str)
threads (int)
autodetect_species (bool)
- Return type:
None
- tcren.cli.refine(structures=<typer.models.OptionInfo object>, out=<typer.models.OptionInfo object>, substitute=<typer.models.OptionInfo object>, organism=<typer.models.OptionInfo object>, n_steps=<typer.models.OptionInfo object>, restraint_w=<typer.models.OptionInfo object>, seed=<typer.models.OptionInfo object>, repack=<typer.models.OptionInfo object>, max_chi=<typer.models.OptionInfo object>, mmcif=<typer.models.OptionInfo object>, compress=<typer.models.OptionInfo object>)[source]#
Potential-guided rigid-body refinement of the peptide pose (knowledge-based, not physics).
Optionally
--substitutea new equal-length peptide first, then run a Monte-Carlo refinement scored by the DOPE atom-level statistical potential (restrained to the input pose; independent of the TCRen/MJ scoring potentials). Writes one structure per input and prints the final DOPE energy.--repackadds the side-chain half: the MC moves the peptide rigidly and leaves every χ where it found it, so a full-atom model whose side chains a predictor placed keeps them. The packer re-samples χ discretely, which is what a local minimiser structurally cannot do — measured on five crystals with χ1 deliberately rotated 120°, it recovers side-chain RMSD from 4.13 Å to 2.36 Å in 6 ms, where OpenMM’s restrained minimisation returns 4.13 Å (unchanged) in 3.1 s, because gradient descent cannot cross a torsional barrier. It rotates the side chains a model has; it cannot rebuild ones--substitutestripped.- Parameters:
structures (str)
out (Path)
substitute (str)
organism (str)
n_steps (int)
restraint_w (float)
seed (int)
repack (bool)
max_chi (int)
mmcif (bool)
compress (bool)
- Return type:
None
- tcren.cli.substitute_tcr_cmd(host=<typer.models.OptionInfo object>, donor=<typer.models.OptionInfo object>, out=<typer.models.OptionInfo object>, by=<typer.models.OptionInfo object>, organism=<typer.models.OptionInfo object>)[source]#
Graft the donor TCR onto the host pMHC → a chimeric TCR:pMHC complex.
Keeps the host peptide + MHC and the donor TCR.
--by mhcsuperposes the donor MHC groove onto the host groove (the donor TCR keeps its native docking geometry);--by tcrsuperposes the donor TCR onto the host TCR (the donor TCR inherits the host’s docking pose). Both inputs are chain-typed automatically (and, for--by mhc, MHC-annotated).- Parameters:
host (Path)
donor (Path)
out (Path)
by (str)
organism (str)
- Return type:
None
- tcren.cli.assess(features_table=<typer.models.OptionInfo object>, out=<typer.models.OptionInfo object>, peptide=<typer.models.OptionInfo object>, model_file=<typer.models.OptionInfo object>, iptm_column=<typer.models.OptionInfo object>, band=<typer.models.OptionInfo object>, budget=<typer.models.OptionInfo object>, list_bands=<typer.models.OptionInfo object>)[source]#
Assess modelled complexes: is the pose real, is it a binder, and which channel says so.
The one command to run on a folder of AlphaFold models. Four blocks, one table.
THE SCORE SET – every one defined for a SINGLE structure, because the transform, the class means and the covariance are all frozen on a hold-out that ships with the package. Nothing is estimated from the rows you pass, so a score does not move depending on what was scored beside it. Higher is better throughout.
pose_score
Is this the kind of interface real complexes make? A one-class distance to the manifold hold-out binders occupy, reading NO binder label at all. This is the bad-pose channel.
binder_score
Log-odds that the complex is a genuine recognition interface.
channel_*
The same log-odds marginalized to one descriptor family, so a number can be attributed: placement (where the receptor sits), interface (how much it makes, of what chemistry), shape (the footprint free of its size), energetics (the contact chemistry in kT), mechanics (the interface as breakable springs).
peptide_score
The poly-alanine-referenced recognition energy, with nothing fitted in it. This ranks PEPTIDES for a fixed receptor and reads below chance on a receptor benchmark – a property of the reference frame, not a fault.
confidence_residual
Reported ipTM minus what the coordinates say it should have been. A large positive residual is a model the generator is more certain of than its own geometry and chemistry warrant.
binder_iptm
binder_score + logit(ipTM): two log-odds added, no coefficient to fit. The recommended read when a confidence is available.
THE PREDECESSOR TIER – S, the fit-free composition of interface quality, footprint shape and contact energy in native-sd units. It leads the functionally validated receptor screen on its own and COMPOSES with binder_score rather than being replaced by it, so it is reported beside it rather than dropped.
TRIAGE – rank and percentile within the set on the recommended score, for when only the order matters, plus the expected mean score if you keep the top –budget fraction.
GENERATOR DIAGNOSTIC – with an ipTM column, p_nonbinder_af reads the frozen band table: how often a model this confident is a non-binder, and what S still separates inside that band.
Pass
--peptidewhen the peptide is what varies across the structures being compared. Otherwise the five descriptors computed without the receptor are marginalized out, because they are constant across every structure of one epitope on one allele and a model reading them reaches the cohort’s name without reading an interface.Not to be confused with
tcren score, which is the other direction entirely: it threads candidate epitopes onto one template structure and ranks them by contact energy.Examples:
tcren features -s models/ -o feats.tsv tcren assess --features feats.tsv -o assessed.tsv tcren assess --features cpl_feats.tsv --peptide -o cpl.tsv
- Parameters:
features_table (Path)
out (Path)
peptide (bool)
model_file (Path)
iptm_column (str)
band (str)
budget (float)
list_bands (bool)
- Return type:
None
- tcren.cli.features(structures=<typer.models.OptionInfo object>, out=<typer.models.OptionInfo object>, include=<typer.models.OptionInfo object>, all_families=<typer.models.OptionInfo object>, organism=<typer.models.OptionInfo object>, radii=<typer.models.OptionInfo object>, threads=<typer.models.OptionInfo object>, autodetect_species=<typer.models.OptionInfo object>, metadata=<typer.models.OptionInfo object>)[source]#
Raw per-structure descriptors, one row per structure, in six feature families.
This command emits features only — no model, no probability, no cohort score. Its companion is
tcren recognize, which turns a feature table into scores and can read this file back with--featuresinstead of re-reading the structures.The families are split by what each quantity is invariant under, which is also the axis along which they carry independent evidence:
placement– where the receptor sits in the groove frame: docking angles, the TCRdock rigid-body parameters, ride height / shift / offset, and the CDR3 loop frames. Frame-DEPENDENT.interface– how much contact there is and of what chemical kind: buried area, contact counts and types, hydrogen bonds, clashes, chain and loop balance.topology– the SHAPE of the contact set, free of its size: coverage entropy and Hill numbers, the footprint’s Betti numbers and persistence entropy, the canonical germline/CDR3 preference.energetics– statistical-potential interface energies Phi and their poly-alanine reference differences dPhi. Thedis the reference difference, never a derivative.potts– the same interface read against the partition function of the coupled contact model rather than against a poly-alanine one. Off by default; add it with –all or -i.kinetics– the interface as a spring network: stiffness, rupture, coupling residues. Off by default (it is the most expensive family); add it with –all or -i.
Only what you ask for is computed:
-i topologynever builds the energies and-i placementnever runs the spring network. Whatever the selection, the whole set is annotated in one arda call per organism and one mmseqs MHC search, never one per structure.Every emitted column is catalogued in
tcren.recognition.DESCRIPTORS, so the families are a partition of the table rather than a label on it.Examples:
tcren features -s models/ -o feats.tsv # the four default families tcren features -s models/ -i topology -o shape.tsv # footprint shape alone tcren features -s models/ --all -t 0 -o feats.tsv # everything, all cores tcren recognize --features feats.tsv -o scores.tsv # score without re-reading structures
- Parameters:
structures (str)
out (Path)
include (str)
all_families (bool)
organism (str)
radii (str)
threads (int)
autodetect_species (bool)
metadata (bool)
- Return type:
None
- tcren.cli.footprint(structures=<typer.models.OptionInfo object>, out=<typer.models.OptionInfo object>, organism=<typer.models.OptionInfo object>, cutoff=<typer.models.OptionInfo object>, radii=<typer.models.OptionInfo object>, group=<typer.models.OptionInfo object>, meta=<typer.models.OptionInfo object>, score=<typer.models.OptionInfo object>)[source]#
Footprint shape: how a receptor’s contacts are DISTRIBUTED, not what they score.
Superseded by
tcren features -i topology, which emits the same columns from the shared feature pass; kept working, and hidden from the command list.One TSV row per structure with the coverage measures – normalised Shannon entropy
H_celland the Hill numbersD1/D2over the 6 CDR loops x {peptide, MHC}, plusD2_pep24on the finer partition that splits the peptide into thirds – the canonical docking preference (L_canon,p_germ_mhc,p_cdr3_pep), the alpha/beta contact imbalance, and the footprint’s topology (fp_b0_*patches,fp_b1_*holes, the Euler characteristic, and the H0 persistence entropy).None of these is an energy and none needs a potential, a reference structure or a fitted parameter. They are invariant under rigid motion, so the inputs do not have to be oriented – only chain-typed with CDR region markup, which this command does for you in one batched annotation pass over the whole set.
--scoreaddsT, the fit-free shape score: a directional score against the Native2026 crystal reference, restricted to these descriptors. Nothing is fitted at call time, so it is defined for a single input and--groupno longer changes any value – the option is kept only because it carries the grouping column through. The cohort-fitted channel posterior this replaced went at 2.26.0.Complementary scorers on the same inputs:
tcren recognize(the energies plusQ,TandS) andtcren assess(the score set).- Parameters:
structures (str)
out (Path)
organism (str)
cutoff (float)
radii (str)
group (str)
meta (Path)
score (bool)
- Return type:
None
- tcren.cli.main()[source]#
Console-script entry point.
Every command takes
-sas a free-form spec (file, directory, glob, manifest, archive), so Typer cannot check it exists and a typo’d path surfaced as an 80-line Rich traceback out of Biopython. One line is what the user needs;typer.BadParametercovers the rest.- Return type:
None
- tcren.cli.potts_fit_cmd(structures=<typer.models.OptionInfo object>, out=<typer.models.OptionInfo object>, partner=<typer.models.OptionInfo object>, receptor=<typer.models.OptionInfo object>, radius=<typer.models.OptionInfo object>, cutoff=<typer.models.OptionInfo object>, couplings=<typer.models.OptionInfo object>, coupling_matrix=<typer.models.OptionInfo object>, balance=<typer.models.OptionInfo object>, ridge=<typer.models.OptionInfo object>, pairs_out=<typer.models.OptionInfo object>)[source]#
Fit the coupled contact-map model to a set of structures.
Penalised pseudolikelihood, then a projection to the zero-sum gauge. No partition function is needed to fit — the conditional of one site given the rest is an ordinary logistic regression whose extra covariates are counts of contacting neighbours.
The αβ TCR:pMHC HARD RULE applies, as in
derive-potential: a structure missing either CDR3 or the peptide is out of scope and is skipped.- Parameters:
structures (Path)
out (Path)
partner (str)
receptor (str)
radius (float)
cutoff (float)
couplings (bool)
coupling_matrix (str | None)
balance (str | None)
ridge (float)
pairs_out (Path | None)
- Return type:
None
- tcren.cli.potts_score_cmd(structures=<typer.models.OptionInfo object>, out=<typer.models.OptionInfo object>, model=<typer.models.OptionInfo object>, partner=<typer.models.OptionInfo object>, particles=<typer.models.OptionInfo object>, steps=<typer.models.OptionInfo object>, seed=<typer.models.OptionInfo object>, workers=<typer.models.OptionInfo object>)[source]#
Energy, partition function and likelihood of each structure’s observed contact map.
log Zis estimated by annealed importance sampling from the uncoupled model, whose partition function is exact and closed form, so the reference is a verified model rather than an approximation. Checkais_ess: close to--particlesmeans the schedule was long enough.psiis the log-likelihood per available pair, and is the column to compare across interfaces of different size.- Parameters:
structures (Path)
out (Path)
model (Path | None)
partner (str)
particles (int)
steps (int)
seed (int)
workers (int | None)
- Return type:
None
- tcren.cli.potts_contacts_cmd(structures=<typer.models.OptionInfo object>, out=<typer.models.OptionInfo object>, model=<typer.models.OptionInfo object>, partner=<typer.models.OptionInfo object>, chains=<typer.models.OptionInfo object>, burn=<typer.models.OptionInfo object>, draws=<typer.models.OptionInfo object>, thin=<typer.models.OptionInfo object>, seed=<typer.models.OptionInfo object>, workers=<typer.models.OptionInfo object>)[source]#
Per-residue-pair contact probability under the model, beside what the structure did.
Three probabilities, and their differences are the couplings:
p_independentis the one-body model alone;p_modelis the marginal of the full coupled model, sampled by block Gibbs, and is the one to use;p_conditionalisP(contact | the observed rest).- Parameters:
structures (Path)
out (Path)
model (Path | None)
partner (str)
chains (int)
burn (int)
draws (int)
thin (int)
seed (int)
workers (int | None)
- Return type:
None
- tcren.cli.potts_map_cmd(structures=<typer.models.OptionInfo object>, out=<typer.models.OptionInfo object>, model=<typer.models.OptionInfo object>, by=<typer.models.OptionInfo object>, partner=<typer.models.OptionInfo object>, chains=<typer.models.OptionInfo object>, burn=<typer.models.OptionInfo object>, draws=<typer.models.OptionInfo object>, thin=<typer.models.OptionInfo object>, seed=<typer.models.OptionInfo object>, workers=<typer.models.OptionInfo object>)[source]#
Predicted contact-frequency map, and how engaged each peptide residue is.
--by loopgives one row per (structure, CDR loop, peptide position): the frequency map an MD trajectory reports as the fraction of frames in which any residue of that loop touches that position.--by positioncollapses the loops and reads peptide residue importance – how engaged the model expects each position to be, before any residue identity is scored.--by pairis the ungrouped table and is exactlytcren potts contacts.p_anyis1 - prod(1 - p)over the group’s pairs,p_expectedtheir sum, andobservedthe 0/1 the structure itself made. These are frequencies, not energies.- Parameters:
structures (Path)
out (Path)
model (Path | None)
by (str)
partner (str)
chains (int)
burn (int)
draws (int)
thin (int)
seed (int)
workers (int | None)
- Return type:
None
- tcren.cli.potts_scan_cmd(structures=<typer.models.OptionInfo object>, out=<typer.models.OptionInfo object>, model=<typer.models.OptionInfo object>, partner=<typer.models.OptionInfo object>, coupled=<typer.models.OptionInfo object>, chains=<typer.models.OptionInfo object>, burn=<typer.models.OptionInfo object>, draws=<typer.models.OptionInfo object>, thin=<typer.models.OptionInfo object>, seed=<typer.models.OptionInfo object>, workers=<typer.models.OptionInfo object>)[source]#
Free-energy effect of every substitution at every partner position.
map --by positionreads how engaged a position is expected to be before any residue identity is scored; this reads what happens when the identity changes. The partner residue enters the field through both the partner propensity and the pair term, so threading a residue through position i moves every available pair carrying it, andlog Z0moves with it.dFis the equimolar-referenced effect – against the mean over the twenty residues at that position, the null a positional-scanning library holds its other positions at – so it sums to zero down a position and is additive across them. Higher is more favourable. Unlikemap’s frequencies this is an energy.- Parameters:
structures (Path)
out (Path)
model (Path | None)
partner (str)
coupled (bool)
chains (int)
burn (int)
draws (int)
thin (int)
seed (int)
workers (int | None)
- Return type:
None
- tcren.cli.explain(structures=<typer.models.OptionInfo object>, out=<typer.models.OptionInfo object>, score=<typer.models.OptionInfo object>, chain_types=<typer.models.OptionInfo object>, interfaces=<typer.models.OptionInfo object>, organism=<typer.models.OptionInfo object>)[source]#
Per-residue leave-one-out contribution to a whole-structure score.
Writes one row per interface residue with
delta= score(complex) - score(complex without that residue), which is what a per-residue confidence colouring shows: how much this residue carries. Defined for every read-out and every channel, so the same table colours a figure by any of them — feed it totcren.viz.pymol.importance_scene.The peptide read-out has an EXACT decomposition already (
tcren.energetics.scoring.position_profile, which sums to the score); prefer it there and use this for the read-outs that do not decompose. Chain typing and the MHC call run once for the whole structure, so cost is one descriptor pass per interface residue, a few minutes.- Parameters:
structures (Path)
out (Path)
score (str)
chain_types (str)
interfaces (str)
organism (str)
- Return type:
None