RNApolis 0.4.4__tar.gz → 0.4.7__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- {rnapolis-0.4.4/src/RNApolis.egg-info → rnapolis-0.4.7}/PKG-INFO +1 -1
- {rnapolis-0.4.4 → rnapolis-0.4.7}/setup.py +1 -1
- {rnapolis-0.4.4 → rnapolis-0.4.7/src/RNApolis.egg-info}/PKG-INFO +1 -1
- {rnapolis-0.4.4 → rnapolis-0.4.7}/src/rnapolis/common.py +4 -1
- {rnapolis-0.4.4 → rnapolis-0.4.7}/src/rnapolis/parser.py +47 -9
- {rnapolis-0.4.4 → rnapolis-0.4.7}/src/rnapolis/tertiary.py +59 -29
- {rnapolis-0.4.4 → rnapolis-0.4.7}/tests/test_annotator.py +11 -0
- {rnapolis-0.4.4 → rnapolis-0.4.7}/tests/test_bugfixes.py +1 -1
- {rnapolis-0.4.4 → rnapolis-0.4.7}/tests/test_common.py +19 -0
- {rnapolis-0.4.4 → rnapolis-0.4.7}/tests/test_parser.py +15 -0
- {rnapolis-0.4.4 → rnapolis-0.4.7}/LICENSE +0 -0
- {rnapolis-0.4.4 → rnapolis-0.4.7}/README.md +0 -0
- {rnapolis-0.4.4 → rnapolis-0.4.7}/pyproject.toml +0 -0
- {rnapolis-0.4.4 → rnapolis-0.4.7}/setup.cfg +0 -0
- {rnapolis-0.4.4 → rnapolis-0.4.7}/src/RNApolis.egg-info/SOURCES.txt +0 -0
- {rnapolis-0.4.4 → rnapolis-0.4.7}/src/RNApolis.egg-info/dependency_links.txt +0 -0
- {rnapolis-0.4.4 → rnapolis-0.4.7}/src/RNApolis.egg-info/entry_points.txt +0 -0
- {rnapolis-0.4.4 → rnapolis-0.4.7}/src/RNApolis.egg-info/requires.txt +0 -0
- {rnapolis-0.4.4 → rnapolis-0.4.7}/src/RNApolis.egg-info/top_level.txt +0 -0
- {rnapolis-0.4.4 → rnapolis-0.4.7}/src/rnapolis/annotator.py +0 -0
- {rnapolis-0.4.4 → rnapolis-0.4.7}/src/rnapolis/clashfinder.py +0 -0
- {rnapolis-0.4.4 → rnapolis-0.4.7}/src/rnapolis/metareader.py +0 -0
- {rnapolis-0.4.4 → rnapolis-0.4.7}/src/rnapolis/molecule_filter.py +0 -0
- {rnapolis-0.4.4 → rnapolis-0.4.7}/src/rnapolis/motif_extractor.py +0 -0
- {rnapolis-0.4.4 → rnapolis-0.4.7}/src/rnapolis/rfam_folder.py +0 -0
- {rnapolis-0.4.4 → rnapolis-0.4.7}/src/rnapolis/transformer.py +0 -0
- {rnapolis-0.4.4 → rnapolis-0.4.7}/src/rnapolis/util.py +0 -0
- {rnapolis-0.4.4 → rnapolis-0.4.7}/tests/test_metareader.py +0 -0
- {rnapolis-0.4.4 → rnapolis-0.4.7}/tests/test_quadruplexes.py +0 -0
- {rnapolis-0.4.4 → rnapolis-0.4.7}/tests/test_rfam_folder.py +0 -0
- {rnapolis-0.4.4 → rnapolis-0.4.7}/tests/test_tertiary.py +0 -0
@@ -338,6 +338,9 @@ class Entry(Sequence):
|
|
338
338
|
return self.pair
|
339
339
|
raise IndexError()
|
340
340
|
|
341
|
+
def __lt__(self, other):
|
342
|
+
return self.index_ < other.index_
|
343
|
+
|
341
344
|
def __len__(self) -> int:
|
342
345
|
return 3
|
343
346
|
|
@@ -838,7 +841,7 @@ class BpSeq:
|
|
838
841
|
|
839
842
|
for i in range(1, len(regions)):
|
840
843
|
k, l, _ = regions[i]
|
841
|
-
available = [True for
|
844
|
+
available = [True for _ in range(len("([{<" + string.ascii_uppercase))]
|
842
845
|
|
843
846
|
for j in range(i):
|
844
847
|
m, n, _ = regions[j]
|
@@ -1,7 +1,10 @@
|
|
1
1
|
import logging
|
2
2
|
from typing import IO, Dict, List, Optional, Tuple, Union
|
3
3
|
|
4
|
+
import numpy as np
|
4
5
|
from mmcif.io.IoAdapterPy import IoAdapterPy
|
6
|
+
from scipy.spatial import KDTree
|
7
|
+
|
5
8
|
from rnapolis.common import ResidueAuth, ResidueLabel
|
6
9
|
from rnapolis.tertiary import BASE_ATOMS, Atom, Residue3D, Structure3D
|
7
10
|
|
@@ -53,10 +56,10 @@ def parse_cif(
|
|
53
56
|
|
54
57
|
io_adapter = IoAdapterPy()
|
55
58
|
data = io_adapter.readFile(cif.name)
|
56
|
-
|
59
|
+
atoms_to_process: List[Atom] = []
|
57
60
|
modified: Dict[Union[ResidueLabel, ResidueAuth], str] = {}
|
58
|
-
sequence_by_entity = {}
|
59
|
-
is_nucleic_acid_by_entity = {}
|
61
|
+
sequence_by_entity: Dict[str, str] = {}
|
62
|
+
is_nucleic_acid_by_entity: Dict[str, bool] = {}
|
60
63
|
|
61
64
|
if data:
|
62
65
|
atom_site = data[0].getObj("atom_site")
|
@@ -136,7 +139,7 @@ def parse_cif(
|
|
136
139
|
else None
|
137
140
|
)
|
138
141
|
|
139
|
-
|
142
|
+
atoms_to_process.append(
|
140
143
|
Atom(
|
141
144
|
label_entity_id,
|
142
145
|
label,
|
@@ -216,6 +219,7 @@ def parse_cif(
|
|
216
219
|
if entity_id and pdbx_seq_one_letter_code_can:
|
217
220
|
sequence_by_entity[entity_id] = pdbx_seq_one_letter_code_can
|
218
221
|
|
222
|
+
atoms = filter_clashing_atoms(atoms_to_process)
|
219
223
|
return atoms, modified, sequence_by_entity, is_nucleic_acid_by_entity
|
220
224
|
|
221
225
|
|
@@ -228,7 +232,7 @@ def parse_pdb(
|
|
228
232
|
Dict[str, bool],
|
229
233
|
]:
|
230
234
|
pdb.seek(0)
|
231
|
-
|
235
|
+
atoms_to_process: List[Atom] = []
|
232
236
|
modified: Dict[Union[ResidueLabel, ResidueAuth], str] = {}
|
233
237
|
model = 1
|
234
238
|
|
@@ -236,9 +240,6 @@ def parse_pdb(
|
|
236
240
|
if line.startswith("MODEL"):
|
237
241
|
model = int(line[10:14].strip())
|
238
242
|
elif line.startswith("ATOM") or line.startswith("HETATM"):
|
239
|
-
alternate_location = line[16]
|
240
|
-
if alternate_location != " ":
|
241
|
-
continue
|
242
243
|
atom_name = line[12:16].strip()
|
243
244
|
residue_name = line[17:20].strip()
|
244
245
|
chain_identifier = line[21]
|
@@ -251,7 +252,10 @@ def parse_pdb(
|
|
251
252
|
auth = ResidueAuth(
|
252
253
|
chain_identifier, residue_number, insertion_code, residue_name
|
253
254
|
)
|
254
|
-
|
255
|
+
|
256
|
+
atoms_to_process.append(
|
257
|
+
Atom(None, None, auth, model, atom_name, x, y, z, occupancy)
|
258
|
+
)
|
255
259
|
elif line.startswith("MODRES"):
|
256
260
|
original_name = line[12:15]
|
257
261
|
chain_identifier = line[16]
|
@@ -263,6 +267,7 @@ def parse_pdb(
|
|
263
267
|
)
|
264
268
|
modified[auth] = standard_residue_name
|
265
269
|
|
270
|
+
atoms = filter_clashing_atoms(atoms_to_process)
|
266
271
|
return atoms, modified, {}, {}
|
267
272
|
|
268
273
|
|
@@ -392,3 +397,36 @@ def try_parse_int(s: str) -> Optional[int]:
|
|
392
397
|
return int(s)
|
393
398
|
except ValueError:
|
394
399
|
return None
|
400
|
+
|
401
|
+
|
402
|
+
def filter_clashing_atoms(atoms: List[Atom], clash_distance: float = 0.5) -> List[Atom]:
|
403
|
+
# First, remove duplicate atoms
|
404
|
+
unique_atoms = {}
|
405
|
+
|
406
|
+
for i, atom in enumerate(atoms):
|
407
|
+
key = (atom.label, atom.auth, atom.name)
|
408
|
+
if key not in unique_atoms or atom.occupancy > unique_atoms[key].occupancy:
|
409
|
+
unique_atoms[key] = atom
|
410
|
+
|
411
|
+
unique_atoms_list = list(unique_atoms.values())
|
412
|
+
|
413
|
+
# Now handle clashing atoms
|
414
|
+
coords = np.array([(atom.x, atom.y, atom.z) for atom in unique_atoms_list])
|
415
|
+
tree = KDTree(coords)
|
416
|
+
|
417
|
+
pairs = tree.query_pairs(r=clash_distance)
|
418
|
+
|
419
|
+
atoms_to_keep = set(range(len(unique_atoms_list)))
|
420
|
+
|
421
|
+
for i, j in pairs:
|
422
|
+
if (
|
423
|
+
unique_atoms_list[i].occupancy is None
|
424
|
+
or unique_atoms_list[j].occupancy is None
|
425
|
+
):
|
426
|
+
continue
|
427
|
+
if unique_atoms_list[i].occupancy > unique_atoms_list[j].occupancy:
|
428
|
+
atoms_to_keep.discard(j)
|
429
|
+
else:
|
430
|
+
atoms_to_keep.discard(i)
|
431
|
+
|
432
|
+
return [unique_atoms_list[i] for i in atoms_to_keep]
|
@@ -124,36 +124,17 @@ class Residue3D(Residue):
|
|
124
124
|
outermost_atoms = {"A": "N9", "G": "N9", "C": "N1", "U": "N1", "T": "N1"}
|
125
125
|
# Dist representing expected name of atom closest to the tetrad center
|
126
126
|
innermost_atoms = {"A": "N6", "G": "O6", "C": "N4", "U": "O4", "T": "O4"}
|
127
|
+
# Heavy atoms in phosphate and ribose
|
128
|
+
phosphate_atoms = {"P", "OP1", "OP2", "O3'", "O5'"}
|
129
|
+
sugar_atoms = {"C1'", "C2'", "C3'", "C4'", "C5'", "O4'"}
|
127
130
|
# Heavy atoms for each main nucleobase
|
128
131
|
nucleobase_heavy_atoms = {
|
129
132
|
"A": set(["N1", "C2", "N3", "C4", "C5", "C6", "N6", "N7", "C8", "N9"]),
|
130
133
|
"G": set(["N1", "C2", "N2", "N3", "C4", "C5", "C6", "O6", "N7", "C8", "N9"]),
|
131
134
|
"C": set(["N1", "C2", "O2", "N3", "C4", "N4", "C5", "C6"]),
|
132
135
|
"U": set(["N1", "C2", "O2", "N3", "C4", "O4", "C5", "C6"]),
|
136
|
+
"T": set(["N1", "C2", "O2", "N3", "C4", "O4", "C5", "C5M", "C6"]),
|
133
137
|
}
|
134
|
-
# Heavy atoms in nucleotide
|
135
|
-
nucleotide_heavy_atoms = (
|
136
|
-
set(
|
137
|
-
[
|
138
|
-
"P",
|
139
|
-
"OP1",
|
140
|
-
"OP2",
|
141
|
-
"O5'",
|
142
|
-
"C5'",
|
143
|
-
"C4'",
|
144
|
-
"O4'",
|
145
|
-
"C3'",
|
146
|
-
"O3'",
|
147
|
-
"C2'",
|
148
|
-
"O2'",
|
149
|
-
"C1'",
|
150
|
-
]
|
151
|
-
)
|
152
|
-
.union(nucleobase_heavy_atoms["A"])
|
153
|
-
.union(nucleobase_heavy_atoms["G"])
|
154
|
-
.union(nucleobase_heavy_atoms["C"])
|
155
|
-
.union(nucleobase_heavy_atoms["U"])
|
156
|
-
)
|
157
138
|
|
158
139
|
def __lt__(self, other):
|
159
140
|
return (self.model, self.chain, self.number, self.icode or " ") < (
|
@@ -202,9 +183,59 @@ class Residue3D(Residue):
|
|
202
183
|
|
203
184
|
@cached_property
|
204
185
|
def is_nucleotide(self) -> bool:
|
205
|
-
|
206
|
-
|
186
|
+
scores = {"phosphate": 0.0, "sugar": 0.0, "base": 0.0, "connections": 0.0}
|
187
|
+
weights = {"phosphate": 0.25, "sugar": 0.25, "base": 0.25, "connections": 0.25}
|
188
|
+
|
189
|
+
residue_atoms = {atom.name for atom in self.atoms}
|
190
|
+
|
191
|
+
phosphate_match = len(residue_atoms.intersection(self.phosphate_atoms))
|
192
|
+
scores["phosphate"] = phosphate_match / len(self.phosphate_atoms)
|
193
|
+
|
194
|
+
sugar_match = len(residue_atoms.intersection(self.sugar_atoms))
|
195
|
+
scores["sugar"] = sugar_match / len(self.sugar_atoms)
|
196
|
+
|
197
|
+
nucleobase_atoms = {
|
198
|
+
key: self.nucleobase_heavy_atoms[key] for key in self.nucleobase_heavy_atoms
|
199
|
+
}
|
200
|
+
matches = {
|
201
|
+
key: len(residue_atoms.intersection(nucleobase_atoms[key]))
|
202
|
+
/ len(nucleobase_atoms[key])
|
203
|
+
for key in nucleobase_atoms
|
204
|
+
}
|
205
|
+
best_match = max(matches.items(), key=lambda x: x[1])
|
206
|
+
scores["base"] = best_match[1]
|
207
|
+
|
208
|
+
connection_score = 0.0
|
209
|
+
distance_threshold = 2.0
|
210
|
+
|
211
|
+
if "P" in residue_atoms and "O5'" in residue_atoms:
|
212
|
+
p_atom = next(atom for atom in self.atoms if atom.name == "P")
|
213
|
+
o5_atom = next(atom for atom in self.atoms if atom.name == "O5'")
|
214
|
+
if (
|
215
|
+
numpy.linalg.norm(p_atom.coordinates - o5_atom.coordinates)
|
216
|
+
<= distance_threshold
|
217
|
+
):
|
218
|
+
connection_score += 0.5
|
219
|
+
if "C1'" in residue_atoms:
|
220
|
+
c1_atom = next(atom for atom in self.atoms if atom.name == "C1'")
|
221
|
+
for base_connection in ["N9", "N1"]:
|
222
|
+
if base_connection in residue_atoms:
|
223
|
+
base_atom = next(
|
224
|
+
atom for atom in self.atoms if atom.name == base_connection
|
225
|
+
)
|
226
|
+
if (
|
227
|
+
numpy.linalg.norm(c1_atom.coordinates - base_atom.coordinates)
|
228
|
+
<= distance_threshold
|
229
|
+
):
|
230
|
+
connection_score += 0.5
|
231
|
+
break
|
232
|
+
|
233
|
+
scores["connections"] = connection_score
|
234
|
+
|
235
|
+
probability = sum(
|
236
|
+
scores[component] * weights[component] for component in scores.keys()
|
207
237
|
)
|
238
|
+
return probability > 0.5
|
208
239
|
|
209
240
|
@cached_property
|
210
241
|
def base_normal_vector(self) -> Optional[numpy.typing.NDArray[numpy.floating]]:
|
@@ -566,15 +597,14 @@ class Mapping2D3D:
|
|
566
597
|
return self.__generate_bpseq(canonical)
|
567
598
|
|
568
599
|
def __generate_bpseq(self, base_pairs):
|
600
|
+
nucleotides = list(filter(lambda r: r.is_nucleotide, self.structure3d.residues))
|
569
601
|
result: Dict[int, List] = {}
|
570
602
|
residue_map: Dict[Residue3D, int] = {}
|
571
603
|
i = 1
|
572
604
|
|
573
|
-
for j, residue in enumerate(
|
574
|
-
filter(lambda r: r.is_nucleotide, self.structure3d.residues)
|
575
|
-
):
|
605
|
+
for j, residue in enumerate(nucleotides):
|
576
606
|
if self.find_gaps and j > 0:
|
577
|
-
previous =
|
607
|
+
previous = nucleotides[j - 1]
|
578
608
|
|
579
609
|
if (
|
580
610
|
not previous.is_connected(residue)
|
@@ -43,3 +43,14 @@ def test_8btk():
|
|
43
43
|
with open("tests/8btk_B7.cif") as f:
|
44
44
|
structure3d = read_3d_structure(f, 1)
|
45
45
|
assert extract_secondary_structure(structure3d, 1) is not None
|
46
|
+
|
47
|
+
|
48
|
+
def test_488d():
|
49
|
+
"""
|
50
|
+
There are clashing residues 151 in chains B and D. The clash is caused by occupancy factors less than 1.
|
51
|
+
"""
|
52
|
+
with open("tests/488d.pdb") as f:
|
53
|
+
structure3d = read_3d_structure(f)
|
54
|
+
|
55
|
+
base_interactions = extract_base_interactions(structure3d)
|
56
|
+
assert base_interactions is not None
|
@@ -42,7 +42,7 @@ def test_4WTI():
|
|
42
42
|
mapping = Mapping2D3D(
|
43
43
|
structure3d, base_interactions.basePairs, base_interactions.stackings, True
|
44
44
|
)
|
45
|
-
assert mapping.dot_bracket == ">strand_T\
|
45
|
+
assert mapping.dot_bracket == ">strand_T\nCGG\n.((\n>strand_P\nCC\n))"
|
46
46
|
|
47
47
|
|
48
48
|
# in 1HMH the bases are oriented in 45 degrees and it caused the program to identify invalid base pair
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import string
|
1
2
|
from collections import Counter
|
2
3
|
|
3
4
|
import orjson
|
@@ -11,6 +12,7 @@ from rnapolis.common import (
|
|
11
12
|
BaseRibose,
|
12
13
|
BpSeq,
|
13
14
|
DotBracket,
|
15
|
+
Entry,
|
14
16
|
Interaction,
|
15
17
|
LeontisWesthof,
|
16
18
|
MultiStrandDotBracket,
|
@@ -180,3 +182,20 @@ def test_conflicted_base_pairs():
|
|
180
182
|
assert (
|
181
183
|
mapping.dot_bracket == ">strand_B\nGGACUAGCGGAGGCUAGUCC\n((((((((....))))))))"
|
182
184
|
)
|
185
|
+
|
186
|
+
|
187
|
+
def test_high_level_pseudoknot():
|
188
|
+
entries = []
|
189
|
+
brackets = "([{<" + string.ascii_uppercase
|
190
|
+
|
191
|
+
for i in range(len(brackets)):
|
192
|
+
entries.append(Entry(i + 1, "C", i + len(brackets) + 1))
|
193
|
+
entries.append(Entry(i + len(brackets) + 1, "G", i + 1))
|
194
|
+
|
195
|
+
bpseq = BpSeq(sorted(entries))
|
196
|
+
dot_bracket = bpseq.fcfs
|
197
|
+
assert dot_bracket.sequence == "C" * len(brackets) + "G" * len(brackets)
|
198
|
+
assert (
|
199
|
+
dot_bracket.structure
|
200
|
+
== "([{<" + string.ascii_uppercase + ")]}>" + string.ascii_lowercase
|
201
|
+
)
|
@@ -16,3 +16,18 @@ def test_1ato():
|
|
16
16
|
structure3d = read_3d_structure(f)
|
17
17
|
sequence = "".join([residue.one_letter_name for residue in structure3d.residues])
|
18
18
|
assert sequence == "GGCACCUCCUCGCGGUGCC"
|
19
|
+
|
20
|
+
|
21
|
+
def test_4qln_no_duplicate_atoms():
|
22
|
+
for ext in (".pdb", ".cif"):
|
23
|
+
with open(f"tests/4qln{ext}") as f:
|
24
|
+
structure3d = read_3d_structure(f)
|
25
|
+
|
26
|
+
chain_a = [r for r in structure3d.residues if r.auth.chain == "A"]
|
27
|
+
residues_to_check = [r for r in chain_a if r.auth.number in (18, 19, 20)]
|
28
|
+
|
29
|
+
for residue in residues_to_check:
|
30
|
+
atom_names = [atom.name for atom in residue.atoms]
|
31
|
+
assert len(atom_names) == len(
|
32
|
+
set(atom_names)
|
33
|
+
), f"Duplicate atoms found in residue {residue.auth}"
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|