Assessing a modelled complex#

Important

Final models use fixed empirical contact potentials with explicit provenance. Gaussian/GLM, fitted Potts and transition models, and reference-estimated Q/T/S are historical readouts, excluded from the final specification. Nothing fitted at scoring time does not mean fit-free. See Final fit-free calculations.

A co-folding model builds a confident TCR:pMHC complex for any receptor–peptide pair, binding or not. tcren.score reads the coordinates it produced and answers four separate questions about them, and every answer is defined for a single structure: the transform, the class means and the covariance are all frozen on a hold-out that ships with the package, so nothing is estimated from the rows you are scoring and a score does not change depending on what was scored beside it.

Two commands#

$ tcren features -s models/ --all -o feats.tsv
$ tcren assess --features feats.tsv -o scores.tsv

The first is the expensive pass and runs once. The second is arithmetic over that table.

Pass --peptide when the peptide is what varies across the structures being compared, as in a combinatorial library or a mutational scan. 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.

What each column is for#

column

tier

what it answers

pose_score

1

Is this the kind of interface real complexes make? A one-class distance to the manifold hold-out binders occupy. Binder moments use labelled positives, and preprocessing uses both classes. Tier 1 names the one-class readout, not negative-independent training.

binder_score

2

Log-odds that the complex is a genuine recognition interface.

channel_*

2

The same log-odds, marginalized to one descriptor family, so a number can be attributed.

peptide_score

0

The poly-alanine-referenced recognition energy. Nothing is fitted in it. This ranks peptides against a fixed receptor, and it reads below chance on a receptor benchmark — a property of the reference frame, not a defect.

confidence_residual

1

The reported logit(ipTM) minus its binder-conditional mean on that axis. A large positive residual is a model the generator is more certain of than its own geometry warrants.

binder_iptm

2

binder_score plus logit(ipTM). A coefficient-free heuristic, without a calibrated likelihood-ratio or conditional-independence interpretation. The recommended read when a confidence is available.

Higher is better throughout.

The five channels#

A marginal of a Gaussian is a sub-block of its covariance — exact, closed form, no re-fit — so reading one family of descriptors costs an index and nothing else:

placement

where the receptor sits in the groove frame.

interface

how much interface it makes, and of what chemistry.

shape

the footprint’s shape, free of its size.

energetics

the contact chemistry on its statistical potential scale. The conventional kT label denotes a reduced scale; it does not establish a calibrated binding free energy.

mechanics

the interface read as a network of breakable springs.

They do not sum to binder_score and should not: the whole model also reads the correlations between channels, which a per-channel view cannot show. What they give you is attribution — and, sometimes, a better instrument. A channel beats the whole model where the whole model dilutes it: on template-free cohorts channel_shape reads 0.637 against the full posterior’s 0.615, and on a combinatorial peptide library channel_energetics reads 0.700 against 0.542.

Which residue carries it#

The five channels say which part of the structure a score comes from. residue_deltas says which residue:

tcren explain -s model.pdb --score binder -o deltas.tsv

Each row is one interface residue, and delta is

\[\Delta_i = L(x) - L(x_{\setminus i})\]

the score of the complex minus the score of the same complex with that residue’s atoms removed. Positive means the residue carries the score. It is defined for every read-out and every channel, so one table colours a figure by any of them — pass it straight to tcren.viz.pymol.importance_scene().

peptide_score is the exception and does not need it: that score is a sum over contacts, so tcren.energetics.scoring.position_profile() already returns an exact per-position decomposition that sums back to the score. Use the exact one where it exists. Leave-one-out is for the four read-outs that have no exact split, because each is a function of ~149 whole-structure scalars through a full covariance and no residue owns a share of one.

Cost is one descriptor pass per interface residue — a few minutes for a complex. Chain typing and the MHC call are mmseqs searches and run once for the whole structure, never per mask. A masked row whose descriptors go undefined is reported as a null delta rather than imputed.

chain_types restricts reported residues without changing the score. The Python API accepts receptor=False to explain the full peptide-presentation score; the default and CLI explain the receptor marginal. Noncontact residues are omitted, without claiming their contributions to whole-chain geometry descriptors vanish.

Reproducing the frozen model#

The coefficients are frozen, and the inputs they were frozen against are named:

$ tcren fetch-data                                   # the structure sets the manifest names
$ tcren features -s <those structures> -o hold.tsv
$ tcren fit-holdout --features hold.tsv -o refit.npz

The frozen numerical parameters retain their original training catalogue. The 2026-10-14 metadata migration records corrected definitions and changed descriptor semantics without refitting; it does not establish a bit-for-bit refit under corrected feature extraction. A fresh refit writes a separate model and requires a matched comparison before promotion. holdout_manifest() returns the 8,292 structures with their dataset, epitope, label and ipTM. Refit on your own hold-out by passing --manifest, then read it back with tcren assess --model.

