_protein_peptide_pass mis-assigns antigens for structures with >26 chains (list(cpx.id) splits two-character chain ids) - #90
Open
ElainW wants to merge 1 commit into
Conversation
… string list(cpx.id) decomposes the concatenated complex id correctly only while every chain id is one character. Past 26 chains a two-character chain id never enters all_cpx_chains and is then mis-classified as antigen (e.g. 8rro copy 5 gets its own alpha chain AA). Take the chain objects instead.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Version: stcrpy 1.0.6, anarci-mhc 0.0.16, Python 3.11
Summary
TCRParser._protein_peptide_passbuilds its receptor-chain registry from the string form ofa complex id:
cpx.idis the concatenation of the complex's chain ids, solist()decomposes it correctlyonly while every chain id is a single character. STCRpy itself assigns two-character ids
once a structure has more than 26 chains, so on those structures the registry is wrong: for a
receptor with chains
AAandZ, it records'A'and'Z', and'AA'is neverregistered.
Antigen assignment then tests membership in that registry:
so a receptor's own chain passes the "not a receptor chain, therefore antigen" test.
Reproducer
PDB 8rro (40 chains, 8 copies):
Also emits
Crystal Contact Warning: antigen AA has been paired with TCR AAZ.It is intermittent, and that is a second issue
cpxidsandagsin the same function are sets of strings, and the first pairing loopclaims-and-removes (
ags.remove(ag)) while resettingmodel[cpx_id].antigen = []. Iterationorder over
strkeys is randomized per process byPYTHONHASHSEED, so the result variesbetween runs of the same input. Pinning the seed makes it deterministic:
PYTHONHASHSEEDget_antigen()on copy 5DA,EAA,DA,EAA,DA,EAA,DA,EDA,EAA,DA,EAA,DA,EAA,DA,EImpact beyond the spurious chain
On 8i5c and 9iky (50 chains, 10 copies, 10 peptide chains) the symptom is different and
arguably worse: no receptor chain appears as antigen, but copies 0–4 get no antigen at all
while copies 5–9 are handed single-character chains belonging to other copies. The peptides are
not distributed one-per-copy.
Fix
Take the chain objects rather than re-parsing the id string (the change in this PR). With it
applied:
8rrocopy 5 antigenAA, DA, E(own chain, + Crystal Contact Warning)DA8i5ccopies 0–48i5ccopies 5–9 antigenBA,H,M,R,C;W(mixed id spaces)BA,GA,LA,QA,VA(one each)The
try/exceptpreserves the documented contract thatcomplexesmay hold either pairedcomplexes or bare chain objects.
Fixing the registry also removes the observed run-to-run variation on these structures, since
the spurious candidate is what the order-dependent second loop was flip-flopping over. The
set-iteration-order dependence itself remains and may deserve a separate look (an orderedcontainer, or sorting
cpxids/agsbefore iterating, would make results reproducible byconstruction).
One more place multi-character ids are still parsed by concatenation
The patch repairs the registry, but the same function identifies a chain pair the same
concatenating way, a few lines down:
and
cpxidsare themselves concatenations (Entity.__init__(self, c1.id + c2.id),TCR.py:567). Once chain ids differ in length that mapping is not injective: chainsA+ABgive
"AAB", which is also the id of a receptor whose chains areAA+B. The pair thenlooks like "a receptor, not an antigen contact" and is skipped — a genuine antigen dropped,
this time silently.
This is latent, not observed: with the RCSB id scheme (
A…Z, thenAA,BA, …ZA,then
AB…) an id of theABform only appears past ~52 chains, and the largest structures wehave are 50 chains — on
8rro/8i5c/9ikythe patch alone is sufficient and verified. Itbelongs in the report because it is the same root cause: complex ids are a lossy encoding of
their chain set, so any code that recovers chains from the id string is wrong above 26 chains.
The robust version keys
contact_freqon the chain-id tuple rather than the concatenation.