nuc2d 0.1.2__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.
- nuc2d/__init__.py +5 -0
- nuc2d/annotation.py +78 -0
- nuc2d/draw.py +106 -0
- nuc2d/layout.py +321 -0
- nuc2d/parser.py +205 -0
- nuc2d/structure.py +96 -0
- nuc2d/style.py +75 -0
- nuc2d/svg.py +596 -0
- nuc2d/vec2.py +108 -0
- nuc2d-0.1.2.dist-info/METADATA +66 -0
- nuc2d-0.1.2.dist-info/RECORD +14 -0
- nuc2d-0.1.2.dist-info/WHEEL +5 -0
- nuc2d-0.1.2.dist-info/licenses/LICENSE +21 -0
- nuc2d-0.1.2.dist-info/top_level.txt +1 -0
nuc2d/__init__.py
ADDED
nuc2d/annotation.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Utilities for attaching annotations to RNA secondary structures.
|
|
2
|
+
|
|
3
|
+
This module provides functions for adding biological or visualization-
|
|
4
|
+
related annotations to parsed secondary structure objects. Examples
|
|
5
|
+
include nucleotide sequences, base-pair probabilities, and other
|
|
6
|
+
metadata associated with nucleotides or structural elements.
|
|
7
|
+
|
|
8
|
+
Annotations are applied after parsing and before layout or rendering,
|
|
9
|
+
allowing structural topology and auxiliary information to remain
|
|
10
|
+
separated.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from .structure import (
|
|
16
|
+
StemRegion,
|
|
17
|
+
LoopRegion,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def attach_sequences(root_loop: LoopRegion, sequences: list[str]):
|
|
21
|
+
"""Attach nucleotide sequences to a secondary structure.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
root_loop : LoopRegion
|
|
26
|
+
Root loop of the secondary structure.
|
|
27
|
+
sequences : list[str]
|
|
28
|
+
Nucleotide sequences for all strands.
|
|
29
|
+
"""
|
|
30
|
+
def _attach_stem_sequences(current_stem: StemRegion):
|
|
31
|
+
for nt in current_stem.nucleotides[1:-1]:
|
|
32
|
+
nt.base = sequences[nt.strand_index][nt.index_in_strand]
|
|
33
|
+
_attach_loop_sequences(current_stem.child_loop)
|
|
34
|
+
|
|
35
|
+
def _attach_loop_sequences(current_loop: LoopRegion):
|
|
36
|
+
if current_loop.is_root:
|
|
37
|
+
nucleotides = current_loop.nucleotides
|
|
38
|
+
else:
|
|
39
|
+
nucleotides = current_loop.nucleotides[1:-1]
|
|
40
|
+
for nt in nucleotides:
|
|
41
|
+
nt.base = sequences[nt.strand_index][nt.index_in_strand]
|
|
42
|
+
for stem in current_loop.child_stems:
|
|
43
|
+
_attach_stem_sequences(stem)
|
|
44
|
+
|
|
45
|
+
_attach_loop_sequences(root_loop)
|
|
46
|
+
|
|
47
|
+
def attach_basepair_probabilities(root_loop: LoopRegion, probs: np.ndarray):
|
|
48
|
+
"""Attach base-pair probabilities to a secondary structure.
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
root_loop : LoopRegion
|
|
53
|
+
Root loop of the secondary structure.
|
|
54
|
+
probs : ndarray
|
|
55
|
+
Base-pair probability matrix. Element (i, j) gives the probability
|
|
56
|
+
that nucleotide i pairs with nucleotide j. Diagonal elements give
|
|
57
|
+
the probabilities that nucleotides remain unpaired.
|
|
58
|
+
"""
|
|
59
|
+
def _attach_stem_probs(current_stem: StemRegion):
|
|
60
|
+
nucleotides = current_stem.nucleotides
|
|
61
|
+
for idx in range(len(nucleotides)//2):
|
|
62
|
+
nt1 = nucleotides[idx]
|
|
63
|
+
nt2 = nucleotides[-(idx+1)]
|
|
64
|
+
prob = probs[nt1.index][nt2.index]
|
|
65
|
+
nt1.basepair_probability = nt2.basepair_probability = prob
|
|
66
|
+
_attach_loop_probs(current_stem.child_loop)
|
|
67
|
+
|
|
68
|
+
def _attach_loop_probs(current_loop: LoopRegion):
|
|
69
|
+
if current_loop.is_root:
|
|
70
|
+
nucleotides = current_loop.nucleotides
|
|
71
|
+
else:
|
|
72
|
+
nucleotides = current_loop.nucleotides[1:-1]
|
|
73
|
+
for nt in nucleotides:
|
|
74
|
+
nt.basepair_probability = probs[nt.index][nt.index]
|
|
75
|
+
for stem in current_loop.child_stems:
|
|
76
|
+
_attach_stem_probs(stem)
|
|
77
|
+
|
|
78
|
+
_attach_loop_probs(root_loop)
|
nuc2d/draw.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""High-level drawing interface for nucleic acid secondary structures.
|
|
2
|
+
|
|
3
|
+
This module provides convenience functions for generating SVG drawings
|
|
4
|
+
directly from secondary structure strings. Parsing, annotation,
|
|
5
|
+
layout generation, and rendering are performed automatically.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import svgwrite
|
|
12
|
+
|
|
13
|
+
from .parser import parse
|
|
14
|
+
from .annotation import (
|
|
15
|
+
attach_sequences,
|
|
16
|
+
attach_basepair_probabilities,
|
|
17
|
+
)
|
|
18
|
+
from .layout import RadialLayoutEngine
|
|
19
|
+
from .style import DrawingStyle
|
|
20
|
+
from .svg import (
|
|
21
|
+
PlacedComponent,
|
|
22
|
+
render_structure,
|
|
23
|
+
render_colorbar,
|
|
24
|
+
compose,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def draw_svg(
|
|
29
|
+
dpp_string: str,
|
|
30
|
+
sequences: Optional[list[str]] = None,
|
|
31
|
+
probs: Optional[np.ndarray] = None,
|
|
32
|
+
style: Optional[DrawingStyle] = None,
|
|
33
|
+
target_height: float = 500.0,
|
|
34
|
+
) -> svgwrite.Drawing:
|
|
35
|
+
"""Generate an SVG drawing from a secondary structure string.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
dpp_string : str
|
|
40
|
+
A secondary structure written in dot-parens-plus notation.
|
|
41
|
+
sequences : list[str], optional
|
|
42
|
+
A list of sequences corresponding to the structure.
|
|
43
|
+
probs : ndarray, optional
|
|
44
|
+
Base-pair probability matrix.
|
|
45
|
+
style : DrawingStyle, optional
|
|
46
|
+
Drawing style configuration.
|
|
47
|
+
target_height : float, default=500.0
|
|
48
|
+
Target height of the rendered secondary structure in the SVG
|
|
49
|
+
coordinate system.
|
|
50
|
+
|
|
51
|
+
Returns
|
|
52
|
+
-------
|
|
53
|
+
svgwrite.Drawing
|
|
54
|
+
Generated SVG drawing.
|
|
55
|
+
"""
|
|
56
|
+
# Parse the secondary structure string.
|
|
57
|
+
root_loop = parse(dpp_string)
|
|
58
|
+
|
|
59
|
+
# Attach sequence and probability annotations.
|
|
60
|
+
if sequences is not None:
|
|
61
|
+
attach_sequences(root_loop, sequences)
|
|
62
|
+
if probs is not None:
|
|
63
|
+
attach_basepair_probabilities(root_loop, probs)
|
|
64
|
+
|
|
65
|
+
# Compute nucleotide positions and drawing geometry.
|
|
66
|
+
layout_result = RadialLayoutEngine().layout(root_loop)
|
|
67
|
+
|
|
68
|
+
# Create an empty SVG drawing that will hold all rendered components.
|
|
69
|
+
svg_drawing = svgwrite.Drawing()
|
|
70
|
+
|
|
71
|
+
# Render the RNA secondary structure as an independent SVG component.
|
|
72
|
+
structure = render_structure(
|
|
73
|
+
svg_drawing,
|
|
74
|
+
layout_result,
|
|
75
|
+
style,
|
|
76
|
+
)
|
|
77
|
+
placed_components = [
|
|
78
|
+
PlacedComponent(
|
|
79
|
+
component=structure,
|
|
80
|
+
x=0.0,
|
|
81
|
+
y=0.0,
|
|
82
|
+
scale=target_height / structure.height,
|
|
83
|
+
)
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
# Add a colorbar when base-pair probabilities are visualized.
|
|
87
|
+
if probs is not None:
|
|
88
|
+
colorbar = render_colorbar(
|
|
89
|
+
svg_drawing,
|
|
90
|
+
style=style,
|
|
91
|
+
)
|
|
92
|
+
placed_components.append(
|
|
93
|
+
PlacedComponent(
|
|
94
|
+
component=colorbar,
|
|
95
|
+
x=placed_components[0].width,
|
|
96
|
+
y=0.0,
|
|
97
|
+
scale=target_height / colorbar.height,
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Compose all positioned components into the final SVG drawing.
|
|
102
|
+
compose(
|
|
103
|
+
svg_drawing,
|
|
104
|
+
placed_components,
|
|
105
|
+
)
|
|
106
|
+
return svg_drawing
|
nuc2d/layout.py
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""Geometry layout generation for secondary structure visualization.
|
|
2
|
+
|
|
3
|
+
This module converts secondary structure representations into drawable
|
|
4
|
+
geometric layouts. The generated layouts define spatial relationships
|
|
5
|
+
between nucleotides, stems, loops, and their connections, independently
|
|
6
|
+
from rendering.
|
|
7
|
+
|
|
8
|
+
The layout result typically consists of layout nodes and edges annotated
|
|
9
|
+
with geometric information such as positions, orientations, and edge
|
|
10
|
+
shapes.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from enum import Enum, auto
|
|
17
|
+
from abc import ABC, abstractmethod
|
|
18
|
+
import math
|
|
19
|
+
|
|
20
|
+
from .structure import Nucleotide, LoopRegion, StemRegion
|
|
21
|
+
from .vec2 import Vec2
|
|
22
|
+
|
|
23
|
+
class EdgeType(Enum):
|
|
24
|
+
"""Enumeration of edge types used in the drawing graph."""
|
|
25
|
+
BACKBONE = auto()
|
|
26
|
+
BASE_PAIR = auto()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class Node:
|
|
31
|
+
"""Node representing a nucleotide and its drawing position.
|
|
32
|
+
|
|
33
|
+
Attributes
|
|
34
|
+
----------
|
|
35
|
+
nucleotide : Nucleotide
|
|
36
|
+
Nucleotide associated with this node.
|
|
37
|
+
pos : Vec2
|
|
38
|
+
Position of the node in the drawing coordinate system.
|
|
39
|
+
"""
|
|
40
|
+
nucleotide: Nucleotide
|
|
41
|
+
pos: Vec2
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class Edge:
|
|
46
|
+
"""Base class representing a connection between two nodes.
|
|
47
|
+
|
|
48
|
+
Attributes
|
|
49
|
+
----------
|
|
50
|
+
start : Node
|
|
51
|
+
Start node of the edge.
|
|
52
|
+
end : Node
|
|
53
|
+
End node of the edge.
|
|
54
|
+
type : EdgeType
|
|
55
|
+
Type of the edge.
|
|
56
|
+
"""
|
|
57
|
+
start: Node
|
|
58
|
+
end: Node
|
|
59
|
+
type: EdgeType
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class LineEdge(Edge):
|
|
64
|
+
"""Edge represented as a straight line segment."""
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class ArcEdge(Edge):
|
|
70
|
+
"""Edge represented as an SVG elliptical arc.
|
|
71
|
+
|
|
72
|
+
Attributes
|
|
73
|
+
----------
|
|
74
|
+
rx : float
|
|
75
|
+
Radius of the ellipse along the x-axis.
|
|
76
|
+
ry : float
|
|
77
|
+
Radius of the ellipse along the y-axis.
|
|
78
|
+
x_axis_rotation : float
|
|
79
|
+
Rotation angle of the ellipse x-axis in degrees.
|
|
80
|
+
large_arc : bool
|
|
81
|
+
Whether to use the larger arc between the endpoints.
|
|
82
|
+
sweep : bool
|
|
83
|
+
Direction of the arc sweep.
|
|
84
|
+
"""
|
|
85
|
+
rx: float
|
|
86
|
+
ry: float
|
|
87
|
+
x_axis_rotation: float
|
|
88
|
+
large_arc: bool
|
|
89
|
+
sweep: bool
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class Marker():
|
|
93
|
+
node: Node
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class ArrowMarker(Marker):
|
|
97
|
+
direction: Vec2
|
|
98
|
+
length: float = 7.0
|
|
99
|
+
is_start: bool = True
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class LayoutResult():
|
|
103
|
+
"""Container for the generated layout information.
|
|
104
|
+
|
|
105
|
+
Attributes
|
|
106
|
+
----------
|
|
107
|
+
nodes : list[Node]
|
|
108
|
+
Nodes with computed layout positions.
|
|
109
|
+
edges : list[Edge]
|
|
110
|
+
Edges connecting the laid out nodes.
|
|
111
|
+
markers : list[Marker]
|
|
112
|
+
Markers associated with the layout, such as directional annotations.
|
|
113
|
+
"""
|
|
114
|
+
nodes: list[Node]
|
|
115
|
+
edges: list[Edge]
|
|
116
|
+
markers: list[Marker]
|
|
117
|
+
|
|
118
|
+
class LayoutEngine(ABC):
|
|
119
|
+
"""Abstract base class for secondary structure layout engines."""
|
|
120
|
+
|
|
121
|
+
@abstractmethod
|
|
122
|
+
def layout(self, root_loop: LoopRegion):
|
|
123
|
+
"""Compute a layout for the given secondary structure."""
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
class RadialLayoutEngine(LayoutEngine):
|
|
127
|
+
"""Layout engine for generating a radial representation of a secondary structure.
|
|
128
|
+
|
|
129
|
+
This layout engine places nucleotides and structural elements using a
|
|
130
|
+
radial geometry based on backbone lengths, base-pair lengths, and
|
|
131
|
+
deflection angles between connected regions.
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
backbone_length : float, default=15
|
|
136
|
+
Length assigned to backbone connections between adjacent nucleotides.
|
|
137
|
+
basepair_length : float, default=20
|
|
138
|
+
Length assigned to base-pair connections in stem regions.
|
|
139
|
+
deflection_angle : float, default=math.pi/18
|
|
140
|
+
Angular deflection applied when traversing connected regions.
|
|
141
|
+
|
|
142
|
+
Attributes
|
|
143
|
+
----------
|
|
144
|
+
backbone_length : float
|
|
145
|
+
Length assigned to backbone connections.
|
|
146
|
+
basepair_length : float
|
|
147
|
+
Length assigned to base-pair connections.
|
|
148
|
+
deflection_angle : float
|
|
149
|
+
Angular deflection between connected regions.
|
|
150
|
+
nodes : list[Node]
|
|
151
|
+
Layout nodes generated during layout computation.
|
|
152
|
+
edges : list[Edge]
|
|
153
|
+
Layout edges generated during layout computation.
|
|
154
|
+
current_pos : Vec2
|
|
155
|
+
Current position used during recursive layout generation.
|
|
156
|
+
current_vec : Vec2
|
|
157
|
+
Current unit direction vector used during recursive layout generation.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
def __init__(
|
|
161
|
+
self,
|
|
162
|
+
backbone_length: float = 15,
|
|
163
|
+
basepair_length: float = 20,
|
|
164
|
+
deflection_angle: float = math.pi/18,
|
|
165
|
+
) -> None:
|
|
166
|
+
self.backbone_length = backbone_length
|
|
167
|
+
self.basepair_length = basepair_length
|
|
168
|
+
self.deflection_angle = deflection_angle
|
|
169
|
+
self.nodes: list[Node] = []
|
|
170
|
+
self.edges: list[Edge] = []
|
|
171
|
+
self.markers: list[Marker] = []
|
|
172
|
+
self.current_pos: Vec2 = Vec2(0, 0)
|
|
173
|
+
self.current_vec: Vec2 = Vec2(1, 0)
|
|
174
|
+
|
|
175
|
+
def add_last_stem_backbone(self):
|
|
176
|
+
if not self.nodes[-2].nucleotide.is_three_prime:
|
|
177
|
+
self.edges.append(LineEdge(self.nodes[-2], self.nodes[-1], EdgeType.BACKBONE))
|
|
178
|
+
|
|
179
|
+
def add_last_loop_backbone(self, radius: float):
|
|
180
|
+
if not self.nodes[-2].nucleotide.is_three_prime:
|
|
181
|
+
self.edges.append(ArcEdge(self.nodes[-2], self.nodes[-1], EdgeType.BACKBONE, radius, radius, 0, 0, 1))
|
|
182
|
+
|
|
183
|
+
def layout_stem(
|
|
184
|
+
self,
|
|
185
|
+
current_stem: StemRegion,
|
|
186
|
+
) -> None:
|
|
187
|
+
"""Generate layout information for a stem region.
|
|
188
|
+
|
|
189
|
+
Parameters
|
|
190
|
+
----------
|
|
191
|
+
current_stem : StemRegion
|
|
192
|
+
Stem region to layout.
|
|
193
|
+
"""
|
|
194
|
+
self.current_vec = self.current_vec.normalized()
|
|
195
|
+
stem_length = len(current_stem.nucleotides)//2
|
|
196
|
+
start_node = self.nodes[-1]
|
|
197
|
+
for start_idx in [0, stem_length]:
|
|
198
|
+
nucleotides = current_stem.nucleotides[start_idx+1:start_idx+stem_length]
|
|
199
|
+
for nt in nucleotides:
|
|
200
|
+
# Generate nodes
|
|
201
|
+
self.current_pos += self.current_vec * self.backbone_length
|
|
202
|
+
self.nodes.append(Node(nt, self.current_pos))
|
|
203
|
+
# Generate backbones
|
|
204
|
+
self.add_last_stem_backbone()
|
|
205
|
+
# Generate markers for 3' termini
|
|
206
|
+
if nucleotides and nucleotides[-1].is_three_prime:
|
|
207
|
+
self.markers.append(ArrowMarker(self.nodes[-1], self.current_vec))
|
|
208
|
+
# Layout child loop region
|
|
209
|
+
if start_idx == 0:
|
|
210
|
+
child_loop = current_stem.child_loop
|
|
211
|
+
if not child_loop.is_hinge:
|
|
212
|
+
self.current_vec = self.current_vec.rotated(-math.pi/2)
|
|
213
|
+
self.layout_loop(child_loop)
|
|
214
|
+
if not child_loop.is_hinge:
|
|
215
|
+
self.current_vec = self.current_vec.rotated(-math.pi/2)
|
|
216
|
+
# Generate base pairs
|
|
217
|
+
base_idx = self.nodes.index(start_node)
|
|
218
|
+
for idx in range(stem_length):
|
|
219
|
+
self.edges.append(LineEdge(self.nodes[base_idx+idx], self.nodes[-(idx+1)], EdgeType.BASE_PAIR))
|
|
220
|
+
self.current_vec = self.current_vec.normalized()
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
def layout_loop(
|
|
224
|
+
self,
|
|
225
|
+
current_loop: LoopRegion,
|
|
226
|
+
) -> None:
|
|
227
|
+
"""Generate layout information for a loop region.
|
|
228
|
+
|
|
229
|
+
Parameters
|
|
230
|
+
----------
|
|
231
|
+
current_loop : LoopRegion
|
|
232
|
+
Loop region to layout.
|
|
233
|
+
"""
|
|
234
|
+
self.current_vec = self.current_vec.normalized()
|
|
235
|
+
nucleotides = current_loop.nucleotides
|
|
236
|
+
child_stems = current_loop.child_stems
|
|
237
|
+
if (current_loop.is_root
|
|
238
|
+
and child_stems
|
|
239
|
+
and child_stems[0].nucleotides[0] is nucleotides[0]):
|
|
240
|
+
self.current_vec = self.current_vec.rotated(-math.pi/2)
|
|
241
|
+
self.layout_stem(child_stems[0])
|
|
242
|
+
if not current_loop.is_hinge:
|
|
243
|
+
self.current_vec = self.current_vec.rotated(-math.pi/2)
|
|
244
|
+
nucleotides = nucleotides[1:]
|
|
245
|
+
child_stems = child_stems[1:]
|
|
246
|
+
if current_loop.is_hinge:
|
|
247
|
+
defl_angle = (
|
|
248
|
+
self.deflection_angle
|
|
249
|
+
if nucleotides[0].is_three_prime
|
|
250
|
+
else -self.deflection_angle
|
|
251
|
+
)
|
|
252
|
+
intermediate_vec = self.current_vec.rotated(defl_angle/2)
|
|
253
|
+
delta = self.basepair_length * math.sin(defl_angle/2)
|
|
254
|
+
# Layout the second nucleotide in this loop region
|
|
255
|
+
self.current_pos += intermediate_vec * (self.backbone_length + delta)
|
|
256
|
+
self.current_vec = self.current_vec.rotated(defl_angle)
|
|
257
|
+
self.nodes.append(Node(nucleotides[1], self.current_pos))
|
|
258
|
+
self.add_last_stem_backbone()
|
|
259
|
+
# Layout child stem region
|
|
260
|
+
self.layout_stem(child_stems[0])
|
|
261
|
+
# Layout the 4th nucleotide in this loop region
|
|
262
|
+
if not current_loop.is_root:
|
|
263
|
+
self.current_pos -= intermediate_vec * (self.backbone_length - delta)
|
|
264
|
+
self.current_vec = self.current_vec.rotated(-defl_angle)
|
|
265
|
+
self.nodes.append(Node(nucleotides[3], self.current_pos))
|
|
266
|
+
self.add_last_stem_backbone()
|
|
267
|
+
else:
|
|
268
|
+
delta_angle = 2*math.pi / len(current_loop.nucleotides)
|
|
269
|
+
radius = self.basepair_length/2 / math.sin(delta_angle/2)
|
|
270
|
+
self.current_vec = self.current_vec.rotated(delta_angle)
|
|
271
|
+
stem_map = {stem.nucleotides[0]: stem for stem in child_stems}
|
|
272
|
+
# Layout nucleotides except the first and stem merge nucleotides
|
|
273
|
+
for nt in [curr for prev, curr in zip(nucleotides, nucleotides[1:]) if prev not in stem_map]:
|
|
274
|
+
self.current_pos += self.current_vec * self.basepair_length
|
|
275
|
+
self.current_vec = self.current_vec.rotated(delta_angle)
|
|
276
|
+
self.nodes.append(Node(nt, self.current_pos))
|
|
277
|
+
self.add_last_loop_backbone(radius)
|
|
278
|
+
if nt.is_three_prime:
|
|
279
|
+
direction = self.current_vec.rotated(-delta_angle/2)
|
|
280
|
+
self.markers.append(ArrowMarker(self.nodes[-1], direction=direction))
|
|
281
|
+
if (stem := stem_map.pop(nt, None)) is not None:
|
|
282
|
+
self.current_vec = self.current_vec.rotated(-math.pi/2)
|
|
283
|
+
self.layout_stem(stem)
|
|
284
|
+
self.current_vec = self.current_vec.rotated(-math.pi/2+delta_angle)
|
|
285
|
+
self.current_vec = self.current_vec.normalized()
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
def layout(self, root_loop: LoopRegion) -> None:
|
|
289
|
+
"""Generate a complete layout starting from the root loop region.
|
|
290
|
+
|
|
291
|
+
Parameters
|
|
292
|
+
----------
|
|
293
|
+
root_loop : LoopRegion
|
|
294
|
+
Root loop region of the secondary structure tree.
|
|
295
|
+
"""
|
|
296
|
+
self.current_pos = Vec2(0, 0)
|
|
297
|
+
self.current_vec = Vec2(1, 0)
|
|
298
|
+
nucleotides = root_loop.nucleotides
|
|
299
|
+
delta_angle = 2*math.pi / len(nucleotides)
|
|
300
|
+
if root_loop.child_stems:
|
|
301
|
+
# Adjust the layout so that the first stem region extends upward
|
|
302
|
+
offset = nucleotides.index(root_loop.child_stems[0].nucleotides[0])
|
|
303
|
+
for _ in range(offset):
|
|
304
|
+
self.current_vec = self.current_vec.rotated(-delta_angle)
|
|
305
|
+
self.current_pos -= self.backbone_length * self.current_vec
|
|
306
|
+
if offset != 0:
|
|
307
|
+
self.current_vec = self.current_vec.rotated(-delta_angle)
|
|
308
|
+
self.nodes.append(Node(nucleotides[0], self.current_pos))
|
|
309
|
+
self.layout_loop(root_loop)
|
|
310
|
+
else:
|
|
311
|
+
# Layout for secondary structures without base pairs
|
|
312
|
+
self.nodes.append(Node(nucleotides[0], self.current_pos))
|
|
313
|
+
for nt in nucleotides[1:]:
|
|
314
|
+
self.current_pos += self.backbone_length * self.current_vec
|
|
315
|
+
self.nodes.append(Node(nt, self.current_pos))
|
|
316
|
+
self.edges.append(LineEdge(self.nodes[-2], self.nodes[-1], EdgeType.BACKBONE))
|
|
317
|
+
self.markers.append(ArrowMarker(self.nodes[-1], self.current_vec))
|
|
318
|
+
return LayoutResult(self.nodes, self.edges, self.markers)
|
|
319
|
+
|
|
320
|
+
def layout(root_loop):
|
|
321
|
+
return RadialLayoutEngine().layout(root_loop)
|