Confidence probability scale#

Let z = logit(ipTM) and let m(x) be its Gaussian conditional mean given transformed structural coordinates x. The library residual is z - m(x). Applying sigmoid to m(x) gives a plug-in original-scale prediction, not E[ipTM | x]: nonlinear expectation and back-transformation generally do not commute. The benchmark owns its fitted GLM and operating-point correction producers; those are separate from the library conditional residual. Future confidence fitting validates that the joint covariance is positive semidefinite via the Schur complement, without modifying the shipped arrays.

The predecessor tier#

tcren.cohort.q_score(), tcren.reliability.t_score() and tcren.reliability.s_score() are the reference-estimated directional scores this layer generalises, and they are still supported. S leads the functionally validated receptor screen on its own, and it composes with binder_score rather than being replaced by it.

API#

tcren.score.pose_score(table, *, model=None)[source]#

See ScoreModel.pose_score().

Parameters:

model (ScoreModel | None)

Return type:

ndarray

tcren.score.binder_score(table, *, receptor=True, model=None)[source]#

See ScoreModel.binder_score().

Parameters:
Return type:

ndarray

tcren.score.channel_scores(table, *, receptor=True, model=None)[source]#

See ScoreModel.channel_scores().

Parameters:
Return type:

dict[str, ndarray]

tcren.score.confidence_residual(table, iptm, *, model=None)[source]#

See ScoreModel.confidence_residual().

Parameters:

model (ScoreModel | None)

Return type:

ndarray

tcren.score.peptide_score(table)[source]#

Tier 0, and nothing is fitted anywhere in it.

The poly-alanine-referenced recognition energy over the two interfaces the peptide is part of, sign-flipped so higher is better. This is the instrument for peptide ranking against a fixed receptor, and it is not the instrument for receptor ranking: on a receptor benchmark it reads below chance, which is a property of the reference frame rather than a defect.

Return type:

ndarray

tcren.score.residue_deltas(source, *, score='binder', model=None, organism='human', receptor=True, chain_types=None, interfaces=('tcr_peptide', 'tcr_mhc', 'peptide_mhc'), radii=(7.0, 8.0))[source]#

Leave-one-residue-out contribution to a whole-structure score.

Parameters:
  • source – a path to a structure, or an already-parsed Structure.

  • score (str) – "binder", "pose", "peptide", or a channel name ("placement", "interface", "shape", "energetics", "mechanics").

  • model – overrides tcren.score.holdout_model().

  • organism (str) – passed to chain typing when the structure is not already annotated.

  • receptor (bool) – scoring mode for binder/channel readouts, as in binder_score; independent of which residues are selected. False includes peptide-presentation coordinates.

  • chain_types (tuple[str, ...] | None) – restrict to residues of these chain types, e.g. ("PEPTIDE",) to colour a peptide only. None takes every residue on either side of interfaces.

  • interfaces (tuple[str, ...]) – which interfaces define the residue set.

  • radii (tuple[float, ...]) – footprint radii, as in tcren.descriptors.table.recognition_table().

Returns:

chain.id, residue.index (seq index), residue.pdb, residue.aa, score.full, score.without and delta = full − without. delta is null where masking the residue left a descriptor undefined.

Return type:

One row per masked residue

tcren.score.score_table(table, *, receptor=True, iptm=None, model=None)[source]#

Every read-out for a tcren features table, as one polars frame.

Each numeric readout has a corresponding <name>_available flag. assessment_available aliases binder_score_available. missing_descriptors is a JSON list of nonfinite model descriptors present in each row. Entirely absent columns are marginalized out; individual readouts can remain available when the missing values occur only outside their coordinate subset.

Adds binder_iptm when iptm is supplied: a coefficient-free heuristic sum of binder log-odds and logit confidence, without a calibrated likelihood-ratio or independence claim. It stays defined for a single structure. On the functionally validated receptor screen it reads 0.771 against the posterior’s 0.768 and ipTM’s 0.795, and it is the recommended read when a confidence is available.

Parameters:
tcren.score.holdout_model(path=None)[source]#

The frozen hold-out fit that ships with the package.

Raises a message naming tcren fit-holdout rather than a bare FileNotFoundError, because a source checkout that has never run the fitter is the common way to arrive here.

Parameters:

path (str | None)

Return type:

ScoreModel

tcren.score.holdout_manifest()[source]#

The structures the shipped model was fitted on: id, dataset, epitope and binder label.

This is what makes the frozen coefficients reproducible rather than merely stated. The descriptors themselves are not shipped – 8,292 rows by 147 columns is 19 MB and a wheel is not the place for it – but they are recomputable: tcren fetch-data brings down the structure sets these ids name, tcren features writes the table, and tcren fit-holdout fits a separate output from the two. Corrected descriptor semantics require a matched comparison before replacing the historical frozen model.

