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.
Files changed (31) hide show
  1. {rnapolis-0.4.4/src/RNApolis.egg-info → rnapolis-0.4.7}/PKG-INFO +1 -1
  2. {rnapolis-0.4.4 → rnapolis-0.4.7}/setup.py +1 -1
  3. {rnapolis-0.4.4 → rnapolis-0.4.7/src/RNApolis.egg-info}/PKG-INFO +1 -1
  4. {rnapolis-0.4.4 → rnapolis-0.4.7}/src/rnapolis/common.py +4 -1
  5. {rnapolis-0.4.4 → rnapolis-0.4.7}/src/rnapolis/parser.py +47 -9
  6. {rnapolis-0.4.4 → rnapolis-0.4.7}/src/rnapolis/tertiary.py +59 -29
  7. {rnapolis-0.4.4 → rnapolis-0.4.7}/tests/test_annotator.py +11 -0
  8. {rnapolis-0.4.4 → rnapolis-0.4.7}/tests/test_bugfixes.py +1 -1
  9. {rnapolis-0.4.4 → rnapolis-0.4.7}/tests/test_common.py +19 -0
  10. {rnapolis-0.4.4 → rnapolis-0.4.7}/tests/test_parser.py +15 -0
  11. {rnapolis-0.4.4 → rnapolis-0.4.7}/LICENSE +0 -0
  12. {rnapolis-0.4.4 → rnapolis-0.4.7}/README.md +0 -0
  13. {rnapolis-0.4.4 → rnapolis-0.4.7}/pyproject.toml +0 -0
  14. {rnapolis-0.4.4 → rnapolis-0.4.7}/setup.cfg +0 -0
  15. {rnapolis-0.4.4 → rnapolis-0.4.7}/src/RNApolis.egg-info/SOURCES.txt +0 -0
  16. {rnapolis-0.4.4 → rnapolis-0.4.7}/src/RNApolis.egg-info/dependency_links.txt +0 -0
  17. {rnapolis-0.4.4 → rnapolis-0.4.7}/src/RNApolis.egg-info/entry_points.txt +0 -0
  18. {rnapolis-0.4.4 → rnapolis-0.4.7}/src/RNApolis.egg-info/requires.txt +0 -0
  19. {rnapolis-0.4.4 → rnapolis-0.4.7}/src/RNApolis.egg-info/top_level.txt +0 -0
  20. {rnapolis-0.4.4 → rnapolis-0.4.7}/src/rnapolis/annotator.py +0 -0
  21. {rnapolis-0.4.4 → rnapolis-0.4.7}/src/rnapolis/clashfinder.py +0 -0
  22. {rnapolis-0.4.4 → rnapolis-0.4.7}/src/rnapolis/metareader.py +0 -0
  23. {rnapolis-0.4.4 → rnapolis-0.4.7}/src/rnapolis/molecule_filter.py +0 -0
  24. {rnapolis-0.4.4 → rnapolis-0.4.7}/src/rnapolis/motif_extractor.py +0 -0
  25. {rnapolis-0.4.4 → rnapolis-0.4.7}/src/rnapolis/rfam_folder.py +0 -0
  26. {rnapolis-0.4.4 → rnapolis-0.4.7}/src/rnapolis/transformer.py +0 -0
  27. {rnapolis-0.4.4 → rnapolis-0.4.7}/src/rnapolis/util.py +0 -0
  28. {rnapolis-0.4.4 → rnapolis-0.4.7}/tests/test_metareader.py +0 -0
  29. {rnapolis-0.4.4 → rnapolis-0.4.7}/tests/test_quadruplexes.py +0 -0
  30. {rnapolis-0.4.4 → rnapolis-0.4.7}/tests/test_rfam_folder.py +0 -0
  31. {rnapolis-0.4.4 → rnapolis-0.4.7}/tests/test_tertiary.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: RNApolis
3
- Version: 0.4.4
3
+ Version: 0.4.7
4
4
  Summary: A Python library containing RNA-related bioinformatics functions and classes
5
5
  Home-page: https://github.com/tzok/rnapolis-py
6
6
  Author: Tomasz Zok
@@ -5,7 +5,7 @@ with open("README.md") as f:
5
5
 
6
6
  setup(
7
7
  name="RNApolis",
8
- version="0.4.4",
8
+ version="0.4.7",
9
9
  packages=["rnapolis"],
10
10
  package_dir={"": "src"},
11
11
  author="Tomasz Zok",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: RNApolis
3
- Version: 0.4.4
3
+ Version: 0.4.7
4
4
  Summary: A Python library containing RNA-related bioinformatics functions and classes
5
5
  Home-page: https://github.com/tzok/rnapolis-py
6
6
  Author: Tomasz Zok
@@ -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 i in range(10)]
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
- atoms: List[Atom] = []
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
- atoms.append(
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
- atoms: List[Atom] = []
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
- atoms.append(Atom(None, None, auth, model, atom_name, x, y, z, occupancy))
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
- return self.nucleotide_heavy_atoms.intersection(
206
- set([atom.name for atom in self.atoms])
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 = self.structure3d.residues[j - 1]
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\nACGG\n..((\n>strand_P\nCC\n))"
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