stcrpy 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- examples/__init__.py +0 -0
- examples/egnn.py +425 -0
- stcrpy/__init__.py +5 -0
- stcrpy/tcr_datasets/__init__.py +0 -0
- stcrpy/tcr_datasets/tcr_graph_dataset.py +499 -0
- stcrpy/tcr_datasets/tcr_selector.py +0 -0
- stcrpy/tcr_datasets/tcr_structure_dataset.py +0 -0
- stcrpy/tcr_datasets/utils.py +350 -0
- stcrpy/tcr_formats/__init__.py +0 -0
- stcrpy/tcr_formats/tcr_formats.py +114 -0
- stcrpy/tcr_formats/tcr_haddock.py +556 -0
- stcrpy/tcr_geometry/TCRCoM.py +350 -0
- stcrpy/tcr_geometry/TCRCoM_LICENCE +168 -0
- stcrpy/tcr_geometry/TCRDock.py +261 -0
- stcrpy/tcr_geometry/TCRGeom.py +450 -0
- stcrpy/tcr_geometry/TCRGeomFiltering.py +273 -0
- stcrpy/tcr_geometry/__init__.py +0 -0
- stcrpy/tcr_geometry/reference_data/__init__.py +0 -0
- stcrpy/tcr_geometry/reference_data/dock_reference_1_imgt_numbered.pdb +6549 -0
- stcrpy/tcr_geometry/reference_data/dock_reference_2_imgt_numbered.pdb +6495 -0
- stcrpy/tcr_geometry/reference_data/reference_A.pdb +31 -0
- stcrpy/tcr_geometry/reference_data/reference_B.pdb +31 -0
- stcrpy/tcr_geometry/reference_data/reference_D.pdb +31 -0
- stcrpy/tcr_geometry/reference_data/reference_G.pdb +31 -0
- stcrpy/tcr_geometry/reference_data/reference_data.py +104 -0
- stcrpy/tcr_interactions/PLIPParser.py +147 -0
- stcrpy/tcr_interactions/TCRInteractionProfiler.py +433 -0
- stcrpy/tcr_interactions/TCRpMHC_PLIP_Model_Parser.py +133 -0
- stcrpy/tcr_interactions/__init__.py +0 -0
- stcrpy/tcr_interactions/utils.py +170 -0
- stcrpy/tcr_methods/__init__.py +0 -0
- stcrpy/tcr_methods/tcr_batch_operations.py +223 -0
- stcrpy/tcr_methods/tcr_methods.py +150 -0
- stcrpy/tcr_methods/tcr_reformatting.py +18 -0
- stcrpy/tcr_metrics/__init__.py +2 -0
- stcrpy/tcr_metrics/constants.py +39 -0
- stcrpy/tcr_metrics/tcr_interface_rmsd.py +237 -0
- stcrpy/tcr_metrics/tcr_rmsd.py +179 -0
- stcrpy/tcr_ml/__init__.py +0 -0
- stcrpy/tcr_ml/geometry_predictor.py +3 -0
- stcrpy/tcr_processing/AGchain.py +89 -0
- stcrpy/tcr_processing/Chemical_components.py +48915 -0
- stcrpy/tcr_processing/Entity.py +301 -0
- stcrpy/tcr_processing/Fragment.py +58 -0
- stcrpy/tcr_processing/Holder.py +24 -0
- stcrpy/tcr_processing/MHC.py +449 -0
- stcrpy/tcr_processing/MHCchain.py +149 -0
- stcrpy/tcr_processing/Model.py +37 -0
- stcrpy/tcr_processing/Select.py +145 -0
- stcrpy/tcr_processing/TCR.py +532 -0
- stcrpy/tcr_processing/TCRIO.py +47 -0
- stcrpy/tcr_processing/TCRParser.py +1230 -0
- stcrpy/tcr_processing/TCRStructure.py +148 -0
- stcrpy/tcr_processing/TCRchain.py +160 -0
- stcrpy/tcr_processing/__init__.py +3 -0
- stcrpy/tcr_processing/annotate.py +480 -0
- stcrpy/tcr_processing/utils/__init__.py +0 -0
- stcrpy/tcr_processing/utils/common.py +67 -0
- stcrpy/tcr_processing/utils/constants.py +367 -0
- stcrpy/tcr_processing/utils/region_definitions.py +782 -0
- stcrpy/utils/__init__.py +0 -0
- stcrpy/utils/error_stream.py +12 -0
- stcrpy-1.0.0.dist-info/METADATA +173 -0
- stcrpy-1.0.0.dist-info/RECORD +68 -0
- stcrpy-1.0.0.dist-info/WHEEL +5 -0
- stcrpy-1.0.0.dist-info/licenses/LICENCE +28 -0
- stcrpy-1.0.0.dist-info/licenses/stcrpy/tcr_geometry/TCRCoM_LICENCE +168 -0
- stcrpy-1.0.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
from ..tcr_processing import abTCR, MHCchain
|
|
5
|
+
from .TCRCoM import MHCI_TCRCoM, MHCII_TCRCoM
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TCRGeom:
|
|
9
|
+
"""Class for TCR geometry calculations."""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
tcr: "TCR",
|
|
14
|
+
save_aligned_as: bool = False,
|
|
15
|
+
polarity_as_sign: bool = True,
|
|
16
|
+
mode: str = "cys",
|
|
17
|
+
):
|
|
18
|
+
"""Constructor for TCR geometry calculation class.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
tcr (TCR): TCR structure object of which to calculate complex geometry. TCR must be in complex to MHC.
|
|
22
|
+
save_aligned_as (bool, optional): Save the alignment of the TCR:pMHC complex as PDB file for verification. Defaults to False.
|
|
23
|
+
polarity_as_sign (bool, optional): Use the sign of the angle to indicate non-canonical pose. ie reverse binding angles are assigned negative sign. Defaults to True.
|
|
24
|
+
mode (str, optional): Method to calculate geometry with, see STCRpy paper for details. Defaults to "cys". Options: 'rudolph', 'com', 'cys'.
|
|
25
|
+
"""
|
|
26
|
+
self.tcr_com = None
|
|
27
|
+
self.mhc_com = None
|
|
28
|
+
self.tcr_VA_com = None
|
|
29
|
+
self.tcr_VB_com = None
|
|
30
|
+
self.tcr_VA_cys_centroid = None
|
|
31
|
+
self.tcr_VB_cys_centroid = None
|
|
32
|
+
self.tcr_VA_VB_angle = None
|
|
33
|
+
self.scanning_angle = np.nan
|
|
34
|
+
self.tcr_pitch_angle = np.nan
|
|
35
|
+
self.tcr_docking_angles_cys = None
|
|
36
|
+
self.tcr_mhc_dist = None
|
|
37
|
+
self.mode = mode
|
|
38
|
+
|
|
39
|
+
self._type_checks(tcr)
|
|
40
|
+
|
|
41
|
+
mhc = tcr.get_MHC()[0]
|
|
42
|
+
|
|
43
|
+
self._set_mhc_reference(mhc)
|
|
44
|
+
|
|
45
|
+
if self.mode != "rudolph":
|
|
46
|
+
(self.tcr_com, self.mhc_com, self.tcr_VA_com, self.tcr_VB_com) = (
|
|
47
|
+
self.mhc_tcr_com_calculator.calculate_centres_of_mass(
|
|
48
|
+
tcr, save_aligned_as=save_aligned_as
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if self.mode != "com":
|
|
53
|
+
(self.tcr_VA_cys_centroid, self.tcr_VB_cys_centroid) = (
|
|
54
|
+
self._get_cys_centroids(tcr)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if self.mode in ["cys", "rudolph"]:
|
|
58
|
+
self.tcr_vector = self.get_tcr_vector(
|
|
59
|
+
self.tcr_VA_cys_centroid, self.tcr_VB_cys_centroid
|
|
60
|
+
)
|
|
61
|
+
elif self.mode == "com":
|
|
62
|
+
self.tcr_vector = self.get_tcr_vector(self.tcr_VA_com, self.tcr_VB_com)
|
|
63
|
+
else:
|
|
64
|
+
warnings.warn(
|
|
65
|
+
f"Geometry mode {self.mode} not recognised. Using CYS atom coordinates to calculate geometry"
|
|
66
|
+
)
|
|
67
|
+
self.tcr_vector = self.get_tcr_vector(
|
|
68
|
+
self.tcr_VA_cys_centroid, self.tcr_VB_cys_centroid
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if self.mode == "rudolph":
|
|
72
|
+
self.mhc_vector = self._get_mhc_helix_vectors(mhc)
|
|
73
|
+
self.scanning_angle = self.calculate_rudolph_angle(
|
|
74
|
+
self.tcr_vector, self.mhc_vector
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
else:
|
|
78
|
+
self.scanning_angle, self.tcr_pitch_angle = (
|
|
79
|
+
self.calculate_tcr_docking_angles(
|
|
80
|
+
self.tcr_vector, polarity_as_sign=polarity_as_sign
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
self.polarity = self.get_polarity()
|
|
85
|
+
|
|
86
|
+
def __repr__(self) -> str:
|
|
87
|
+
"""Print TCR:pMHC complex geometry.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
str: TCR:pMHC complex geometry metrics.
|
|
91
|
+
"""
|
|
92
|
+
def polarity_to_str(polarity):
|
|
93
|
+
return "canonical" if polarity == 0 else "reverse"
|
|
94
|
+
|
|
95
|
+
def mode_to_str(mode):
|
|
96
|
+
if mode == "cys":
|
|
97
|
+
return "Cysteine centroids"
|
|
98
|
+
elif mode == "com":
|
|
99
|
+
return "Centre of mass"
|
|
100
|
+
elif mode == "rudolph":
|
|
101
|
+
return "Rudolph et. al. 2006"
|
|
102
|
+
else:
|
|
103
|
+
return mode
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
f"TCR CoM: {self.tcr_com}\nMHC CoM: {self.mhc_com}\n"
|
|
107
|
+
+ f"TCR VA CoM: {self.tcr_VA_com}\nTCR VB CoM: {self.tcr_VB_com}\n"
|
|
108
|
+
+ f"TCR VA CYS Centroid: {self.tcr_VA_cys_centroid}\nTCR VB CYS Centroid: {self.tcr_VB_cys_centroid}\n"
|
|
109
|
+
+ f"Scanning angle: {np.degrees(self.scanning_angle)}\n"
|
|
110
|
+
+ f"Pitch angle: {np.degrees(self.tcr_pitch_angle)}\n"
|
|
111
|
+
+ f"Polarity: {polarity_to_str(self.polarity)}\n"
|
|
112
|
+
+ f"Geometry mode: {mode_to_str(self.mode)}"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def to_dict(self) -> dict:
|
|
116
|
+
"""Return TCR:pMHC complex geometry metrics as dictionary.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
dict: TCR:pMHC complex geometry.
|
|
120
|
+
"""
|
|
121
|
+
return {
|
|
122
|
+
"tcr_com": [self.tcr_com],
|
|
123
|
+
"mhc_com": [self.mhc_com],
|
|
124
|
+
"tcr_VA_com": [self.tcr_VA_com],
|
|
125
|
+
"tcr_VB_com": [self.tcr_VB_com],
|
|
126
|
+
"tcr_VA_cys_centroid": [self.tcr_VA_cys_centroid],
|
|
127
|
+
"tcr_VB_cys_centroid": [self.tcr_VB_cys_centroid],
|
|
128
|
+
"scanning_angle": np.degrees(self.scanning_angle),
|
|
129
|
+
"pitch_angle": np.degrees(self.tcr_pitch_angle),
|
|
130
|
+
"polarity": self.polarity,
|
|
131
|
+
"mode": self.mode,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
def to_df(self) -> "pandas.DataFrame":
|
|
135
|
+
"""Return TCR:pMHC complex geometry metrics and pandas dataframe.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
pandas.DataFrame: TCR:pMHC complex geometry metrics
|
|
139
|
+
"""
|
|
140
|
+
import pandas as pd
|
|
141
|
+
|
|
142
|
+
return pd.DataFrame.from_dict(self.to_dict())
|
|
143
|
+
|
|
144
|
+
def get_scanning_angle(self, rad: bool = False) -> float:
|
|
145
|
+
"""Return TCR:pMHC complex scanning (aka crossing, incident angle) of TCR to MHC.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
rad (bool, optional): Return angle in radians. Defaults to False.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
float: TCR:pMHC scanning angle.
|
|
152
|
+
"""
|
|
153
|
+
if rad:
|
|
154
|
+
return self.scanning_angle
|
|
155
|
+
else:
|
|
156
|
+
return np.degrees(self.scanning_angle)
|
|
157
|
+
|
|
158
|
+
def get_pitch_angle(self, rad: bool = False) -> float:
|
|
159
|
+
"""Return TCR:pMHC pitch angle, ie tilt of the TCR over the MHC.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
rad (bool, optional): Return angle in radians. Defaults to False.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
float: TCR:pMHC pitch angle
|
|
166
|
+
"""
|
|
167
|
+
if rad:
|
|
168
|
+
return self.tcr_pitch_angle
|
|
169
|
+
else:
|
|
170
|
+
return np.degrees(self.tcr_pitch_angle)
|
|
171
|
+
|
|
172
|
+
def _type_checks(self, tcr: "TCR"):
|
|
173
|
+
"""Run checks on TCR structure object to ensure geometry can be calculated.
|
|
174
|
+
\nChecks if TCR is in complex with MHC and whether TCR is alpha/beta chain TCR.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
tcr (TCR): TCR structure object.
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
NotImplementedError: TCR:pMHC geometry calculations are compatible the alpha beta chain TCRs.
|
|
181
|
+
"""
|
|
182
|
+
if len(tcr.get_MHC()) == 0:
|
|
183
|
+
warnings.warn(
|
|
184
|
+
f"No MHC associated with TCR {tcr.parent.parent.id}_{tcr.id}. Geometry cannot be calculated."
|
|
185
|
+
)
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
if not isinstance(tcr, abTCR):
|
|
189
|
+
raise NotImplementedError(
|
|
190
|
+
f"TCR MHC geometry only implemented for abTCR types, not {type(tcr)}"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def _set_mhc_reference(self, mhc: "MHC"):
|
|
194
|
+
"""Sets the MHC structure reference to use for alignment.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
mhc (MHC): MHC structure object
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
NotImplementedError: MHC alignments are compatible with MHC class I and II. CD1 and MR1 types currently not supported.
|
|
201
|
+
ValueError: MHC unrecognised.
|
|
202
|
+
"""
|
|
203
|
+
if (isinstance(mhc, MHCchain) and mhc.chain_type not in ["GA", "GB"]) or (
|
|
204
|
+
hasattr(mhc, "MHC_type") and mhc.MHC_type == "MH1"
|
|
205
|
+
):
|
|
206
|
+
self.mhc_tcr_com_calculator = MHCI_TCRCoM()
|
|
207
|
+
elif (isinstance(mhc, MHCchain) and mhc.chain_type in ["GA", "GB"]) or (
|
|
208
|
+
hasattr(mhc, "MHC_type") and mhc.MHC_type == "MH2"
|
|
209
|
+
):
|
|
210
|
+
self.mhc_tcr_com_calculator = MHCII_TCRCoM()
|
|
211
|
+
else:
|
|
212
|
+
if hasattr(mhc, "MHC_type") and mhc.get_MHC_type() in ["CD1", "MR1"]:
|
|
213
|
+
raise NotImplementedError(
|
|
214
|
+
"TCR geometry not yet implemented for CD1 and MR1 antigen."
|
|
215
|
+
)
|
|
216
|
+
else:
|
|
217
|
+
raise ValueError(f"MHC type of {mhc} not recognised.")
|
|
218
|
+
|
|
219
|
+
def get_tcr_vector(self, tcr_VA_com: np.array, tcr_VB_com: np.array) -> np.array:
|
|
220
|
+
"""Calculates the vector from the VA centre of mass to the VB centre of mass, translated such that the vector originates at the total TCR's centre of mass and truncated to unit length.
|
|
221
|
+
Args:
|
|
222
|
+
tcr_VA_com (np.array): TCR VA centre of mass
|
|
223
|
+
tcr_VB_com (np.array): TCR VB center of mass
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
np.array: Unit vector from TCR CoM in direction of VA CoM to VB CoM
|
|
227
|
+
"""
|
|
228
|
+
direction_vec = tcr_VB_com - tcr_VA_com
|
|
229
|
+
tcr_vector = direction_vec / np.linalg.norm(direction_vec)
|
|
230
|
+
return tcr_vector
|
|
231
|
+
|
|
232
|
+
def calculate_tcr_docking_angles(
|
|
233
|
+
self, tcr_vector: np.array, polarity_as_sign: bool = True
|
|
234
|
+
) -> tuple[np.array]:
|
|
235
|
+
"""
|
|
236
|
+
Calculates the scanning angle and pitch of the TCR relative to the MHC.
|
|
237
|
+
This function relies on the previous alignment of the MHC to the reference MHC,
|
|
238
|
+
with the x axis defined perpedicular to the peptide, the y-axis along the peptide,
|
|
239
|
+
and the z-axis pointing 'up' away from the MHC.
|
|
240
|
+
|
|
241
|
+
The scanning angle is calculated as the angle between the y axis
|
|
242
|
+
and the projection of the TCR vector, which points from VA to VB, onto the x-y plane.
|
|
243
|
+
|
|
244
|
+
The pitch is calculated as the angle between the z-axis and the plane normal
|
|
245
|
+
to the TCR vector and passing through the TCR CoM. Specifically the angle of the vector from the
|
|
246
|
+
z-axis intersection and the nearest projection of the z-axis onto the plane dividing the VA and VB
|
|
247
|
+
TCR domains is calculated, which is defined as the pitch of the TCR.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
tcr_vector (np.array): Unit vector in direction of VA CoM to VB CoM
|
|
251
|
+
polarity (bool, optional): Set the polarity as the sign of the scanning angle
|
|
252
|
+
ie negative if polarity is reversed (1),
|
|
253
|
+
else positive if polarity is canonical (0). Defaults to True.
|
|
254
|
+
Returns:
|
|
255
|
+
tuple[np.array]: Tuple containing the scanning angle and pitch angle of the TCR to the MHC
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
xy_projection = tcr_vector[:2] / np.linalg.norm(tcr_vector[:2])
|
|
259
|
+
self.scanning_angle = np.arccos(np.dot(xy_projection, np.asarray([0.0, 1.0])))
|
|
260
|
+
phi = np.arccos(np.sqrt(1 - (tcr_vector[-1] ** 2)))
|
|
261
|
+
if polarity_as_sign:
|
|
262
|
+
self.scanning_angle = self.scanning_angle * ((-1) ** self.get_polarity())
|
|
263
|
+
return self.scanning_angle, phi
|
|
264
|
+
|
|
265
|
+
def get_polarity(self) -> int:
|
|
266
|
+
"""
|
|
267
|
+
Return the polarity of the TCR based on the TCR vector pointing from the VA to the VB CoM, or the scanning angle if using Rudolph et. al..
|
|
268
|
+
If the x component is negative, ie the tcr_vector points from the alpha 2 chain to the
|
|
269
|
+
alpha 1 chain of the MHC, the polarity is canonical (0). Otherwise the polarity is reverse (1).
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
int: 0 for canonical polarity, 1 for reverse polarity.
|
|
273
|
+
"""
|
|
274
|
+
if self.mode in ["cys", "com"]:
|
|
275
|
+
if self.tcr_vector[0] <= 0:
|
|
276
|
+
return 0
|
|
277
|
+
else:
|
|
278
|
+
return 1
|
|
279
|
+
elif self.mode == "rudolph":
|
|
280
|
+
return 0 if np.abs(np.degrees(self.scanning_angle)) < 120.0 else 1
|
|
281
|
+
|
|
282
|
+
def _get_cys_centroids(self, tcr: "TCR") -> tuple[np.array, np.array]:
|
|
283
|
+
"""Calculate the halfway coordintate bewtween the CYS residues of the V.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
tcr (TCR): TCR structure object
|
|
287
|
+
|
|
288
|
+
Raises:
|
|
289
|
+
KeyError: Cys residue not found in V domain
|
|
290
|
+
KeyError: SG and CA atoms not found in Cys residue
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
tuple[np.array, np.array]: (Cys centroid of VA/VG, Cys centroid of VB/VD)
|
|
294
|
+
"""
|
|
295
|
+
domain_order = {
|
|
296
|
+
"VA": 0,
|
|
297
|
+
"VB": 3,
|
|
298
|
+
"VG": 1,
|
|
299
|
+
"VD": 2,
|
|
300
|
+
} # defines order s.t. order is VA->VB, VG->VD, VD->VB depending on pairing
|
|
301
|
+
cys_coords = {dom: {} for dom in tcr.get_domain_assignment()}
|
|
302
|
+
for domain, chain in tcr.get_domain_assignment().items():
|
|
303
|
+
for cys_res_nr in [23, 104]:
|
|
304
|
+
try:
|
|
305
|
+
cys_coords[domain][cys_res_nr] = tcr[chain][cys_res_nr]["SG"].coord
|
|
306
|
+
except KeyError as e:
|
|
307
|
+
if str(cys_res_nr) in str(e):
|
|
308
|
+
raise KeyError(
|
|
309
|
+
f"IMGT numbered residue {str(cys_res_nr)} not found in {tcr.id} domain {domain} with chain ID {chain}. Consider calculating TCR geometry with centre of mass coordinates. "
|
|
310
|
+
)
|
|
311
|
+
elif "SG" in str(e):
|
|
312
|
+
warnings.warn(
|
|
313
|
+
f"SG atom not found in IMGT residue number {str(cys_res_nr)} of {tcr.id} domain {domain} with chain ID {chain}. Trying CA atom instead."
|
|
314
|
+
)
|
|
315
|
+
try:
|
|
316
|
+
cys_coords[domain][cys_res_nr] = tcr[chain][cys_res_nr][
|
|
317
|
+
"CA"
|
|
318
|
+
]
|
|
319
|
+
except KeyError:
|
|
320
|
+
raise KeyError(
|
|
321
|
+
f"Neither SG not CA atom found in IMGT residue number {str(cys_res_nr)} of {tcr.id} domain {domain} with chain ID {chain}. Consider calculating TCR geometry with centre of mass coordinates."
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
cys_centroids = (
|
|
325
|
+
np.mean([cys_coords[dom][res_nr] for res_nr in [23, 104]], axis=0)
|
|
326
|
+
for dom in sorted(
|
|
327
|
+
cys_coords, key=lambda x: domain_order[x]
|
|
328
|
+
) # calculate cys centroids and orders such that vector can be calculated from VA to VB (VG to VD)
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
return cys_centroids # VA CYS centroid, VB CYS centroid
|
|
332
|
+
|
|
333
|
+
def _get_mhc_helix_vectors(self, mhc: "MHC") -> np.array:
|
|
334
|
+
"""Calculate the vector pointing along the MHC using the MHC helices. Points approximately from N to C terminus of GA chain.
|
|
335
|
+
\nVector is calculated as principal component of SVD decomposition of point cloud of backbone carbon alpha atoms in the helices forming the peptide cleft.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
mhc (MHC): MHC structure object
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
np.array: Unit vector along the MHC
|
|
342
|
+
"""
|
|
343
|
+
helix_residue_ranges = {
|
|
344
|
+
"MH1": ((50, 87), (140, 177)),
|
|
345
|
+
"MH2": {"GA": ((50, 88),), "GB": ((54, 65), (67, 91))},
|
|
346
|
+
# "CD1": (50, 87), # TODO: determine consisent residue ranges for CD1 and MR1 helices
|
|
347
|
+
# "MR1": (50, 87),
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if mhc.get_MHC_type() == "MH1":
|
|
351
|
+
helix_CA_coords = np.asarray(
|
|
352
|
+
[
|
|
353
|
+
mhc.get_MH1()[(" ", i, " ")]["CA"].coord
|
|
354
|
+
for _range in helix_residue_ranges[mhc.get_MHC_type()]
|
|
355
|
+
for i in range(*_range)
|
|
356
|
+
if (" ", i, " ") in mhc.get_MH1()
|
|
357
|
+
and "CA" in mhc.get_MH1()[(" ", i, " ")]
|
|
358
|
+
]
|
|
359
|
+
)
|
|
360
|
+
# mhc vector should point approximately from N to C terminii of GA1 (MH1)
|
|
361
|
+
try: # wrap in try except in case residues at edges don't exist or atoms are missing
|
|
362
|
+
approximate_mhc_vector = (
|
|
363
|
+
mhc.get_MH1()[(" ", helix_residue_ranges["MH1"][0][1] - 1, " ")][
|
|
364
|
+
"CA"
|
|
365
|
+
].coord
|
|
366
|
+
- mhc.get_MH1()[(" ", helix_residue_ranges["MH1"][0][0], " ")][
|
|
367
|
+
"CA"
|
|
368
|
+
].coord
|
|
369
|
+
)
|
|
370
|
+
except KeyError: # try surrounding residues
|
|
371
|
+
approximate_mhc_vector = (
|
|
372
|
+
mhc.get_MH1()[(" ", helix_residue_ranges["MH1"][0][1] - 2, " ")][
|
|
373
|
+
"CA"
|
|
374
|
+
].coord
|
|
375
|
+
- mhc.get_MH1()[(" ", helix_residue_ranges["MH1"][0][0] + 1, " ")][
|
|
376
|
+
"CA"
|
|
377
|
+
].coord
|
|
378
|
+
)
|
|
379
|
+
approximate_mhc_vector = approximate_mhc_vector / np.linalg.norm(
|
|
380
|
+
approximate_mhc_vector
|
|
381
|
+
)
|
|
382
|
+
elif mhc.get_MHC_type() == "MH2":
|
|
383
|
+
helix_CA_coords = np.asarray(
|
|
384
|
+
[
|
|
385
|
+
mhc.get_GA()[(" ", i, " ")]["CA"].coord
|
|
386
|
+
for _range in helix_residue_ranges[mhc.get_MHC_type()]["GA"]
|
|
387
|
+
for i in range(*_range)
|
|
388
|
+
if (" ", i, " ") in mhc.get_GA()
|
|
389
|
+
and "CA" in mhc.get_GA()[(" ", i, " ")]
|
|
390
|
+
]
|
|
391
|
+
+ [
|
|
392
|
+
mhc.get_GB()[(" ", i, " ")]["CA"].coord
|
|
393
|
+
for _range in helix_residue_ranges[mhc.get_MHC_type()]["GB"]
|
|
394
|
+
for i in range(*_range)
|
|
395
|
+
if (" ", i, " ") in mhc.get_GB()
|
|
396
|
+
and "CA" in mhc.get_GB()[(" ", i, " ")]
|
|
397
|
+
]
|
|
398
|
+
)
|
|
399
|
+
# mhc vector should point approximately from N to C terminii of GA (MH2)
|
|
400
|
+
try: # wrap in try except in case residues at edges don't exist or atoms are missing
|
|
401
|
+
approximate_mhc_vector = (
|
|
402
|
+
mhc.get_GA()[
|
|
403
|
+
(" ", helix_residue_ranges["MH2"]["GA"][0][1] - 1, " ")
|
|
404
|
+
]["CA"].coord
|
|
405
|
+
- mhc.get_GA()[(" ", helix_residue_ranges["MH2"]["GA"][0][0], " ")][
|
|
406
|
+
"CA"
|
|
407
|
+
].coord
|
|
408
|
+
)
|
|
409
|
+
except KeyError: # try surrounding residues
|
|
410
|
+
approximate_mhc_vector = (
|
|
411
|
+
mhc.get_GB()[
|
|
412
|
+
(" ", helix_residue_ranges["MH2"]["GA"][0][1] - 2, " ")
|
|
413
|
+
]["CA"].coord
|
|
414
|
+
- mhc.get_GB()[
|
|
415
|
+
(" ", helix_residue_ranges["MH2"]["GA"][0][0] + 1, " ")
|
|
416
|
+
]["CA"].coord
|
|
417
|
+
)
|
|
418
|
+
approximate_mhc_vector = approximate_mhc_vector / np.linalg.norm(
|
|
419
|
+
approximate_mhc_vector
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
mhc_helix_points = helix_CA_coords - np.mean(
|
|
423
|
+
helix_CA_coords, axis=0
|
|
424
|
+
) # centre helices
|
|
425
|
+
mhc_vector = np.linalg.svd(mhc_helix_points)[2][0] # fit line
|
|
426
|
+
# check if approximate vector and MHC vector are aligned by checking magnitude of vector sum, if not, invert MHC vector
|
|
427
|
+
if np.linalg.norm(mhc_vector + approximate_mhc_vector) < np.linalg.norm(
|
|
428
|
+
mhc_vector - approximate_mhc_vector
|
|
429
|
+
):
|
|
430
|
+
mhc_vector = -mhc_vector
|
|
431
|
+
mhc_vector = mhc_vector / np.linalg.norm(mhc_vector)
|
|
432
|
+
return mhc_vector
|
|
433
|
+
|
|
434
|
+
def calculate_rudolph_angle(
|
|
435
|
+
self, tcr_vector: np.array, mhc_vector: np.array
|
|
436
|
+
) -> float:
|
|
437
|
+
"""Calculate the scanning angle of TCR:pMHC complex according to Rudolph et. al.
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
tcr_vector (np.array): Vector pointing from VA Cys centroid to VB Cys centroid
|
|
441
|
+
mhc_vector (np.array): Vector pointing along MHC cleft
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
float: scanning angle
|
|
445
|
+
"""
|
|
446
|
+
scanning_angle = np.arccos(
|
|
447
|
+
np.dot(tcr_vector, mhc_vector)
|
|
448
|
+
/ (np.linalg.norm(tcr_vector) * np.linalg.norm(mhc_vector))
|
|
449
|
+
)
|
|
450
|
+
return scanning_angle
|