Assessing a modelled complex#

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/ -i placement,interface,topology,energetics -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. No negative and no binder label enter it — the same standing as tcren.cohort.q_score(). This is the bad-pose channel.

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

binder_iptm

2

binder_score plus logit(ipTM). Two log-odds added, no coefficient to fit, still defined for one structure. 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, in kT.

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.

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

refit.npz matches the shipped model bit for bit. 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.

The predecessor tier#

tcren.cohort.q_score(), tcren.reliability.t_score() and tcren.reliability.s_score() are the fit-free 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.score_table(table, *, receptor=True, iptm=None, model=None)[source]#

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

Adds binder_iptm when iptm is supplied: the naive-Bayes sum of two log-odds, which needs no coefficient because both terms are already on that scale and 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 returns the shipped model from the two.

class tcren.score.ScoreModel(transform, joint, receptor_names, conf_mu, conf_cov, conf_var, catalogue_digest, n_pos, n_neg, n_epitopes, tcren_version)[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)

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

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. No negative and no binder label enter it, which is the same standing as the shipped q_score and t_score. 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, which is the failure mode a confident non-binder presents as. No binder label enters it.

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.