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: object

A 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: object

A 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#
atoms: tuple[Atom, ...]#
property ca: ndarray | None#

Cα coordinate, or None if the residue has no Cα atom.

property cb: ndarray | None#

Cβ coordinate, or None if the residue has no Cβ atom (e.g. glycine).

property cb_or_ca: ndarray | None#

Cβ coordinate, falling back to Cα (glycine / missing Cβ); None if neither.

class tcren.structure.model.Chain(chain_id, residues, chain_type=None, chain_supertype=None, allele_info=None, regions=<factory>)[source]#

Bases: object

A 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#
residues: list[Residue]#
chain_type: str | None#
chain_supertype: str | None#
allele_info: str | None#
regions: list[RegionMarkup]#
sequence()[source]#

One-letter sequence in residue order.

Return type:

str

by_seq_index(seq_index)[source]#

Return the residue at a given sequential index, or None.

Parameters:

seq_index (int)

Return type:

Residue | None

class tcren.structure.model.RegionMarkup(region_type, start_seq_index, end_seq_index, sequence, residues)[source]#

Bases: object

An 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#
residues: list[Residue]#
class tcren.structure.model.Structure(pdb_id, chains, complex_species=None, cell_type=None, mhc_calls=None)[source]#

Bases: object

A 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#
chains: list[Chain]#
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.

chain(chain_id)[source]#

Return the chain with the given id (raises KeyError if absent).

Parameters:

chain_id (str)

Return type:

Chain

by_type(*types)[source]#

Return chains whose chain_type is in types.

Parameters:

types (str)

Return type:

list[Chain]

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 name is 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 .gz and 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 .gz and the structure extension, then takes the part before the first _ (so 4x5w_renumbered.cif and 1ao7.pdb.gz and 6uk4_TCRpMHCmodels.pdb all 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.pdb and VDJdb_Model_604_min.pdb both give VDJdb. Prefer iter_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.cif come back as 4x5w, 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_index per chain, matching the legacy mir residue.index.

Parameters:
  • path (str | Path) – Path to a .pdb/.ent or .cif/.mmcif file.

  • 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:

Structure

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.gz file. mmCIF is not supported.

  • chain (str | None) – restrict to one author chain id, or None for every atom in the file.

Returns:

The mean, or nan if the file holds no atom line matching chain.

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_gene is 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 Structure with cell_type set.

Return type:

Structure

tcren.structure.io.structure_paths(src)[source]#

List structure files for src, sorted.

src may be a single structure file, a directory (scanned for structure files), a glob pattern (models/*.pdb.gz), or a manifest: a .txt/.list/.lst file 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, use iter_structures().

Parameters:

src (str | Path)

Return type:

list[Path]

tcren.structure.io.resolve_sources(sources)[source]#

Split a CLI -s spec (or several) into individual sources for iter_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.gz archive.

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-based importer works unchanged). The identifier is resolved per file by structure_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) or parse_structure() (parity-pure). Extra kwargs are 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). transform is an optional coord -> coord callable (e.g. for an oriented frame); identity if None. 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_site loop for structure (optionally transformed).

Same atom selection as pdb_lines() (one conformer per atom name per residue). Only the _atom_site category 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 structure to a PDB file; return the path.

A .gz suffix (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.

.pdb by default, .cif if mmcif, with a trailing .gz if compress.

Parameters:
  • directory (str | Path)

  • pdb_id (str)

  • mmcif (bool)

  • compress (bool)

Return type:

Path

tcren.structure.io.write_structure(structure, path, transform=None, keep_hydrogens=True)[source]#

Format-dispatch writer: PDB or mmCIF, optionally gzipped (by the path suffix).

Parameters:
  • structure (Structure)

  • path (str | Path)

  • keep_hydrogens (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 chain in place when arda recognises it as TRA/TRB: sets chain_type, chain_supertype ("TRAB"), allele_info and regions. Returns the arda record (for any locus) or None if 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 structure in 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 force organism verbatim.

  • 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 in annotate_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: object

A 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/end are 0-based half-open indices into sequence. Returns None if no constant domain is present (score below min_score). The constant region is C-terminal, so callers trim residues with seq_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 for classify_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 Structure objects.

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. contacts is the stacked TCR↔peptide contact_table(); markup is 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_atoms is set, each contact row carries an n_atom_contacts heavy-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_annotation table. ids restricts 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 mmseqs easy_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). Returns pdb.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.

threads caps the mmseqs thread count for THIS call. It matters whenever batches are annotated concurrently: arda.annotate_sequences does not forward a thread count, so annotate_records falls back to its threads=0 default, 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. Pass max(1, cpu_count // concurrency). 0 keeps 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 to classify_chains() as precomputed_records.

arda defaults 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_chains per structure spawns one mmseqs easy-search per 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: object

A 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.fasta into cache_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.imgt.parse_human(path)[source]#

Parse IMGT/HLA, keeping classical loci collapsed to two-field resolution.

Parameters:

path (Path)

Return type:

list[MhcAllele]

tcren.mhc.imgt.parse_mouse(mouse_path, human_b2m_path)[source]#

Parse reviewed mouse H-2 / B2m and human B2M from UniProt FASTA headers.

Parameters:
  • mouse_path (Path)

  • human_b2m_path (Path)

Return type:

list[MhcAllele]

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.tsv are 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_cache when 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 unguarded createdb + createindex into the shared path would let every other process search a half-written index. The createindex marker is written last, so its freshness gates completeness.

Parameters:

cache_dir (Path)

Return type:

Path

tcren.mhc.reference.parse_header(header)[source]#

Parse a reference FASTA header back into its metadata fields.

Parameters:

header (str)

Return type:

dict[str, str]

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: object

Result 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.

Parameters:
  • structure (Structure) – A structure whose TCR/peptide chains are already typed.

  • sensitivity (float) – mmseqs search sensitivity.

Returns:

One MhcCall per chain that produced a reference hit.

Return type:

list[MhcCall]

tcren.mhc.mapper.calls_from_hits(candidates, best, key=None)[source]#

Build reconciled MhcCall`s for ``candidates` from precomputed mmseqs hits.

key(chain) -> str maps a candidate chain to its key in best (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.mapper.apply_mhc_calls(structure, calls)[source]#

Write MHC calls onto the structure’s chains in place.

Parameters:
Return type:

None

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.domains.canonical_groove()[source]#

Return the bundled canonical groove definitions.

Return type:

dict

tcren.mhc.domains.groove_for(mhc_class, chain_role)[source]#

Canonical groove definition for a (class, role), or None if none exists.

Parameters:
  • mhc_class (str)

  • chain_role (str)

Return type:

dict | None

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).

Parameters:
Return type:

None

tcren.mhc.regions.annotate_mhc(structure)[source]#

Map and partition the MHC chains of an (already chain-typed) structure.

Returns the MhcCall list and, in place, sets each MHC chain’s type (MHCa/MHCb/B2M), class supertype, allele and groove regions.

Parameters:

structure (Structure)

Return type:

list[MhcCall]

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 calling annotate_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: object

Result 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:

MhcAlignmentCheck

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 MPS region to each groove chain from the best-matching pseudosequence.

structure must already be chain-typed + MHC-annotated. Returns the chosen pseudosequence id (or None if 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 legacy dist <= 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 True add an extra n_atom_contacts column = the count of heavy-atom pairs within cutoff for that residue pair (always >= 1 for any kept row, and >= the single closest-atom row this function keeps). Default False keeps 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 True add sc.from, sc.to and n_sc_pairs: whether each side puts a side-chain heavy atom within cutoff of 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 True return every heavy-atom pair within cutoff instead 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 (plus n_atom_contacts when count_atoms is 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_sep in sequence are dropped; the default of 3 keeps i/i+3 and 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 PEPTIDE is 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_contacts heavy-atom-pair count (default True here, 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 no PEPTIDE chain.

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 Å). Mirrors all_atom_contacts()’ residue-pair schema (atom.from/atom.to carry 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) where matrix[a, b] is the Cα distance and keys[a] is the (chain_id, seq_index) of row/column a.

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: object

Three 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/d3 residue-pair tables with a layer column ("d1"/"d2"/"d3") and the layer’s distance. A residue pair can appear in several layers; callers filter by layer as needed.

Parameters:
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.start are 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 rbind semantics).

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 from and to sides — the input to tcren.contactmap.ContactMap.

When count_atoms is set, the n_atom_contacts per-residue-pair heavy-atom count is carried through (it is symmetric, so it survives the from/to swap unchanged). Default False keeps the table byte-identical to the legacy output.

atom_pairs passes through to all_atom_contacts(): every heavy-atom pair instead of each residue pair’s closest one. Chemical typing needs it. So does sidechain, which adds the sc.from/sc.to/n_sc_pairs side-chain-participation flags; the two sc flags 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: object

Annotated, 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()), with pos.from/pos.to added — populated only when built with peptide_internal=True, and deliberately kept out of contacts so 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_atoms is set, the annotated table carries an n_atom_contacts per-residue-pair heavy-atom count column (needed for atomic-weighted scoring). Default False keeps the contacts table byte-identical to the legacy output.

When peptide_internal is set, the peptide’s contacts with itself are collected into peptide_internal (at the 5 Å / 3-residue-separation defaults of tcren.peptide_internal_contacts() — call that directly to vary them). They are needed by the intra-peptide energy term (intra_weight on tcren.score_peptides() and tcren.pipeline.run()) and are stored apart from contacts, which is unaffected either way.

When sidechain is set, contacts carries sc.from, sc.to and n_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_pairs is set, contacts holds 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 map tcren.contact_types types 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:

ContactMap

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.to columns.

Return type:

DataFrame

tcr_peptide()[source]#

Convenience accessor for the TCR↔peptide interface.

Return type:

DataFrame

to_csv(path)[source]#

Write the full annotated contact table to CSV.

Parameters:

path (str | Path)

Return type:

None

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: object

Length-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``×``grid graphon 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_chains run).

  • grid (int) – output grid size G per axis.

  • target (str) – "peptide" (only target currently supported).

  • metric (str) – "distance" (Cα distances) or "contact" (binary at cutoff Å).

  • 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; None when 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) and y = j/(M+1) (where on the peptide each loop reads). The σ involution shows up as y_alpha + y_beta 1 (α reads the N-terminal half, β the C-terminal). The two loops are never pooled for y — the split is the point — but apex_x is 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 returns None.

Returns:

A ModeCentroid, or None if neither loop makes >= 3 contacts 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 — where other now 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 — the is_<type> booleans are independent and contact.type is 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.recognition were trained on its ct_* 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 the contact.type label.

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 predicate classify_contacts() needs.

Returns:

A set of (chain.id.from, residue.index.from, chain.id.to, residue.index.to) tuples, canonically ordered to match all_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(), carrying residue.aa.from/to, atom.from/to and dist.

  • scheme (str) – "v2" (default) or the frozen "v1" — see the module docstring.

  • stacking (set[tuple] | None) – residue pairs to mark as stacking, from stacked_pairs(). Ring geometry needs coordinates, which a contact frame does not carry, so it is passed in.

Returns:

The frame with a contact.type column (the highest-priority label) and, for "v2", an is_<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.type and the is_<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" needs structure).

  • 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, unlike classify_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) and pairs_<type> (distinct residue-residue pairs with >=1 contact of that type), e.g. pairs_hydrogen_bond is the documented n_hbond feature. Under "v2" a contact is counted under every type it satisfies, so the n_* values do not sum to the contact count.

Raises:

ValueError – for scheme="v2" without a structure.

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, from classify_contacts() with scheme="v2" or from residue_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: object

One 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: object

Geometry 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:
  • a (Ring)

  • b (Ring)

  • centroid_distance (float)

  • interplanar_angle (float)

  • vertical (float)

  • lateral (float)

a: Ring#
b: Ring#
centroid_distance: float#
interplanar_angle: float#
vertical: float#
lateral: float#
tcren.stacking.ring_of(residue, chain_id)[source]#

The ring of one residue, or None if it has none or is missing ring atoms.

Parameters:
Return type:

Ring | None

tcren.stacking.rings(source)[source]#

Every ring in a structure or a single chain, in residue order.

Parameters:

source (Structure | Chain)

Return type:

list[Ring]

tcren.stacking.ring_pair(a, b)[source]#

Geometry of one ring pair.

Parameters:
Return type:

RingPair

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 p1p2 axis, in degrees in (-180, 180].

Returns nan if 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.

Parameters:
Return type:

tuple[float, float, float]

tcren.torsions.chain_torsions(chain, structure_id='')[source]#

Per-residue phi/psi/omega for one chain.

Residues are taken in seq_index order, so a residue whose neighbour is unresolved gets a nan angle: 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). Set False to 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 length L pinned at R.

  • reachability_floor() — the shortest loop that can span a target at distance d.

  • span_saturation() — the fraction of maximal reach a structure actually uses, reach / reach_max; > 1 is 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-length loop pinned at neck.

Parameters:
  • length (int) – CDR3 loop length L (number of residues).

  • neck (float) – neck separation R in Å (the ext frame descriptor).

  • bond (float) – virtual Cα–Cα bond length (default BOND_CA).

Returns:

Maximum reach in Å, or 0.0 if 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 distance from a neck of separation neck.

Inverts reach_max(): the minimum L with reach_max(L, neck) >= distance.

Parameters:
  • distance (float)

  • neck (float)

  • bond (float)

Return type:

int

tcren.geometry.span_saturation(structure)[source]#

Per-loop reach / reach_max for cdr3a and cdr3b of a chain-typed structure.

Composes the existing CDR3 frame descriptors: reach (loop-centroid distance from the groove origin) and ext (neck separation R), with L the resolved CDR3 length. > 1 flags an infeasible pose (the loop cannot reach that far); real structures sit ~0.3–0.5. Returns NaN for 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 -> coordinates extractor: 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 _dihedral primitive for torsions.

Parameters:
  • structure – a chain-typed structure.

  • loop (str) – "cdr3a" (TRA) or "cdr3b" (TRB).

Returns:

A LoopInternalCoords, or None if 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: object

Virtual-bond internal coordinates of a CDR3 Cα trace (the input to the Julia loop model).

A Cα chain of N points has N-1 bonds, N-2 bond angles (interior vertices) and N-3 pseudo-torsions — the inverse of a discrete-Frenet reconstruction. neck is the end-to-end span |p_last - p_first| (the ext frame 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_RADIUS covers anything unlisted.

class tcren.clashes.ClashPair(peptide_residue, partner_residue, partner_chain_type, overlap)[source]#

Bases: object

A 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: object

Interface 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.0 if 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 top worst 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]#
worst: tuple[ClashPair, ...]#
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:

ClashReport

tcren.clashes.has_clashes(structure, *, tolerance=0.4)[source]#

Convenience predicate: does the peptide interface have any clash beyond tolerance?

Parameters:
Return type:

bool

The descriptor catalogue#

tcren.descriptors package#

Descriptors: the catalogue, the computation, and the batch dispatch, in three layers.

catalogue

what every column is – names, families, invariance classes, units, definitions and known defects. Pure data, no arithmetic.

compute

structure -> values for the interface block, and the calls out to the energetics, topology, potts and kinetics modules that own the rest.

table

a 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:

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. d in dPhi_* 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 as Phi_tcr_mhc) and ct_tp_hydrogen_bond (the same number as n_hbond, which is the name Eq. Q uses).

Type:

The core descriptor block recognize emits

tcren.descriptors.catalogue.INTERFACE_SYMMETRY_FEATURES = ('cdr3_dominance', 'cdr3_ab_imbalance', 'chain_cdr_imbalance')#

peptide contact counts (not energies), emitted as extra recognize output columns — not part of RECOGNITION_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 of RECOGNITION_FEATURES (that 40-column block is fixed). height = elevation of the CDR centroid above the groove plane, shift_u/shift_w its in-plane displacement from the peptide centroid along the groove long/short axes, offset the 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 --full output columns — not part of RECOGNITION_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; see DESCRIPTORS.

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} for g ∈ {tp, cdr12, cdr3a, cdr3b}) were removed in the 2026-07-28 audit. The tcren_* four were exact duplicates of the Phi_* energies; the mj_* four scored TCR:peptide contacts under the generic MJ potential, which is not the potential this method uses on that interface, and the d_* 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 energies F and their poly-alanine references dF. 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.

placement and interface were one geometry family 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 of placement, while it is a different question whether it is independent of interface. descriptors() keeps "geometry" and "physics" working as aliases.

involves_tcr is False for 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 with tcr_only=True (descriptors()).

Fitted and cohort-relative composites (p_real, p_real_bn, p_forced, p_bind, q_bind, s_strain) are listed under score. 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 geometry and topology are 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_ent is 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.

units is what the number is measured in – A, A^2, deg, rad, kT, N/m, or one of the dimensionless kinds count, 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 if None. 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 with family.

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 are NaN): docking geometry, per-interface energies (raw F and poly-alanine ΔF), contact-type tallies, interface ΔSASA burial, and the mhc_class_bin indicator. The structure is chain-typed and MHC-annotated in place. DESCRIPTORS gives each column’s family and whether the receptor enters its definition.

With full=True the row is extended with the 18 CDR3-frame descriptors (CDR3_FRAME_FEATURES) — the complete FULL_FEATURES vector.

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.

items is 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 — then recognition_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 is tcren.reliability.s_score() on the table. full also appends the intra-peptide columns PEPTIDE_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.

chunk is retained for signature compatibility and is no longer used.

autodetect_species searches organism and mouse so a mis-declared cohort is still typed correctly. That doubles the annotation cost, so pass False when the organism is known — it halves the mmseqs work and changes nothing else.

mechanics appends the tcren.mechanics koff 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 — running tcren mechanics separately 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 = -\frac{1}{\ln k}\sum_i p_i \ln p_i, \qquad D_q = \Big(\sum_i p_i^q\Big)^{1/(1-q)}\]

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_r7 and friends) are named from the radii argument 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 interface in tcren.recognition.DESCRIPTORS for 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, not n_contacts: through 2.19.0 it was written under the latter name, which tcren.potts also 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 without potts handed 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_FEATURES without the size counts, plus the radius-tagged Betti columns that radii produces.

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 ContactMap build covers both TCR interfaces. loop is "<chain>:<region>" restricted to CELL_LOOPS; target is "pep" or "mhc"; band is the peptide third ("pN"/"pM"/"pC") or "mhc". Counting is a single polars group_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_chains leaves an MHC chain typed generically as "MHC", and ContactMap.interface("tcr_mhc") matches on the supertype that tcren.mhc.annotate_mhc() assigns. Skip it and every TCR:MHC contact vanishes without an error – six of the twelve cells empty, p_germ_mhc collapses from ~0.78 to ~0.06, and H_cell is computed over a partition half of which is structurally unreachable.

Parameters:
  • structures (str | Path | Iterable[Structure]) – a directory, glob, .tar.gz or manifest of structures, or an iterable of already chain-typed Structure objects.

  • 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.id plus every feature of footprint_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 b0 is most informative at 7 A and the hole count b1 at 8 A, so both ship.

Returns:

{feature: value} over FOOTPRINT_FEATURES plus fp_b0_r<r>, fp_b1_r<r>, fp_chi_r<r> and fp_b0_frac_r<r> for each radius. Values are nan where the structure gives them no support (no contacts, a single contacted residue).

Return type:

dict[str, float]

Note

n_loop_contacts and its two components count the contacts the partition sees — those made by the six CDR loops. Framework contacts are outside CELL_LOOPS and are excluded by construction, so this is smaller than the full interface contact count, and it is a different quantity again from tcren.potts’s n_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_tp and ca_cb_agreement_tm are promotions: tcren.pose has computed all three since the pose layer was written and no catalogued family reached them. m_face_tp was called sidechain_toward there 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. Only m_face_tm is 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.pose has computed since the pose layer was written and that no catalogued family reached, because POSE_FEATURES is not DESCRIPTORS. They are the order-2 (participation ratio) reading of the same receptor-side degrees g_even_tcr reads at order 1, over TCR:peptide alone rather than the whole pMHC. Computed here from the contact map this module already builds, through tcren.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} over GRAPH_FEATURES and PROMOTED_POSE_FEATURES. Values are nan where 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 _tm columns need tcren.mhc.annotate_mhc() to have run, since MHC_TYPES matches the supertype it assigns; without it they are nan.

  • cutoff (float) – heavy-atom contact threshold in Angstrom, for the m_face_* pair.

  • sigma (float)

Returns:

{feature: value} over MATRIX_FEATURES, nan where 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 largest h_tcr h_pmhc clearance, in Å, at which a cell still counts as surface facing surface. Calibrated over 60 Native2026 crystals: inside COMPARE_WINDOW the 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 window surface_complementarity() compares over. DEFAULT_EXTENT is 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: object

A 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#
occupancy()[source]#

Fraction of grid cells that any surface point reached.

Return type:

float

to_frame()[source]#

Long form: one row per occupied cell, with the cell centre in Å.

Return type:

DataFrame

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 phobic channel — "kd" (Kyte-Doolittle) or "mj" (the hydrophobicity axis recovered from the MJ 1996 contact matrix by tcren.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 by surface_complementarity().

Returns:

A SurfaceMap.

Raises:

ValueError – if the groove plane or the pMHC atoms cannot be located.

Return type:

SurfaceMap

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:

relief

Standard 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_valley

Max minus min height over the same cells.

frac_above_ridge

Fraction of peptide cells that clear the MHC helix crest (RIDGE_PERCENTILE of 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_centre

Mean 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_mean

Mean formal charge over peptide cells.

area_frac_peptide

Share 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 grid and extent.

  • channel (str) – which channel to compare ("h", "phobic", "charge").

  • region (str | None) – restrict to cells of one source, e.g. "peptide"; None uses 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 with side="tcr". A cell enters the comparison when both maps reach it and the vertical clearance between them is at most max_gap — the Z cutoff that keeps the footprint to surface actually facing surface, rather than the map corners where the receptor overhangs nothing. See MAX_GAP for how the default was chosen.

Returned keys, all over the retained cells:

n_cells / coverage

Cells retained, and their share of the occupied pMHC cells inside window.

gap_mean / gap_sd

Mean and spread of h_tcr h_pmhc in Å. 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_asym

The 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_frac is 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_depth is the mean of -gap over those cells alone, in Å: how far the receptor reaches in where it does. gap_height is the mean of gap over the positive cells alone, in Å: how high it stands off where it does not. gap_asym is (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_index

The gap integrated over the contact plane, with the two signs kept apart because they mean opposite things and a mean cancels them. gap_vol is the void, \(\int \max(0, \mathrm{gap})\,\mathrm{d}A\), in ų; interlock is 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_index is gap_vol over 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_r

Pearson r between the two height fields. Positive is complementary: where the groove rises the receptor must ride up over it.

charge_r / charge_product

Pearson r between the two charge fields, and the mean of their product. Negative is complementary — plus meeting minus.

phobic_r / phobic_product

The same for hydropathy. Positive is complementary — apolar meeting apolar.

d_h / d_charge / d_phobic

Mean 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 a scipy.cluster.hierarchy.linkage() matrix, ready for dendrogram or fcluster.

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} over LITERATURE_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} over SURFACE_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 J supplies 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_FEATURES plus n_contacts (the pair count every value rests on) and n_cb_close. Descriptors that cannot be estimated — fewer than three contacts, a constant axis, no Cbeta-close pairs — come back as nan rather 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 the k terms 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 no from/to orientation 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 its n_atom_contacts heavy-atom-pair count).

Returns:

The summed energy; 0.0 when 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 of interface.

  • 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 its n_atom_contacts heavy-atom-pair count, so the energy tracks the LJ+Coulomb atom-pair sum more closely. "atomic" requires the contact map to have been built with count_atoms=True.

  • intra_weight (float) – weight w of the intra-peptide term, added as score = 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 with peptide_internal=True. The term is on the same energy scale as the interface sum, so w=1 treats 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, score sorted by complex.id then ascending score.

Return type:

DataFrame

tcren.energetics.scoring.score_structures(contact_maps, candidates, potential, **kwargs)[source]#

Score candidates against several structures and stack the results.

Parameters:
Return type:

DataFrame

class tcren.energetics.scoring.RecognitionMatrix(positions, aa, energy, side, interface)[source]#

Bases: object

Per-position × amino-acid substitution-energy landscape (see recognition_matrix()).

energy[i, a] is the summed pairwise potential over position positions[i]’s contacts when amino acid aa[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 are NaN for amino acids the potential leaves undefined (e.g. cysteine pairs in TCRen), exactly as score_peptides() drops those contacts — so reduce columns with np.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 of energy

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) and side="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 as score_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 for tcr_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 are RecognitionMatrix.aa.

Return type:

RecognitionMatrix

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.to on the tcr_peptide interface is the 0-based peptide index, because the peptide chain carries one full-length region starting at 0 — and tcren.refine.anchors has 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.aa and peptide.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 in peptide.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. Summing phi reproduces 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 phi over the central band, or nan for 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 – potential over TCR:peptide plus mhc_potential over peptide:MHC – which is the convention tcren.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, matching tcren.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 to score_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 structure is None, in which case contact_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 probabilitytcren.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. Always 0.0 for 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 native to alanine in turn and reports the ΔΔG of that single substitution. One row per peptide position.

With structure only 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 to score_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) and ddG (E(native) - E(Ala@pos)). Positions without TCR contacts yield ddG == 0.0. For interfaces that do not contain the peptide (e.g. "tcr_mhc") every position yields ddG == 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 by tcren.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 == 0 by 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_aa and ddG = E(native) - E(Ala@residue). A positive ddG marks 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_cdr3b and dPhi_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, mutant and ddG (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 descending ddG puts 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 is reference_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.register for 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 from tcren.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.0 for 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). See SMOOTH_INTERFACES.

  • beta (float) – inverse temperature in the potential’s units (default 1.0).

  • background – 20-vector of amino-acid frequencies in tcren.scoring.RecognitionMatrix column order, or None (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.pipeline assigns there).

  • weights (dict | None) – {interface: coefficient} override; None (default) reads the Native2026 scales through tcren.pipeline._phi_scale().

Returns:

{"dPhi": float, "varPhi": float, "n_positions": int}. Both sums are 0.0 over 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_chi defaults 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-2 atom to the depth n-1 atom, moving every atom deeper than that (Cα–Cβ for χ1 moves everything from Cγ out). Indices are into residue.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 within cutoffp_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) and p (the rotamer-averaged contact probability), one row per pair with p > 0. from is the interface’s first side (the TCR for "tcr_peptide"), as interface() 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 as tcren.pipeline._interface_energy(), with the hard 0/1 contact indicator replaced by contact_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 _relax packer.

This is what the rigid-body refiner could not do. substitute_peptide strips 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. See refine/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 table contact_probabilities() uses, mean field (each residue against its neighbours at their input conformation).

Parameters:
  • structure – chain-typed structure.

  • chains (tuple[str, ...] | None) – which chain_type values to repack. None repacks 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_chi conformers).

  • 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 a polars.DataFrame with chain.id, residue.index, residue.aa, n_conformers, energy (of the chosen conformer) and p_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 tensor K = Σ 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: object

The TCR↔pMHC spring network of one structure.

a/b are the (n, 3) Cα anchor coordinates on the TCR and pMHC sides; k the (n,) spring stiffnesses; rest the (n,) rest lengths |b a|; axis the 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:
  • structure (Structure) – An annotated structure (chains typed; peptide present).

  • cutoff (float) – Heavy-atom contact cutoff (Å) defining a spring.

  • weight (str) – Spring-stiffness model, one of WEIGHTS.

Returns:

The InterfaceSprings (empty arrays if no inter-body contact is found).

Return type:

InterfaceSprings

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. All nan if < 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 exceeds break_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" or stiffness_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. nan if < 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_total and n_interface (interface residue count, a size denominator).

Note: as a binding estimator these are weak/underpowered on current data — couple_pep tracks 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:
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() and coupling_residues() under their shipped defaults. This is the one definition of “the mechanics row”: both tcren mechanics and tcren recognize --mechanics call 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, shear or auto.

  • 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_total and n_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: object

TCR:peptide contact-stability summary.

Variables:
  • n_contacts (int) – Receptor-residue / peptide-residue contacts within cutoff.

  • mean_margin (float) – Mean cutoff - dmin over 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 a delta shift).

  • 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:

A StabilityReport.

Raises:

ValueError – If the structure has no peptide chain or no receptor chain.

Return type:

StabilityReport

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: object

How 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#
to_dict()[source]#
Return type:

dict

tcren.mechanics.dynamics.backbone_torsions(peptide_atoms)[source]#

φ/ψ torsions of a peptide as (axis_start, axis_end, moving_atom_indices).

peptide_atoms is [(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.0 includes them, 0.0 removes 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:

Stability

tcren.mechanics.dynamics.stability_table(structures, *, intra_weights=(1.0, 0.0), **kwargs)[source]#

Run peptide_stability() at each intra_weight and 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/energy at each weight (suffixed _intra1/_intra0 for 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: object

Composed 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:

CanonResult

tcren.docking.build_canonical_frame()[source]

(Re)compute R_canon for 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. Returns True when the VJ chain is on the +y side of the VDJ chain by more than margin Å (reverse dock), False for a canonical dock, and None when 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.

Parameters:

structure (Structure)

Return type:

Structure

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 ValueError if two source chains map to the same canonical id (unresolved multi-copy — run select_primary_complex() first).

Parameters:

structure (Structure)

Return type:

tuple[Structure, dict[str, str]]

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-class R_canon is reused, so the result is in the same frame as the dataset and the composed transform in the returned CanonResult replays 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 (.cif vs .pdb) and compress (trailing .gz); plain PDB by default (pass compress=True to 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 (threads worker threads, default os.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 structure onto a canonical database by MHC (see module docstring).

structure is chain-typed + MHC-annotated here unless annotate=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_dir defaults to data/Canonical2026. Returns the oriented, A–E renamed structure and the consensus OrientationResult (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.

structures is a file, directory, .tar.gz, or a shell glob. out is an output directory, or — for a single input — a structure file whose extension must match mmcif/compress. Annotation is BATCHED (one mmseqs pass over all inputs); only the mmseqs-free ensemble alignment + write runs on the thread pool (threads workers, default all cores). See tcren.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: object

TCR 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: object

Where the CDR footprint sits over the groove, in Angstrom, in the groove frame (u, w, n).

DockingAngles says 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() fixes n along the peptide-centroid-to-TCR-centroid direction and sets w = 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_z is 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 u side.

  • 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:

TcrPlacement

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 DockingAngles with 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:

DockingAngles

tcren.docking.crossing_incident_from_vector(v_canon)[source]

(crossing, crossing_signed, incident) degrees from a Vα→Vβ vector in canonical axes.

v_canon is [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: object

TCR: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:

DockingGeometry

tcren.docking.substitute_tcr(host, donor, *, by='mhc')[source]

Graft the donor TCR onto the host pMHC → a chimeric TCR:pMHC Structure.

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 by anchor:

  • "mhc" — superpose the donor MHC groove onto the host MHC groove (both inputs must be tcren.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()). Raises ValueError if by is 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.

Parameters:
Return type:

Structure

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 z is 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: object

TCR 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_canon is [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 DockingAngles with 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:

DockingAngles

class tcren.docking.angles.TcrPlacement(height, shift_u, shift_w, offset, n_cdr)[source]#

Bases: object

Where the CDR footprint sits over the groove, in Angstrom, in the groove frame (u, w, n).

DockingAngles says 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() fixes n along the peptide-centroid-to-TCR-centroid direction and sets w = 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_z is 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 u side.

  • 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:

TcrPlacement

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: object

TCR: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.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:

DockingGeometry

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 +z toward the TCR so the MHC sits at −z;

  • y = PC2 (the groove/peptide axis), signed +y toward 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: object

Composed 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:

CanonResult

tcren.docking.frame.build_canonical_frame()[source]#

(Re)compute R_canon for each class reference and return the artifact dict.

Writes nothing; the caller serialises to tcren/data/canonical_frame.json.

Return type:

dict

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: object

Rigid 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 structure onto a native reference by MHC.

structure must already be chain-typed and MHC-annotated (see tcren.mhc.annotate_mhc()). The reference (default a canonical complex for the structure’s MHC class) is loaded from the Native2026 dataset (tcren.paths).

Parameters:
  • structure (Structure)

  • reference_id (str | None)

Return type:

OrientationResult

tcren.docking.align.apply_transform(structure, result)[source]#

Return a copy of structure with the orientation transform applied to all atoms.

Parameters:
Return type:

Structure

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:

  1. Chain-type + MHC-annotate the query; read its MHC class (MHCI/MHCII) and species.

  2. Select every database structure with the same class and species (from the database’s orient_metadata.json).

  3. Superpose the query’s conserved groove Cα onto each selected database structure (sequence alignment establishes the residue correspondence, so alleles/numbering differ freely).

  4. 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 structure onto a canonical database by MHC (see module docstring).

structure is chain-typed + MHC-annotated here unless annotate=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_dir defaults to data/Canonical2026. Returns the oriented, A–E renamed structure and the consensus OrientationResult (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-class R_canon is reused, so the result is in the same frame as the dataset and the composed transform in the returned CanonResult replays 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 (.cif vs .pdb) and compress (trailing .gz); plain PDB by default (pass compress=True to 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 (threads worker threads, default os.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.

structures is a file, directory, .tar.gz, or a shell glob. out is an output directory, or — for a single input — a structure file whose extension must match mmcif/compress. Annotation is BATCHED (one mmseqs pass over all inputs); only the mmseqs-free ensemble alignment + write runs on the thread pool (threads workers, default all cores). See tcren.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.

Parameters:

structure (Structure)

Return type:

Structure

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 ValueError if two source chains map to the same canonical id (unresolved multi-copy — run select_primary_complex() first).

Parameters:

structure (Structure)

Return type:

tuple[Structure, dict[str, str]]

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 donor TCR onto the host pMHC → a chimeric TCR:pMHC Structure.

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 by anchor:

  • "mhc" — superpose the donor MHC groove onto the host MHC groove (both inputs must be tcren.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()). Raises ValueError if by is 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.

Parameters:
Return type:

Structure

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. Returns True when the VJ chain is on the +y side of the VDJ chain by more than margin Å (reverse dock), False for a canonical dock, and None when 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: object

A 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 like index; 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]#
h(aa)[source]#

One-body term of a residue.

Parameters:

aa (str)

Return type:

float

j(aa, bb)[source]#

Pair-specific term of a residue pair, with the one-body parts removed.

Parameters:
  • aa (str)

  • bb (str)

Return type:

float

energy(aa, bb)[source]#

Reassemble the original contact energy; equals the potential’s own value.

Parameters:
  • aa (str)

  • bb (str)

Return type:

float

class tcren.potential.model.HydrophobicityFit(name, c0, c1, c2, q, index, r2, eigenvalue_share)[source]#

Bases: object

A potential approximated as C0 + C1 (q_a + q_b) + C2 q_a q_b.

Variables:
  • name (str) – Name of the potential this came from.

  • c2 (c0, c1,) – Fitted coefficients.

  • 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#
eigenvalue_share: float#
value(aa, bb)[source]#

The fitted contact energy for a residue pair.

Parameters:
  • aa (str)

  • bb (str)

Return type:

float

one_body(aa)[source]#

C1 q_a – the per-residue term, which is what H(a) refers to.

Parameters:

aa (str)

Return type:

float

class tcren.potential.model.Potential(name, matrix, alphabet)[source]#

Bases: object

A 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 by residue.aa.to. Missing pairs are filled with nan. 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 of a less the grand mean, and J the 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 mean and both H terms, because they depend on one residue each. J is 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:

A PotentialDecomposition.

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:

PotentialDecomposition

components()[source]#

The three additive parts of decompose(), each as a scorable Potential.

decompose() splits the matrix as e(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 q gives the form above, with q ordering 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 just C2 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 its r2 before relying on it; the form is an approximation, not an identity, unlike decompose().

Raises:

ValueError – If the matrix is not square and symmetric.

Return type:

HydrophobicityFit

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:

  • wideresidue.aa.from, residue.aa.to, <name> (e.g. TCRen_potential.csv with a TCRen value column).

  • longresidue.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:

Potential

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 both and nothing else.

Load the bundled classic TCRen potential (cached; treat as read-only).

Return type:

Potential

tcren.potential.model.tcren2()[source]#

Load the bundled TCRen2 potential (cached; treat as read-only).

The redundancy-balanced derivation over the 362 fully annotated αβ Native2026 crystals, the default TCR:peptide potential since 2.11.0, and the matrix the TCRen2 manuscript reports. It is not interchangeable with tcren(): 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:

Potential

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() and keskin() are raw contact energies (mean ≈ −3.3) and betancourt() is the other pair-form matrix. Compare like with like, and see Potential.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:

Potential

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 bundled mj() 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 bundled MJ matrix at r = 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 Potential shape 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:

Potential

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.98 with the hydrophobicity axis recovered by Potential.hydrophobicity_fit() from mj1996(), 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 KESO980102 is the residue-mediated one, also available through aaindex().

Every entry is negative, from -7.23 to -0.50, so this is a raw contact matrix in the same reference state as mj1996() and not in the pair-contact reference state of the bundled mj() (mixed sign, mean -0.08). Compare it against mj1996; comparing it against mj compares 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:

Potential

tcren.potential.model.betancourt()[source]#

Betancourt–Thirumalai contact energies, the B matrix, 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 bundled mj(), 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 over ARNDCQEGHILKMFPSTWYV, 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:

Potential

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 directedfrom is a TCR residue and to a peptide residue — so N[a,b] and N[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.to and count.

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.to and (for filtering) pdb.id.

  • include (list[str] | None) – If given, keep only contacts whose pdb.id is in this list.

  • exclude (list[str] | None) – If given, drop contacts whose pdb.id is 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/beta over 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 to False when symmetric is 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 whose pdb.id is absent from the map default to weight 1.0); this down-weights redundancy while keeping all data (see tcren.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-structure weights. 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 symmetric value[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. Default False keeps the directed TCR→peptide potential, which is the shipped TCRen_potential.csv.

  • smooth_beta – Substitution-matrix pseudocount weight (tcren.potential.smoothing.smooth_counts()), applied to the pair counts before the log-odds. A cell holding smooth_beta observations 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. 0 disables it. Applied after smooth_beta when 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 the pseudocount mass over the grid instead of spreading it uniformly – see tcren.potential.smoothing.composition_prior(). The total mass added is unchanged, so pseudocount means the same thing either way. None keeps the flat prior. NOT USED FOR TCRen2.

Returns:

The derived Potential. For "am" the long matrix additionally carries a count column.

Return type:

Potential

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 both and 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.id stacking 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_types separates 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_contacts per type, and frac_cells_ge_min — the share of the 400 cells that reach min_count observations. 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; stacking 13. 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 shipped TCRen_potential.csv at 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:
Returns:

(potentials, report) — a {contact_type: Potential} mapping and a polars frame with contact.type, n_contacts, n_cells_observed, frac_cells_ge_min, median_count.

Raises:

ValueError – if contacts has no contact.type column.

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 both and nothing else.

Non-redundant representative pdb.id for each cluster of similar complexes.

Clusters structures by the summed Damerau–Levenshtein distance over fields (CDR3α + CDR3β + peptide) using hierarchical clustering, then returns one representative pdb.id per cluster (the first in row order).

Parameters:
  • markup (DataFrame) – Per-structure table with a pdb.id column and the fields columns.

  • t (float | None) – Distance cutoff for fcluster (criterion="distance"); None turns redundancy filtering off and returns every pdb.id unchanged.

  • 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.id values, 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 both and 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 weight 1 / cluster_size. A unique structure gets weight 1.0; each member of a size-k redundancy cluster gets 1/k, so the cluster contributes a total weight of 1 to the derivation. Feed the result to tcren.potential.derive.derive_tcren()’s weights argument to down-weight redundancy while keeping every structure’s data.

Parameters:
  • markup (DataFrame) – Per-structure table with a pdb.id column and the fields columns.

  • 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 i with n_a(i) structures sharing its value on axis a,

\[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, not 1/9, because it is a genuinely new recognition event. A structure unique on every axis gets 1.0; a true re-solve, duplicated on all of them, gets 1/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.id and every column named in axes.

  • 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 both and nothing else.

One-epitope-one-vote weight for each pdb.id.

Every structure carrying a given peptide gets weight 1 / n, where n is the number of structures in the set with that peptide, so each distinct epitope contributes a total weight of 1 to the derivation however often it was crystallized. A peptide seen once gets weight 1.0.

The single-axis case of balanced_weights(). It corrects epitope bias only; in Native2026 receptor 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.id and field columns.

  • 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.id of complexes whose receptor contacts are exclusively TRA/TRB (αβ TCRs).

Parameters:

contacts (DataFrame) – Contact table with pdb.id and chain.type.from columns.

Returns:

The pdb.id values whose set of chain.type.from is 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:

energy

A pairwise contact energy. The 42 entries a scoring pipeline can use.

count

Observed contact counts, not energies (TANS760102, MIYS960103).

distance

Side-chain centre separations in angstroms (BONM030104BONM030106).

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: object

One parsed AAindex3 record.

Variables:
  • accession (str) – The H field, e.g. "MIYS990106".

  • description (str) – The D field, one line of prose.

  • authors (str) – The A field, verbatim.

  • title (str) – The T field, verbatim (AAindex truncates long titles).

  • journal (str) – The J field, verbatim.

  • pmid (str) – The R field with the PMID: prefix stripped, or "". AAindex sometimes cites the paper that tabulated a matrix rather than the one that derived itMIYS850102 carries 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 cells nan.

  • 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 - or NA; four entries have 39 of them.

to_potential()[source]#

This entry as a Potential, named for its accession. Missing cells are dropped.

Return type:

Potential

tcren.potential.aaindex.parse_aaindex3(text)[source]#

Parse an AAindex3 flat file into one AAindexEntry per accession.

The M block 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 - or NA becomes nan rather than an error, because four entries genuinely omit one residue’s whole row and column.

Parameters:

text (str) – The contents of an aaindex3 flat file.

Returns:

Accession -> entry.

Raises:

ValueError – If a record’s M block 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:

Potential

tcren.potential.aaindex.entry(accession)[source]#

The parsed record itself, including the non-energy tables aaindex() refuses.

Parameters:

accession (str)

Return type:

AAindexEntry

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.

mean is 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 (see Potential.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() and keskin() were identified this way, at max |delta| = 0 over 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: object

Fit the per-column parameters on a reference set, then apply anywhere.

Only yeo carries 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]

fit(X)[source]#
Parameters:

X (ndarray)

Return type:

Transformer

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.transform.working_set(*, receptor_task=True)[source]#

The descriptors a model may read: catalogue minus determined, banned, and (optionally) the five computed without the receptor.

Parameters:

receptor_task (bool)

Return type:

list[str]

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: object

Two 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:

Joint

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. features and manifest are polars frames keyed on pdb.id.

manifest needs the id column, y (1 binder / 0 non-binder) and epitope; an iptm column 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, whereas Q has 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. reference calibrates against another cohort.

Passing reference is what makes strain_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 x itself (cohort-relative).

  • method"z" (mean/sd, the default) or "rank" — the percentile of each x against 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 of Q (chain balance, H-bond and contact counts are not normal). On the benchmarks z and rank agree to Spearman ρ≈0.98 and differ by <0.005 AUROC, so z is kept as the default; use rank when 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, default native_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=False recovers the legacy equal-weight mean \(Q=\frac1k\sum_k z(d_k)\).

Parameters:
  • table – the tcren recognize --full table (dict / pandas / polars).

  • reference – cohort defining \(\mu,\sigma,C\). None uses native_reference() when decorrelate (so the covariance is defined for any input, incl. one structure); with decorrelate=False it means cohort-relative (the table itself).

  • features – the k descriptors. Default Q_FEATURES_GEOM (k=4, geometry only) — the validated default. Adding the pp_combo energy 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" — see zscore().

  • 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. None keeps \(\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_TERMS contact energies, so higher = more binder-like and it is on the same z-scale as q_score(). Unlike Q (interface geometry), F reads the actual contact chemistry — and unlike Q it 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; see PHI_TERMS.

Cohort-relative (standardized over the ranked set); pass reference to standardize against another cohort (see zscore()).

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 erf rather 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.

  1. 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.

  2. 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.

  3. 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 with coupling(), 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 at n = 1, where the cohort estimator is not (at n = 2 it 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.0 if 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_TERMS with fixed physical signs. Pass the crystal cohort as reference to 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 term pp_combo is 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 the reference argument. Provenance: tcren recognize --full over 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_combo is 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_CORE to q_score() for the simpler, marginally better score.

Type:

The four load-bearing descriptors. n_pep_contacted is dropped

tcren.cohort.Q_FEATURES_GEOM = ('burial', 'n_pep_contacted', 'chain_balance', 'n_hbond')#

The four geometry-only descriptors — Q_FEATURES without the pp_combo energy contrast. This is Q_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 to q_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_pep is the TCR:peptide TCRen energy, Phi_tcr_mhc the TCR:MHC energy; both are emitted by tcren recognize. They are raw energies (lower = tighter), so phi_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_pep ranks binders at AUROC 0.36 (backwards), on the clean YLQPRTFLL pose at 0.59. Use it only conditioned on pose quality — gate with strain_z(), or read z(Q)-z(F) on forced poses and z(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 (or None) 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:

True only if n_contacts >= N_CONTACTS_MIN and scanning_angle lies in SCANNING_RANGE and pitch_angle lies in PITCH_RANGE; False otherwise, 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 (or None) 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:

True only if n_contacts >= N_CONTACTS_MIN and scanning_angle lies in SCANNING_RANGE and pitch_angle lies in PITCH_RANGE; False otherwise, 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 n peptides 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 n upper-case peptide strings, each of length length.

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 peptide together 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 smaller rank_pct indicates 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 background is 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 to score_peptides.

  • background (Iterable[str] | None) – Explicit background peptides; when None a uniform-random background of length len(peptide) is generated.

  • contact_weight (str) – "residue" (default) or "atomic"; passed through to score_peptides ("atomic" needs an n_atom_contacts contact map).

Returns:

Mapping with keys peptide, score (native energy), rank_pct (fraction of background with score <= native), and n_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 is tcren.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 position i is 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_mhc is 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 reference argument.

class tcren.cpl.ResponseMatrix(peptide, positions, interface_class, aa, phi, phi_tcr, phi_mhc, structure_id)[source]#

Bases: object

A predicted CPL response matrix: peptide positions x twenty amino acids.

phi[i, a] is the total interface energy of the template with amino acid aa[a] threaded at positions[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() (or mutation_effect() / position_scan() / equimolar_effect()) rather than reading phi directly.

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

aa: tuple[str, ...]#

the twenty column labels, AA20

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 effect column. None (default) emits both as effect_equimolar and effect_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 to tcren.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 as tcren.ddg.ddg()’s weights: replace the map’s hard 0/1 contact indicator with a contact probabilitytcren.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:

A ResponseMatrix.

Raises:

ValueError – if the complex has no peptide contacts at all, or if peptide has the wrong length. Both are silent-empty failures otherwise: an empty matrix is indistinguishable from a genuinely flat one.

Return type:

ResponseMatrix

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-scan ddG.

Returns:

A single number. Under "wild_type" the template’s own residue returns exactly 0.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:
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 i is the equimolar mixture, so the cost of giving up a defined residue for that mixture is

mean_b Phi(x_{i->b}) - Phi(x_{i->a})

– exactly the "equimolar" reference. A positive result means aa is 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, or None if 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 of tcr_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:
  • pmhc_source (Structure) – the complex whose peptide + MHC (and their coordinates) are kept.

  • tcr_source (Structure) – the complex whose TCR is grafted on.

  • pdb_id (str | None) – id for the decoy (default "<pmhc>__tcr_<tcr>").

Returns:

The decoy Structure.

Raises:

ValueError – if pmhc_source has no pMHC or tcr_source has no TCR.

Return type:

Structure

tcren.shuffle.make_decoys(structures, n_per=10, within_class=True, seed=0)[source]#

Yield decoy structures: each input pMHC paired with n_per distinct 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 given seed.

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 Structure objects.

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 n decoys 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:

  1. import the structure (C-gene trimmed);

  2. annotate chains — TCR loci/CDRs via arda, MHC allele/class/role + groove regions;

  3. superimpose onto the canonical database (canonical Cα frame; optional);

  4. markup + contacts — the per-residue region table and the 5 Å contact map;

  5. 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: object

Everything the pipeline produces for one structure.

extra carries the interface-sanity flag when the complex was oriented (superimpose=True): real_interface (boolFalse marks assay noise / a failed dock; see tcren.binder.is_real_interface()), and the raw descriptors it was computed from (n_contacts, scanning_angle, pitch_angle). With superimpose=False the docking angles are unavailable, so real_interface is None (scanning_angle/pitch_angle None) while n_contacts is still set.

Parameters:
  • pdb_id (str)

  • mhc_calls (list[MhcCall])

  • markup (DataFrame)

  • contacts (DataFrame)

  • scores (dict[str, float])

  • oriented (Structure | None)

  • rmsd (float | None)

  • extra (dict)

pdb_id: str#
mhc_calls: list[MhcCall]#
markup: DataFrame#
contacts: DataFrame#
scores: dict[str, float]#
oriented: Structure | None#
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 (default data/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 a Potential, a bundled name ("tcren2"/"karnaukhov2022"/"mj"/"keskin"), a CSV path, or None. 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 its n_atom_contacts heavy-atom-pair count (the contact map is then built with count_atoms=True). Applies to tcr_peptide, tcr_mhc and peptide_mhc alike.

  • reference_aa (str | None) – if set (typically "A"), also report the reference-normalised energies delta_<interface> and delta_total — each interface’s tcren.ddg.reference_delta(), i.e. its energy minus the energy of a poly-reference_aa peptide threaded onto the same contact map. Off by default, so the default scores dict is unchanged.

  • typed (bool) – the structure’s chains are already typed, so skip classify_chains(). Chain typing costs one mmseqs easy-search per 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 when structure is an already-parsed Structure.

  • 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 folds intra_weight * that energy into scores["total"]. 0.0 (default) computes nothing and leaves scores unchanged. Its potential is MJ unless potentials["peptide_internal"] overrides it.

Returns:

A PipelineResult with the markup, contacts, per-interface scores and (if requested) the canonical-frame oriented structure.

Return type:

PipelineResult

tcren.pipeline.score_row(result)[source]#

Flatten a PipelineResult to a one-row scores dict (for a CSV table).

The d_* reference-normalised columns are present only when the pipeline was run with reference_aa set, and Phi_pep_int only when it was run with a non-zero intra_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+S2tcren.pipeline.run() (annotate → orient → contacts → per-interface scores), giving the scores, markup and contacts frames;

  • S3tcren.scoring_rank.percentile_rank() of the structure’s native peptide against a random pMHC background, giving the rank frame;

  • S4tcren.ddg.alanine_scan() of the native peptide, giving the ddg frame.

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 rmsd to scores).

  • 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 the rank and ddg frames.

  • 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 the ddg frame. When False (default) the scan is skipped and ddg is an empty frame with the same schema.

  • contact_weight (str) – "residue" (default, legacy: each contacting residue pair contributes potential[a, b] x 1) or "atomic" (each pair contributes potential[a, b] x n_atom_contacts). Applies to all three interfaces of the scores frame and to the rank/ddg frames. "residue" keeps every output byte-identical to the legacy facade.

Returns:

  • scores — one row of per-interface energies (and rmsd if superimposed), identical to tcren.pipeline.run()’s scores.

  • rank — one row: the native peptide’s energy and its rank_pct against the background.

  • ddg — the alanine scan (columns pos/wt_aa/ddG); empty unless alanine=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 structure with the peptide chain threaded to new_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_peptide must 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_peptide all-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.

Raises:

ValueError – if there is no peptide chain, the length differs, or a code is non-standard.

Parameters:
  • structure (Structure)

  • new_peptide (str)

  • chain_type (str)

Return type:

Structure

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 within shell Å of the peptide are considered. The structure must be chain-typed (peptide = chain of chain_type == 'PEPTIDE'). Requires the compiled _refine ext + 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 within shell Å of the peptide are considered (beyond DOPE’s range they contribute nothing). Requires the compiled tcren._relax ext + the bundled DOPE table.

Parameters:
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 (see tcren.mhc.annotate_mhc()) to use its real class assignment; otherwise the class is inferred from peptide length.

Parameters:
Return type:

Decomposition

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: object

Anchor 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 model vs reference in the MHC-groove frame.

Both structures must be chain-typed and MHC-annotated. model is superposed onto reference by their shared groove Cα; the peptide RMSDs are then computed over atoms (and residues) present in both. anchors are 0-based peptide residue indices for the anchor-Cα RMSD.

Parameters:
Return type:

PeptideRMSD

class tcren.refine.PeptideRMSD(backbone_rmsd, ca_rmsd, anchor_ca_rmsd, n_backbone, n_ca, groove_rmsd)[source]#

Bases: object

Peptide 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:

A RegisterReport.

Return type:

RegisterReport

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/dope run out of the box; openmm/promod3 optional).

  • seed (int) – RNG seed for engines that sample.

  • **engine_kwargs – Forwarded to the engine (e.g. n_steps for dope).

Returns:

A tcren.refine.engines.base.ModelResult with 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: object

Register / 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 (Å); nan without a reference.

  • anchor_rmsd (float) – Anchor-Cα RMSD vs the reference (Å) — the register signal; nan without one.

  • groove_rmsd (float) – MHC-groove superposition residual (Å); nan without a reference.

  • wrong_register (bool | None) – True/False when a reference is given (anchor RMSD over the cut), None when 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 = N and c = C , the Cβ sits along a fixed combination of b, c and 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 structure with 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:

Structure

tcren.refine.substitute.substitute_peptide(structure, new_peptide, chain_type='PEPTIDE')[source]#

Return a copy of structure with the peptide chain threaded to new_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_peptide must 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_peptide all-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.

Raises:

ValueError – if there is no peptide chain, the length differs, or a code is non-standard.

Parameters:
  • structure (Structure)

  • new_peptide (str)

  • chain_type (str)

Return type:

Structure

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: object

Register / 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 (Å); nan without a reference.

  • anchor_rmsd (float) – Anchor-Cα RMSD vs the reference (Å) — the register signal; nan without one.

  • groove_rmsd (float) – MHC-groove superposition residual (Å); nan without a reference.

  • wrong_register (bool | None) – True/False when a reference is given (anchor RMSD over the cut), None when 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:

A RegisterReport.

Return type:

RegisterReport

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/dope run out of the box; openmm/promod3 optional).

  • seed (int) – RNG seed for engines that sample.

  • **engine_kwargs – Forwarded to the engine (e.g. n_steps for dope).

Returns:

A tcren.refine.engines.base.ModelResult with 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_class in {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: object

Anchor 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 peptide into anchor (groove-facing) and TCR-facing parts.

Parameters:
  • peptide (str)

  • mhc_class (str | None)

Return type:

Decomposition

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 (see tcren.mhc.annotate_mhc()) to use its real class assignment; otherwise the class is inferred from peptide length.

Parameters:
Return type:

Decomposition

tcren.refine.anchors.native_peptide(structure)[source]#

One-letter sequence of the structure’s PEPTIDE chain (raises if absent).

Parameters:

structure (Structure)

Return type:

str

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: object

Peptide 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 model vs reference in the MHC-groove frame.

Both structures must be chain-typed and MHC-annotated. model is superposed onto reference by their shared groove Cα; the peptide RMSDs are then computed over atoms (and residues) present in both. anchors are 0-based peptide residue indices for the anchor-Cα RMSD.

Parameters:
Return type:

PeptideRMSD

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 within shell Å of the peptide are considered (beyond DOPE’s range they contribute nothing). Requires the compiled tcren._relax ext + the bundled DOPE table.

Parameters:
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_peptide into structure’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). None re-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_targets for ccd, n_steps for dope).

Returns:

A ModelResult with the refined structure, engine score, and anchors used.

Return type:

ModelResult

class tcren.refine.model.ModelResult(structure, energy, engine, anchors, iterations=0, info=<factory>)[source]#

Bases: object

Outcome of (re)modelling a peptide into the groove.

Parameters:
  • structure (Structure)

  • energy (float)

  • engine (str)

  • anchors (tuple[int, ...])

  • iterations (int)

  • info (dict)

structure: Structure#
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 via pyrosetta-installer (academic license). No external binary needed.

  • External binary — a licensed FlexPepDocking executable 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.pyrosetta_available()[source]#
Return type:

bool

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 FlexPepDocking binary. The structure should carry full chains (imported with keep_c_gene=True). Raises RuntimeError if neither route is available.

Parameters:
  • structure (Structure)

  • rosetta_bin (str | None)

  • seed (int)

  • nstruct (int)

  • extra_flags (tuple[str, ...])

Return type:

Structure

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.

exception tcren.refine.engines.base.EngineUnavailable[source]#

Bases: RuntimeError

Raised 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: object

Outcome of (re)modelling a peptide into the groove.

Parameters:
  • structure (Structure)

  • energy (float)

  • engine (str)

  • anchors (tuple[int, ...])

  • iterations (int)

  • info (dict)

structure: Structure#
energy: float#
engine: str#
anchors: tuple[int, ...]#
iterations: int#
info: dict#
class tcren.refine.engines.base.Engine(*args, **kwargs)[source]#

Bases: Protocol

A 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:
Return type:

ModelResult

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().

class tcren.refine.engines.dope.DopeEngine[source]#

Bases: object

name = 'dope'#
available()[source]#
Return type:

bool

run(structure, decomp, *, seed=0, n_steps=2000)[source]#
Parameters:
Return type:

ModelResult

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'#
available()[source]#
Return type:

bool

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:

ModelResult

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'#
available()[source]#
Return type:

bool

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:

ModelResult

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.

class tcren.refine.engines.promod3_engine.ProMod3Engine[source]#

Bases: object

name = 'promod3'#
available()[source]#
Return type:

bool

run(structure, decomp, *, seed=0, anchor_targets=None)[source]#
Parameters:
Return type:

ModelResult

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_HOME when set; otherwise the source checkout, recognised by its pyproject.toml. An installed wheel has no checkout above it — parents[2] is then site-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_DIR or data/ under tcren_home().

Return type:

Path

tcren.paths.native_dir()[source]#

Directory holding the canonical Native2026 structures (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/Native2026 first; if absent (e.g. a pip-installed library with no repo data/), 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 FileNotFoundError if 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:

id

structure stem; the join key. Required.

y

binding label, 1/0, when the set carries one

epitope

peptide sequence

mhc

allele, e.g. HLA-A*02:01

iptm

AlphaFold/TCRmodel2 interface pTM

plddt

mean pLDDT

ptm

pTM

ranking_confidence

the generator’s own ranking scalar

provenance

free text: what produced this structure

Any further column is passed through untouched.

tcren.metadata.find_metadata(structures)[source]#

The metadata.tsv governing structures, or None.

structures may 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 None if it has none.

Raises:

ValueError – if the file exists but has no id column, 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 features output, or anything keyed by on.

  • structures (str | Path) – the structure set, so its metadata.tsv can be found.

  • on (str) – the key column in table. Default complex.id.

  • columns (tuple[str, ...] | None) – restrict to these metadata columns (id is always kept). None takes all.

Returns:

table with 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 table lives: the table’s path with .provenance.json appended.

Parameters:

table (str | Path)

Return type:

Path

tcren.provenance.stamp(table, *, command, columns=None, extra=None)[source]#

Write the sidecar for table and 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 None to 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, or None if it has none.

Parameters:

table (str | Path)

Return type:

dict | None

tcren.provenance.check(table, *, require=True)[source]#

Raise StaleTableError unless table was 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

exception tcren.provenance.StaleTableError[source]#

Bases: RuntimeError

A generated table does not match the installed descriptor catalogue.

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 the nonred flag.

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

tcren.analysis.compare_potentials(a, b)[source]#

Join two potentials on their amino-acid pairs and add their difference.

Returns residue.aa.from, residue.aa.to, value_a, value_b, diff over shared pairs.

Parameters:
Return type:

DataFrame

tcren.analysis.potential_matrix(potential)[source]#

Dense matrix + (from_labels, to_labels) for plotting a potential heatmap.

Parameters:

potential (Potential)

Return type:

tuple[ndarray, list[str], list[str]]

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_transform would 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: object

2D 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:

A ProjectionResult.

Return type:

ProjectionResult

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/z and in-plane u/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-atom contacts_table() (which gives the dashed inter-residue edges).

Parameters:
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_types so 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 on aromatic or hydrophobic at 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. stacking cannot 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.

kind selects the contact definition: "closest" (closest heavy-atom pair, all_atom_contacts(), default 5 Å — the original TCRen definition, and the only kind that carries a contact_type bond 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_dist plus, for kind="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 with n_contacts. For kind="closest" it adds a column per bond type (one n_<type> per tcren.contact_types.TYPES_V2 entry bar stacking), 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.project2d.pockets.pocket_markers(markup)[source]#

Place A–F markers along the projected peptide (u, v) track.

Returns columns pocket, peptide_pos, u, v (empty if the peptide is not projected).

Parameters:

markup (DataFrame)

Return type:

DataFrame

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_chain is 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.SurfaceMap channel 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 channel is 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

x

PC3, the thin axis

groove width

PC2_MHC, groove width

y

PC2, +y to peptide C-term

peptide N→C

PC1_MHC, groove long axis

z

PC1, +z toward the TCR

pMHC→TCR

PC3_MHC, groove normal

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: object

One 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. short is 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-ito is 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 arm is 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 palette is 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 than Axis.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 rotation is not nine floats.

Return type:

str

tcren.viz.pymol.probe_rotation(scene, *, pymol_bin=None)[source]#

Run scene and 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.zoom leaves the rotation alone but cmd.turn and cmd.orient do 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 pymol executable.

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 scene to png, 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 pymol executable.

  • **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 corner is unknown or scale/margin leave 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_dir as <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. phi is the energy share, negative being favourable; n_contacts is 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_mhc or peptide_mhc.

  • cutoff (float) – Heavy-atom contact distance (Å).

  • potential – A tcren.potential.Potential; defaults to the bundled TCRen for tcr_peptide and MJ for the MHC interfaces, matching tcren.pipeline.

  • tcr_regions (str) – Which TCR regions count — all, cdr or cdr+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 spectrum reads.

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) and n_contacts (geometric share) come from residue_importance(); any other numeric column of the frame works too, which is how a predicted engagement from tcren.potts.contact_map() is coloured onto the peptide.

  • regions – Which region types to draw as coloured sticks. PEPTIDE matches 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 by is not a column of importance.

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’s spectrum reads.

This is the picture of tcren.potts.contact_map() with by="position": how engaged the contact model expects each peptide residue to be, before any residue identity is scored. Unlike importance_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.index and by.

  • 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 importance is empty or by is 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.

tcren.viz.palette.color_for(complex_chain, complex_region)[source]#

Resolve a fill color: region shade if known, else chain hue, else grey.

Parameters:
  • complex_chain (str | None)

  • complex_region (str | None)

Return type:

str

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_0000XXXX accessions) — 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 ids from RCSB into dest as .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.recent.native2026_ids()[source]#

PDB ids of the local Native2026 set (the seed for a pdb_recent refresh).

Return type:

list[str]

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_download when available (robust LFS download with resume + retries); otherwise falls back to a retrying requests loop. 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, paper source_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/data by default).

Each set lands in its own folder directly under data_dir (e.g. notebooks/data/ Native2026/) — no structures/ 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. Pass canonical=True to also fetch the re-oriented Canonical2026 set.

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_atoms is set, an extra n_atom_contacts column (the heavy-atom-pair count per residue pair) is carried through for atomic-weighted scoring. Default False keeps the schema byte-identical to the legacy output.

contact_types adds contact.type from tcren.contact_types.residue_pair_types(). Without it a cached contact table cannot be typed after the fact — atom.from, atom.to and dist are 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 keys and report row-set + max numeric differences.

Returns {rows_old, rows_new, matched, only_old, only_new, max_abs_diff, status} where status is "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 for run_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 ΔΔG e_native scorer).

  • tcren mechanics — interface mechanics (stiffness / rupture / coupling) — the koff proxies.

  • tcren scoring — per-interface contact energies Φ (--delta for ΔΦ, --geometry for 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: fit a coupled model over the residue pairs that could have contacted, score a structure’s map (energy, log Z, likelihood), contacts for per-residue-pair contact probabilities, map to 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.info()[source]#

Show version and dependency availability.

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 — --regions restricts the output to one chain class. --pseudo additionally marks the NetMHCpan MHC pseudosequence residues (region MPS). MHC groove + MPS require 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 Canonical2026 set is produced; use superimpose to 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 n different 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 — run tcren orient first.

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).

-s accepts a file, directory, .tar.gz, or a shell glob. -o is an output directory, or — for a single input — a structure file whose extension must match --mmCIF/--compress. Annotation is one batched mmseqs call; -t threads 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 -i CSV or as a --structure-dir of PDBs (assembled via annotate_structure_set); pass exactly one. With a structure directory, --redundancy-t additionally 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.sh does this). Fetches Native2026 (orientation references) and, by default, Canonical2026 (the default superimpose database) into $TCREN_DATA_DIR / repo data/. 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-weight adds 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-weights and --soft all 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-scan walks 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. --side chooses which side is walked: peptide (default), tcr (the contacted CDR residues) or both. --virtual takes the fast path instead, re-indexing the mutant on the native map with no atoms moved – peptide side only.

--mutant scores 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_equimolar scores a residue against the 1/20 mixture, which is the CPL background and the right axis to compare with a measured matrix; effect_wild_type scores 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 -c is omitted) together with --background random peptides of the same length and reports rank_pct — the fraction of background scoring at least as well (lower energy = better binder, so a small rank_pct means 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: --compare writes the pairwise Manhattan map distance, which clusters structures of the same epitope together.

--side tcr maps the receptor’s underside in the same frame instead, and --complementarity builds 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 energies Phi_{tcr_pep,tcr_mhc,pep_mhc} and poly-alanine dPhi, CDR-loop energies Phi_{cdr12,cdr3a,cdr3b}, contact-type tallies, ΔSASA burial and the MHC-class indicator. --full also emits the 18 CDR3-local frame descriptors (the FramePose strain layer) and the intra-peptide term Phi_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_bind and s_strain were 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 --features returns instead is Q, T and S, none of which fits anything at call time; the two-class read-outs are tcren assess, whose frozen model refits from a manifest that ships in the wheel. Output is TSV.

--mechanics appends the koff proxies tcren mechanics reports — 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.npz matches 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 --manifest to fit a different hold-out, then read it back with tcren 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_mhc are the three interface terms Φ_TP, Φ_TM, Φ_PM; Phi_total is their sum Φ. With --delta each also gets its poly-alanine-referenced counterpart dPhi_* (ΔΦ_TP, ΔΦ_TM≡0, ΔΦ_PM) and dPhi_total = ΔΦ. The names match tcren recognize, so the two tables join on pdb.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 w adds the term the three interface sums omit — Phi_pep_int, the peptide’s contact energy with itself (5 Å, sequence separation >= 3, MJ) — and folds w x Phi_pep_int into Phi_total. The energy is reported raw, so the term and the weight stay separable.

--geometry appends the interface descriptors (buried surface burial, peptide coverage n_pep_contacted, chain_balance, n_hbond, docking pitch/crossing) and Q — 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, use tcren features; for the scores built on it, tcren recognize (Q, T, S) and tcren 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. -t sets 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 0 uses 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 _relax kernel).

Sums the DOPE potential over peptide↔partner heavy-atom pairs — the interface ΔG contribution of the peptide (lower = more favourable). With --relax it also reports the energy after a rigid-body DOPE refinement (tcren.refine_peptide()) and the relaxation gap = 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), and couple_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 --substitute a 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.

--repack adds 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 --substitute stripped.

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 mhc superposes the donor MHC groove onto the host groove (the donor TCR keeps its native docking geometry); --by tcr superposes 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 --peptide when 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 --features instead 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. The d is 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 topology never builds the energies and -i placement never 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_cell and the Hill numbers D1/D2 over the 6 CDR loops x {peptide, MHC}, plus D2_pep24 on 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.

--score adds T, 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 --group no 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 plus Q, T and S) and tcren 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 -s as 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.BadParameter covers 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 Z is 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. Check ais_ess: close to --particles means the schedule was long enough. psi is 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_independent is the one-body model alone; p_model is the marginal of the full coupled model, sampled by block Gibbs, and is the one to use; p_conditional is P(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 loop gives 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 position collapses the loops and reads peptide residue importance – how engaged the model expects each position to be, before any residue identity is scored. --by pair is the ungrouped table and is exactly tcren potts contacts.

p_any is 1 - prod(1 - p) over the group’s pairs, p_expected their sum, and observed the 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 position reads 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, and log Z0 moves with it.

dF is 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. Unlike map’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 to tcren.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