class tcren.score.ScoreModel(transform, joint, receptor_names, conf_mu, conf_cov, conf_var, catalogue_digest, n_pos, n_neg, n_epitopes, tcren_version, confidence_available=True)[source]#

The frozen hold-out fit. Load it with holdout_model(), never construct it by hand.

Parameters:
  • transform (Transformer)

  • joint (Joint)

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

  • conf_mu (float)

  • conf_cov (ndarray)

  • conf_var (float)

  • catalogue_digest (str)

  • n_pos (int)

  • n_neg (int)

  • n_epitopes (int)

  • tcren_version (str)

  • confidence_available (bool)

receptor_names: tuple[str, ...]#

the five descriptors computed without the receptor are constant across every structure of one epitope on one allele, so a model reading them reaches the cohort label without reading an interface.

Type:

the coordinates a receptor-ranking read-out may use

conf_mu: float#

E[logit ipTM | x] comes from this row of the binder covariance.

Type:

binder-class augmentation

MIN_COORDINATES = 20#

below this many usable coordinates a covariance read-out is not worth reporting.

coordinates(table, *, coordinate_names=None)[source]#

(complete-case mask, coordinates, the coordinate names they carry).

Two kinds of absence, handled differently and both deliberately.

A column the table does not have is marginalized out. That is exact: a marginal of a Gaussian is a sub-block of its covariance, so a table without the potts family is scored by the model restricted to what it does have, not by a model with a hole in it. This is what lets tcren features -i placement,interface,topology still produce a binder score.

A row missing one of the columns that are present is dropped rather than imputed: the whole construction is a covariance, and a filled-in value is a fabricated correlation. With coordinate_names, only the requested marginal’s descriptors determine missingness.

Return type:

tuple[ndarray, ndarray, list[str]]

available(table)[source]#

The model coordinates this table can supply, in model order.

Return type:

list[str]

pose_score(table)[source]#

Tier 1. How far this interface sits from the manifold real binders occupy.

A partial Mahalanobis to the binder Gaussian. Binder moments use label-selected positives; the frozen preprocessing was fitted on both classes. Tier 1 describes the readout, not negative-independent or label-free training. Higher is more plausible: the sign is flipped from the raw distance so that every score in this module reads “higher is better”.

Return type:

ndarray

binder_score(table, *, receptor=True)[source]#

Tier 2. Log-odds that this complex is a genuine recognition interface.

receptor=True (the default) marginalizes out the five descriptors computed without the receptor. Set it False only when the peptide is what varies across the structures being compared, as in a combinatorial peptide library, where the presentation energy is signal rather than the cohort’s name.

Parameters:

receptor (bool)

Return type:

ndarray

channel_scores(table, *, receptor=True)[source]#

Tier 2, one log-odds per channel: which part of the structure says so.

Each is an exact marginal of the same covariance, so the five are on one scale and can be read against each other. They do not sum to binder_score(), and should not: the whole model also reads the correlations between channels, which is the part a per-channel view cannot show.

Parameters:

receptor (bool)

Return type:

dict[str, ndarray]

confidence_residual(table, iptm)[source]#

Tier 1. Reported confidence minus what the coordinates say it should have been.

E[logit ipTM | x] under the binder Gaussian, subtracted from the reported value. A large positive residual is a model the generator is more certain of than its own geometry and chemistry warrant. Training uses label-selected binders and shared preprocessing fitted on both classes. Returns NaN when the model was fitted without enough valid confidence observations.

Return type:

ndarray

tcren.reliability.artefact_directions(binder_coords, crystal_coords, *, tolerance=0.05)[source]#

Which directions of a binder manifold are the generator’s regularity, not the interface’s.

No binding label enters this. The argument is that a direction real complexes are free to vary along, while modelled binders are pinned to it, is a property of the model that produced them. A physical constraint holds on a crystal at least as tightly as on a prediction of one; a generator’s habit does not.

Parameters:
  • binder_coords – (n, p) transformed coordinates for modelled binders – the population whose covariance defines the directions.

  • crystal_coords – (m, p) the same coordinates for experimentally solved complexes.

  • tolerance (float) – the residual s.d. below which a direction counts as tight.

Returns:

{"index", "sd_binder", "sd_crystal", "ratio", "is_artefact"}, one entry per direction, ordered stiffest first. is_artefact is a tight direction that crystals break harder than binders do.

Return type:

dict

Reading it: a large ratio on a tight direction means the constraint is the generator’s. Drop those coordinates, or use a covariance estimator that suppresses them – shrinkage does it for free, which is why the shipped model does not need this call to be safe.