from IPython.display import display, HTML
from matplotlib import colors as mcolors, pyplot as plt
import numpy as np
from typing import List, Optional, Union, Any
import tempfile
# try to import the oxDNA_analysis_tools package
try:
from oxDNA_analysis_tools.UTILS.oxview import oxdna_conf
from oxDNA_analysis_tools.UTILS.RyeReader import describe, get_confs
oat_installed = True
except ModuleNotFoundError:
oat_installed = False
# own imports
from ..core import *
from ..motifs import *
from .motif_lib import *
# A dictionary to convert angles to dovetail values
ANGLES_DT_DICT = {
26: -6,
58: -5,
90: -4,
122: -3,
154: -2,
186: -1,
218: 0,
250: 1,
282: 2,
314: 3,
346: 4,
378: 5,
410: 6,
}
[docs]
def convert_angles_to_dt(angles_list: List[float]) -> List[int]:
"""
Convert a list of helix angles into corresponding dovetail values based
on a predefined mapping.
Parameters
----------
angles_list : list of float
List of helix angles in degrees. Angles will be wrapped modulo 360.
Returns
-------
list of int
Corresponding dovetail values for each angle in the input list.
"""
angles_sanitize = [ang % 360 for ang in angles_list]
# get the closest angle in the dict
dt_list = [
ANGLES_DT_DICT[min(ANGLES_DT_DICT, key=lambda x: abs(x - ang))]
for ang in angles_sanitize
]
return dt_list
[docs]
def simple_origami(
dt_list: List[int],
kl_columns: int = 1,
main_stem: Optional[Union[int, List[int], List[List[int]]]] = None,
left_stem_kl: Optional[Union[int, List[int], List[List[int]]]] = None,
stem_pos: Optional[Union[int, List[int]]] = None,
start: int = 0,
add_terminal_helix: bool = True,
end_helix_len: int = 8,
use_angles: bool = False,
add_start_end: bool = True,
align: str = "first",
) -> Origami:
"""
Construct an RNA origami object based on a sequence of dovetail values and
kissing loop parameters.
Parameters
----------
dt_list : list of int
List of dovetail values representing inter-helix connections.
kl_columns : int, optional
Number of kissing loop repeats in each helix (default is 1).
main_stem : int or list of int or list of list of int, optional
Length(s) of the main stem in each kissing loop.
Can be a single int, a list (same for all loops), or a matrix for per-loop
customization.
left_stem_kl : int or list of int or list of list of int, optional
Length(s) of the left stem for each kissing loop. Defaults to automatic
computation.
stem_pos : int or list of int, optional
Position(s) of the main stem insertion among helices. Default is 0 for all.
start : int, optional
Index of the main stem where origami building starts (default is 0).
add_terminal_helix : bool, default True
Whether to prepend and append helices with no dovetails.
end_helix_len : int, optional
Length of the stems at the ends of the helices (default is 8).
use_angles : bool, optional
If True, interpret `dt_list` as helix angles and convert them to dovetail
values (default is False).
add_start_end : bool, default True
Whether to add a start-end motif in the initial helix.
align : str, optional
Alignment method for the origami object (default is 'first').
Returns
-------
Origami
The assembled Origami structure.
"""
# initialize the origami structure
origami = Origami(align=align)
# convert angles to dovetail values if needed
if use_angles:
dt_list = convert_angles_to_dt(dt_list)
# add the start and end helix to the dovetail list
if add_terminal_helix:
dt_list = [0] + dt_list + [0]
### ADJUST THE MAIN STEM MATRIX
if main_stem is None: # set it to the minimum value for each KL
max_dt = max([abs(dt) for dt in dt_list], default=0)
main_stem = [[11 * ((max_dt + 17) // 11 + 1)] * kl_columns] * len(dt_list)
elif isinstance(main_stem, int):
main_stem = [
[main_stem for _ in range(kl_columns)] for _ in range(len(dt_list))
]
elif isinstance(main_stem, list) and all(isinstance(x, int) for x in main_stem):
main_stem = [main_stem for _ in range(len(dt_list))]
elif isinstance(main_stem, list) and all(
isinstance(x, (tuple, list)) for x in main_stem
):
if not all(len(x) == kl_columns for x in main_stem):
raise ValueError(
"The main_stem list should have the same length"
" as the kissing loops repeats"
)
else:
raise ValueError(
"The main_stem can be an int, a list of int or a" " matrix of int"
)
### ADJUST THE LEFT KL STEM MATRIX
if left_stem_kl is None:
left_stem_kl = [[None] * kl_columns for _ in range(len(dt_list))]
elif isinstance(left_stem_kl, int):
left_stem_kl = [
[left_stem_kl for _ in range(kl_columns)] for _ in range(len(dt_list))
]
elif isinstance(left_stem_kl, list) and all(
isinstance(x, int) for x in left_stem_kl
):
left_stem_kl = [[left_stem_kl[i]] * kl_columns for i in range(len(dt_list))]
elif isinstance(left_stem_kl, list) and all(
isinstance(x, (tuple, list)) for x in left_stem_kl
):
if not all(len(x) == kl_columns for x in left_stem_kl):
raise ValueError(
"The left_stem_kl list should have the same length "
"as the kissing loops repeats"
)
else:
raise ValueError(
"The left_stem_kl can be an int, a list of int or a " "matrix of int"
)
if stem_pos is None:
stem_pos = [0 for _ in range(kl_columns)]
elif isinstance(stem_pos, int):
stem_pos = [stem_pos for _ in range(kl_columns)]
### BUILD THE ORIGAMI STRUCTURE, helix by helix
for helix_in, dt in enumerate(dt_list):
# create the start of the stem
helix = [TetraLoop(), Stem(end_helix_len), Dovetail(dt)]
# add Kissing loops repeats to the helix
for kl_index in range(kl_columns):
# calculate the stem lengths
stem_len = main_stem[helix_in][kl_index]
left_stem = left_stem_kl[helix_in][kl_index]
if left_stem is None:
left_stem = (stem_len - 8 - abs(dt)) // 2
right_stem = (stem_len - 8 - abs(dt)) - left_stem
# this is a position where to add a continuous stem
if stem_pos[kl_index] == helix_in:
# add the start position in this stem
if kl_index == start and add_start_end:
half_l_stem = (stem_len - abs(dt)) // 2
half_r_stem = stem_len - abs(dt) - half_l_stem
helix += [
Stem(half_l_stem).shift((1, 0), extend=True),
start_end_stem(),
Stem(half_r_stem),
Dovetail(dt),
]
else:
stem_len = main_stem[helix_in][kl_index] - abs(dt)
helix += [Stem(stem_len).shift((6, 0), extend=True), Dovetail(dt)]
# normal kissing loop repeat
else:
helix += [
Stem(left_stem),
KissingDimer(),
Stem(right_stem),
Dovetail(dt),
]
# add the end of the helix
helix += [Stem(end_helix_len), TetraLoop(open_left=True)]
# add the helix to the origami
origami.append(helix, copy=False)
# remove the top cross from the dovetails of the first helix
for motif in origami[0]:
if isinstance(motif, Dovetail):
motif.up_cross = False
# remove the bottom cross from the dovetails of the last helix
for motif in origami[-1]:
if isinstance(motif, Dovetail):
motif.down_cross = False
# return the origami structure
return origami
[docs]
def ipython_display_3d(origami: Origami, **kwargs: Any) -> None:
"""
Display a 3D representation of an Origami structure within a J
upyter notebook using oxDNA.
Parameters
----------
origami : Origami
The Origami structure to visualize.
**kwargs : dict, optional
Additional keyword arguments passed to the `oxdna_conf`
visualization function.
Returns
-------
None
"""
if not oat_installed:
warnings.warn(
"The oxDNA_analysis_tools package is not installed, "
"the 3D display is not available."
)
return
# Create a temporary directory
with tempfile.TemporaryDirectory() as tmpdirname:
file_path = f"{tmpdirname}/origami"
origami.save_3d_model(file_path)
top_info, traj_info = describe(f"{file_path}.top", f"{file_path}.dat")
conf = get_confs(top_info, traj_info, 0, 1)[0]
oxdna_conf(top_info, conf, **kwargs)
[docs]
def ipython_display_txt(origami_text: str, max_height: str = "500") -> None:
"""
Render plain text (e.g., a textual representation of an origami object) as a
scrollable HTML block in Jupyter.
Parameters
----------
origami_text : str, Origami
The content to display in scrollable format.
max_height : str, optional
Maximum height of the scrollable box in pixels (default is '500').
Returns
-------
None
"""
# Convert your text to scrollable HTML
ori_txt = str(origami_text).replace("\n", "<br>")
scrollable_html = (
f'<div style="max-height: {max_height}px; white-space: nowrap;'
f"overflow-x: auto; overflow-y: scroll;"
f"font-family: monospace;"
f'border: 1px solid #ccc; padding: 10px;">'
f"{ori_txt.replace(' ', ' ')}"
"</div>"
)
display(HTML(scrollable_html))
[docs]
def log_norm(data, eps=1e-12):
"""
Normalize data using logarithmic normalization.
Parameters
----------
data : array-like
The input data to be normalized.
eps : float, optional
A small value to avoid log(0) issues (default is 1e-12).
Returns
-------
normalized_values : array-like
The log-normalized data.
"""
data = np.maximum(data, eps)
vmin = data.min()
vmax = data.max()
normalized_values = (np.log(data) - np.log(vmin)) / (np.log(vmax) - np.log(vmin))
return normalized_values
[docs]
def ipython_clickable_txt(
origami: Origami,
max_height: Union[str, int] = "500",
barriers: Optional[Any] = None,
gradient: Union[bool, str] = False,
font_size: int = 12,
overlay_data: dict = None,
norm_cmap_function: Optional[Any] = None,
) -> str:
"""
Generate an interactive, scrollable HTML view of a RNA origami structure
with clickable motifs that display their indexes in a JavaScript alert.
Parameters
----------
origami : Origami
An Origami object representing the RNA structure.
max_height : str or int, optional
The maximum height of the scrollable view in pixels. Default is '500'.
barriers : optional
Optional barrier data to overlay on the origami representation.
gradient : bool or str, optional
Whether to color motifs using a gradient.
If str, interpreted as colormap name from matplotlib.
Default is False.
font_size : int, optional
Font size of the text in pixels. Default is 12.
overlay_data: dict, optional
Dictinary of the overlay data. Default is None.
Format is {"Parameter name": [nt1_value, nt2_value, ...]}
If a gradient is given, it is based on this data.
norm_cmap_function: function, optional
Function to normalize the overlay data to generate the
colormap only (the data itself is not changed).
Only applied if overlay_data is not None.
Default is None.
Returns
-------
str
An HTML string rendered via IPython's display system.
Notes
-----
- This function uses inline CSS and JavaScript for visual styling and interaction.
- Clicking a motif will trigger a JavaScript alert showing its position.
"""
barriers_colors = {"▂": "#FFBA08", "▄": "#FFBA08", "█": "#D00000"}
high_color = "#D00000"
normal_color = "inherit"
motif = origami.assembled
# create a dictionary from positions to index
pos_to_index = {pos: ind for ind, pos in enumerate(motif.seq_positions)}
if barriers:
origami_lines = origami.barrier_repr(return_list=True)
else:
origami_str = str(origami)
origami_lines = origami_str.split("\n")
# create color gradient
if gradient:
cmap = plt.get_cmap(gradient)
if overlay_data is None:
tot_len = 0
for s in origami.strands:
tot_len += len(s.sequence)
for protein in s.coords.proteins:
tot_len += len(protein)
c_map = [mcolors.to_hex(cmap(i)) for i in np.linspace(0, 1, tot_len)]
else:
data = np.array(next(iter(overlay_data.values())))
if norm_cmap_function is not None:
data = norm_cmap_function(data)
c_map = [mcolors.to_hex(cmap(i)) for i in data]
# Prepare the string to add 5' and 3' symbols for the strands
motif_list = (
[[" "] * (motif.num_char + 2)]
+ [[" "] + [char for char in line] + [" "] for line in origami_lines]
+ [[" "] * (motif.num_char + 2)]
)
for s in motif: # Add the 5' and 3' symbols to the motif as 1 and 2
if not s.sequence:
continue
if (
s.sequence
and s[0] not in "35"
and motif_list[s.prec_pos[1] + 1][s.prec_pos[0] + 1] == " "
):
if s.directionality == "53":
motif_list[s.prec_pos[1] + 1][s.prec_pos[0] + 1] = "1"
else:
motif_list[s.prec_pos[1] + 1][s.prec_pos[0] + 1] = "2"
if (
s.sequence
and s[-1] not in "35"
and motif_list[s.next_pos[1] + 1][s.next_pos[0] + 1] == " "
):
if s.directionality == "53":
motif_list[s.next_pos[1] + 1][s.next_pos[0] + 1] = "2"
else:
motif_list[s.next_pos[1] + 1][s.next_pos[0] + 1] = "1"
origami_list = ["".join(line) for line in motif_list]
content = (
f"<div style='font-family: monospace;"
f"font-size: {font_size}px; "
"white-space: nowrap; "
"overflow-x: auto; "
"overflow-y: scroll; "
f"max-height: {max_height}px;'>"
)
span = '<span style="font-family: monospace; '
for y, line in enumerate(origami_list):
for x, char in enumerate(line):
ori_pos = (x - 1, y - 1)
color = normal_color
if barriers_colors and char in barriers_colors:
color = barriers_colors[char]
if char == " ":
content += span + 'line-height:1;"> </span>'
elif char == "1":
content += span + f'color: {high_color}; line-height:1;">5</span>'
elif char == "2":
content += span + f'color: {high_color}; line-height:1;">3</span>'
elif char in bp_symbols: # do not highlight the base pair in red
content += span + f'color: {color}; line-height:1;">{char}</span>'
elif ori_pos in origami.pos_index_map: # a motif symbol
sl = origami.pos_index_map[ori_pos]
index = pos_to_index.get(ori_pos)
msg_text = f"Line {sl[0]}, Motif {sl[1]}"
if index is not None:
if overlay_data is not None:
data_name = next(iter(overlay_data))
data_list = overlay_data[data_name]
msg_text = (
f"Base {index}, Line {sl[0]}, Motif {sl[1]}, "
f"{data_name} {data_list[index]}"
)
else:
msg_text = f"Base {index}, Line {sl[0]}, Motif {sl[1]}"
if gradient:
color = c_map[index]
content += (
f'<a style="text-decoration: none;'
"font-family: monospace; "
f"color: {color}; "
'line-height:1;" '
f'href="#/" '
f"""onclick="alert('{msg_text}')" """
f'id="{sl[0]},{sl[1]},{x - 1},{y - 1}">'
f"{char}"
"</a>"
)
else: # is a junction symbol
content += span + f'color: {color}; line-height:1;">{char}</span>'
content += "<br />"
content += "</div>"
return display(HTML(content))
[docs]
def template_2_helix():
"""
Generate the RNA origami template for a 2-helix structure.
Reference:
1. Krissanaprasit, A. et al. A functional RNA-origami as direct thrombin
inhibitor with fast-acting and specific single-molecule reversal agents in
vivo model. Molecular Therapy 32, 2286-2298 (2024).
Returns
-------
Origami
An Origami object representing the 2-helix RNA structure.
"""
import pyfurnace as pf
line_0 = [
pf.TetraLoop(),
pf.Stem(11),
pf.Dovetail(0, up_cross=False),
pf.Stem(11),
pf.start_end_stem(),
pf.Stem(11),
pf.Dovetail(0, up_cross=False),
pf.Stem(11),
pf.TetraLoop(open_left=True),
]
line_1 = [
pf.TetraLoop(),
pf.Stem(11),
pf.Dovetail(0, down_cross=False),
pf.Stem(7),
pf.KissingDimer(),
pf.Stem(7),
pf.Dovetail(0, down_cross=False),
pf.Stem(11),
pf.TetraLoop(open_left=True),
]
origami = pf.Origami([line_0, line_1], align="first")
return origami
[docs]
def template_rna_filament():
"""
Generate the RNA origami template for an RNA filament structure.
Reference:
1. Tran, M. P. et al. Genetic encoding and expression of RNA origami
cytoskeletons in synthetic cells. Nat. Nanotechnol. 20, 664-671 (2025).
Returns
-------
Origami
An Origami object representing the RNA filament structure.
"""
import pyfurnace as pf
origami = pf.simple_origami(
dt_list=[-3],
kl_columns=1,
main_stem=[22],
align="first",
)
# adjust middle helix terminal stem
origami[(1, 7)].length = 5
origami[(1, 1)].length = 5
### STEMS BEFORE THE EXTERNAL KL
origami[(0, 7)].length = 6
origami[(2, 7)].length = 6
origami[(2, 1)].length = 8
origami[(0, 1)].length = 7
origami[(0, 0)] = pf.KissingLoop180(open_left=False, pk_index="2")
origami[(-1, 0)] = pf.KissingLoop180(open_left=False, pk_index="1")
origami[(0, -1)] = pf.KissingLoop180(open_left=True, pk_index="1'")
origami[(-1, -1)] = pf.KissingLoop180(open_left=True, pk_index="2'")
return origami
[docs]
def template_rna_filament_ispinach():
"""
Generate the RNA origami template for an RNA filament structure.
The tiles contains an ispinach aptamer for fluorescence imaging.
Reference:
1. Tran, M. P. et al. Genetic encoding and expression of RNA origami
cytoskeletons in synthetic cells. Nat. Nanotechnol. 20, 664-671 (2025).
Returns
-------
Origami
An Origami object representing the RNA filament structure.
"""
import pyfurnace as pf
origami = pf.simple_origami(
dt_list=[-3],
kl_columns=1,
main_stem=[22],
align="first",
)
# adjust middle helix terminal stem
origami[(1, 7)].length = 5
origami[(1, 1)].length = 5
### STEMS BEFORE THE EXTERNAL KL
origami[(0, 7)].length = 6
origami[(2, 7)].length = 6
origami[(2, 1)].length = 8
origami[(0, 1)].length = 7
origami[(0, 0)] = pf.KissingLoop180(open_left=False, pk_index="2")
origami[(-1, 0)] = pf.KissingLoop180(open_left=False, pk_index="1")
origami[(0, -1)] = pf.KissingLoop180(open_left=True, pk_index="1'")
origami[(-1, -1)] = pf.KissingLoop180(open_left=True, pk_index="2'")
# add connector for ispinach line
origami[(0, 4)] = pf.Dovetail(0, down_cross=False)
# add ispinach line
ispi_line = [
pf.TetraLoop(),
pf.Stem(3, strong_bases=False),
pf.Ispinach().flip(),
pf.Stem(3),
pf.Motif.from_structure("...&", "UUU&").flip(),
pf.start_end_stem(), # make sure the 3d model connects at the ss region
pf.stem_cap_link(hflip=True),
]
origami.insert(0, ispi_line)
return origami
[docs]
def template_3_arms_droplet():
"""
Generate the RNA origami template for a 3-arms droplet structure.
References:
1. Stewart, J. M. et al. Modular RNA motifs for orthogonal phase separated
compartments. Nat Commun 15, (2024).
2. Fabrini, G. et al. Co-transcriptional production of programmable RNA condensates
and synthetic organelles. Nat. Nanotechnol. 19, 1665-1673 (2024).
3. Monari, L., Braun, I., Poppleton, E. & Göpfrich, K. PyFuRNAce: An integrated
design engine for RNA origami. (2025) doi:10.1101/2025.04.17.647389.
Returns
-------
Origami
An Origami object representing the 3-arms droplet structure.
"""
import pyfurnace as pf
palindr_kl = pf.KissingLoop(sequence="AUCGCGAAA")
line_0 = [
palindr_kl.copy(),
pf.Stem(8),
pf.start_end_stem(),
pf.Stem(17),
pf.Motif.from_structure(".&", "U&").flip(),
pf.Dovetail(0, up_cross=False),
pf.Motif.from_structure(".&.", "U&U"),
pf.Stem(25),
palindr_kl.copy().flip(),
]
line_1 = [
palindr_kl.copy(),
pf.Stem(5),
pf.Broccoli(),
pf.Stem(8),
pf.stem_cap_link().flip(),
]
origami = pf.Origami([line_0, line_1], align="first")
return origami
[docs]
def template_4_arms_droplet():
"""
Generate the RNA origami template for a 4-arms droplet structure.
Reference:
1. Fabrini, G. et al. Co-transcriptional production of programmable RNA condensates
and synthetic organelles. Nat. Nanotechnol. 19, 1665-1673 (2024).
Returns
-------
Origami
An Origami object representing the 4-arms droplet structure.
"""
import pyfurnace as pf
uracil_connect = pf.Motif.from_structure(".&.", "U&U")
palindr_kl = pf.KissingLoop(sequence="AUCGCGAAA")
line_0 = [
palindr_kl.copy(),
pf.Stem(10),
pf.MalachiteGreenShort(),
pf.Stem(10),
uracil_connect.copy(),
pf.Dovetail(0, up_cross=False),
pf.Stem(25),
palindr_kl.copy().flip(),
]
line_1 = [
palindr_kl.copy(),
pf.Stem(13),
pf.start_end_stem(),
pf.Stem(12),
pf.Dovetail(0, down_cross=False),
uracil_connect.copy(),
pf.Stem(25),
palindr_kl.copy().flip(),
]
origami = pf.Origami([line_0, line_1], align="first")
return origami
[docs]
def template_pentagon_tile():
"""
Generate the RNA origami template for a pentagon tile structure.
This follows the 3H-4DT design from Geary et al. (2021).
Reference:
1. Geary, C., Grossi, G., McRae, E. K. S., Rothemund, P. W. K. & Andersen, E. S.
RNA origami design tools enable cotranscriptional folding of kilobase-sized
nanoscaffolds. Nat. Chem. 13, 549-558 (2021).
Returns
-------
Origami
An Origami object representing the pentagon tile structure.
"""
import pyfurnace as pf
origami = pf.simple_origami([-4], main_stem=33, end_helix_len=7)
# adjust start position
origami[0, 3].length = 12
origami[0, 5].length = 21
# adjust stems before the KL
origami[0, 1].length = 5
origami[-1, 1].length = 17
origami[0, -2].length = 12
origami[-1, -2].length = 21
# put the KL in the right place
origami[0, 0] = pf.KissingLoop120(pk_index=1)
origami[0, -1] = pf.KissingLoop120(open_left=True, pk_index=-1)
origami[-1, 0] = pf.KissingLoop120(pk_index=2)
origami[-1, -1] = pf.KissingLoop120(open_left=True, pk_index=-2)
return origami
[docs]
def template_rectangle_10H_3X():
"""
Generate the RNA origami template for a rectangle 10H-3X structure.
References:
1. Monari, L., Braun, I., Poppleton, E. & Göpfrich, K. PyFuRNAce: An integrated
design engine for RNA origami. (2025) doi:10.1101/2025.04.17.647389.
Returns
-------
Origami
An Origami object representing the rectangle 10H-3X structure.
"""
import pyfurnace as pf
origami = pf.simple_origami(
dt_list=[180] * 8,
kl_columns=3,
main_stem=33,
add_terminal_helix=True,
align="first",
use_angles=True,
) # Create a simple origami
# If the improved folding pathway is not known, use the function to find it
# origami = origami.improve_folding_pathway(kl_delay=150)
# Known good folding pathway for this origami:
origami.pop(origami.index(lambda m: "5" in m)[0])
origami[1, 5] = [pf.Stem(6), pf.start_end_stem(), pf.Stem(6)]
origami.insert((0, 11), pf.Broccoli().flip(1, 1)) # Add motif
origami.insert((0, 12), pf.Stem(5)) # Add motif
return origami