nuc2d 0.1.2__tar.gz

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-0.1.2/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Soma Ishii
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
nuc2d-0.1.2/PKG-INFO ADDED
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.4
2
+ Name: nuc2d
3
+ Version: 0.1.2
4
+ Summary: A Python library for parsing, annotating, laying out, and rendering nucleic acid secondary structures.
5
+ Author: Soma Ishii
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Soma-yu/nuc2d
8
+ Project-URL: Repository, https://github.com/Soma-yu/nuc2d
9
+ Keywords: nucleic acid,RNA,DNA,secondary structure,visualization
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: numpy
14
+ Requires-Dist: matplotlib
15
+ Requires-Dist: svgwrite
16
+ Dynamic: license-file
17
+
18
+ # nuc2d
19
+
20
+ `nuc2d` is a Python library for parsing, annotating, laying out, and rendering nucleic acid secondary structures.
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ pip install nuc2d
26
+ ```
27
+
28
+ ## Example
29
+
30
+ ```python
31
+ from nuc2d import draw_svg
32
+
33
+ dpp_string = "(((..+...)))"
34
+
35
+ svg = draw_svg(
36
+ dpp_string=dpp_string,
37
+ )
38
+
39
+ svg.saveas("output.svg")
40
+ ```
41
+
42
+ The `dpp_string` argument should be specified in dot-parens-plus notation.
43
+
44
+ If you are using Jupyter Notebook or JupyterLab, you can also display
45
+ the generated SVG directly:
46
+
47
+ ```python
48
+ from IPython.display import SVG, display
49
+
50
+ display(SVG(svg.tostring()))
51
+ ```
52
+
53
+ Optional sequence and base-pair probability annotations can be provided
54
+ through the `sequences` and `probs` arguments:
55
+
56
+ ```python
57
+ svg = draw_svg(
58
+ dpp_string=dpp_string,
59
+ sequences=sequences,
60
+ probs=probs,
61
+ )
62
+ ```
63
+
64
+ ## License
65
+
66
+ This project is licensed under the MIT License.
nuc2d-0.1.2/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # nuc2d
2
+
3
+ `nuc2d` is a Python library for parsing, annotating, laying out, and rendering nucleic acid secondary structures.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install nuc2d
9
+ ```
10
+
11
+ ## Example
12
+
13
+ ```python
14
+ from nuc2d import draw_svg
15
+
16
+ dpp_string = "(((..+...)))"
17
+
18
+ svg = draw_svg(
19
+ dpp_string=dpp_string,
20
+ )
21
+
22
+ svg.saveas("output.svg")
23
+ ```
24
+
25
+ The `dpp_string` argument should be specified in dot-parens-plus notation.
26
+
27
+ If you are using Jupyter Notebook or JupyterLab, you can also display
28
+ the generated SVG directly:
29
+
30
+ ```python
31
+ from IPython.display import SVG, display
32
+
33
+ display(SVG(svg.tostring()))
34
+ ```
35
+
36
+ Optional sequence and base-pair probability annotations can be provided
37
+ through the `sequences` and `probs` arguments:
38
+
39
+ ```python
40
+ svg = draw_svg(
41
+ dpp_string=dpp_string,
42
+ sequences=sequences,
43
+ probs=probs,
44
+ )
45
+ ```
46
+
47
+ ## License
48
+
49
+ This project is licensed under the MIT License.
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "nuc2d"
7
+ version = "0.1.2"
8
+ description = "A Python library for parsing, annotating, laying out, and rendering nucleic acid secondary structures."
9
+ authors = [
10
+ { name = "Soma Ishii" }
11
+ ]
12
+ requires-python = ">=3.10"
13
+
14
+ license = "MIT"
15
+ readme = "README.md"
16
+
17
+ keywords = [
18
+ "nucleic acid",
19
+ "RNA",
20
+ "DNA",
21
+ "secondary structure",
22
+ "visualization",
23
+ ]
24
+
25
+ dependencies = [
26
+ "numpy",
27
+ "matplotlib",
28
+ "svgwrite",
29
+ ]
30
+
31
+ [tool.setuptools.packages.find]
32
+ where = ["src"]
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/Soma-yu/nuc2d"
36
+ Repository = "https://github.com/Soma-yu/nuc2d"
nuc2d-0.1.2/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ from .draw import draw_svg
2
+
3
+ __all__ = [
4
+ "draw_svg",
5
+ ]
@@ -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)
@@ -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
@@ -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)