from typing import Optional, List
import numpy as np
from ..core.symbols import nucl_to_pair
from ..core.coordinates_3d import Coords
from ..core.sequence import Sequence
from ..core.strand import Strand
from .stem import Stem
### transfromantion for AE crossover
### calculated from PDB-7QDU; mean crossover roughly refined
# DAE_T_53 = np.array([[-0.79306138, -0.05738916, -0.60643229, -0.60152068],
# [-0.05738916, -0.98408458, 0.16817856, -0.26217635],
# [-0.60643229, 0.16817856, 0.77714596, -0.83494375],
# [ 0. , 0. , 0. , 1. ],])
# DAE_T_35[[-0.79306138, -0.05738916, -0.60643229, -0.99842575],
# [-0.05738916, -0.98408458, 0.16817856, -0.15210483],
# [-0.60643229, 0.16817856, 0.77714596, 0.32818404],
# [ 0. , 0. , 0. , 1. ],])
### transfromantion for AE crossover currently used
### modeled from oxRNA RNA-A helix
# DAE_T_53 = np.array([[ 0.70838381, -0.47377676, -0.52319017, -1.56379685],
# [-0.47377676, -0.86861008, 0.14509347, -0.27700864],
# [-0.52319017, 0.14509347, -0.83977374, 0.15200037],
# [ 0. , 0. , 0. , 1. ]])
# DAE_T_35 = np.array([[ 0.70838381, -0.47377676, -0.52319017, 1.05605322],
# [-0.47377676, -0.86861008, 0.14509347, -1.00355736],
# [-0.52319017, 0.14509347, -0.83977374, -0.65032507],
# [ 0. , 0. , 0. , 1. ],])
[docs]
class Dovetail(Stem):
"""
Represents a double helix RNA stem with junction crossovers before and after
the stem.
Parameters
----------
length : int, optional
Number of base pairs in the stem. The sign determines dovetail direction.
Ignored if `sequence` is provided.
sequence : str, optional
RNA sequence to assign to the top strand. Overrides `length`.
up_cross : bool, optional
If True, includes a top dovetail crossover motif. Default is True.
down_cross : bool, optional
If True, includes a bottom dovetail crossover motif. Default is True.
sign : int, optional
Direction of the dovetail: +1 (positive/right) or -1 (negative/left).
If 0 or unspecified, inferred from `length` or `sequence`. Default is 1.
wobble_interval : int, optional
Periodicity of wobble base pairs in the stem. Default is 5.
wobble_tolerance : int, optional
Allowed deviation in wobble periodicity. Default is 2.
wobble_insert : str, optional
Position of wobble insertions. Must be "start", "middle", or "end".
Default is "middle".
strong_bases : bool, optional
Whether to enforce GC-rich base pairs. If None, it defaults to True only
when both `up_cross` and `down_cross` are True.
**kwargs : dict
Additional keyword arguments passed to the parent `Stem` class.
Attributes
----------
up_cross : bool
Whether the stem includes a top dovetail crossover motif.
down_cross : bool
Whether the stem includes a bottom dovetail crossover motif.
sign : int
Direction of the dovetail: +1 (right/positive), -1 (left/negative).
length : int
Effective number of nucleotides in the stem, signed based on orientation.
sequence : str
Nucleotide sequence of the top strand, if provided.
"""
def __init__(
self,
length: int = 0,
sequence: str = "",
up_cross: bool = True,
down_cross: bool = True,
sign: int = 1,
wobble_interval: int = 5,
wobble_tolerance: int = 2,
wobble_insert: str = "middle",
strong_bases: Optional[bool] = None,
**kwargs,
) -> None:
"""
Initialize a Dovetail motif, which is a helical stem with configurable
entry/exit junctions.
Parameters
----------
length : int, default=0
Number of nucleotides in the stem. Sign determines dovetail direction
sequence : str, default=''
Sequence of the stem. Overrides length if provided.
up_cross : bool, default=True
Whether to include a top crossover motif.
down_cross : bool, default=True
Whether to include a bottom crossover motif.
sign : int, default=1
Direction of dovetail: +1 (right), -1 (left).
wobble_interval : int, default=5
Interval between wobble base pairs (default is 5).
wobble_tolerance : int, default=2
Random deviation for wobble interval (default is 2).
wobble_insert : str, default="middle"
Position of wobble insertion: "middle", "start", or "end".
strong_bases : bool, optional
If True, use strong base pairing; if None strong bases are set only if
`up_cross` and `down_cross` are both True.
**kwargs : dict
Additional keyword arguments passed to `Stem`.
Raises
------
ValueError
If `length` is not an integer.
"""
if not isinstance(length, int):
raise ValueError("The length parameter must be an integer.")
# initialize the attributes
self._up_cross = bool(up_cross)
self._down_cross = bool(down_cross)
if sequence:
if sign < 0:
self._sign = -1
else:
self._sign = +1
else:
self._sign = +1 if length >= 0 else -1
kwargs["join"] = False
super().__init__(
length=length,
sequence=sequence,
wobble_interval=wobble_interval,
wobble_tolerance=wobble_tolerance,
wobble_insert=wobble_insert,
strong_bases=strong_bases,
**kwargs,
)
###
### PROPERTIES
###
@property
def up_cross(self):
"""Returns boolian describing wether the dovetail has a top crossing"""
return self._up_cross
@up_cross.setter
def up_cross(self, new_bool):
"""Set boolian describing wether the dovetail has a top crossing"""
self._up_cross = bool(new_bool)
self.length = self._length
@property
def down_cross(self):
"""Returns boolian describing wether the dovetail has a bottom crossing"""
return self._down_cross
@down_cross.setter
def down_cross(self, new_bool):
"""Set boolian describing wether the dovetail has a bottom crossing"""
self._down_cross = bool(new_bool)
self.length = self._length
[docs]
def set_up_sequence(self, sequence, sign=0):
"""Set the sequence of the top strand"""
if not isinstance(sequence, (str, Sequence)):
raise TypeError(f"The sequence of a stem must be a string, got {sequence}.")
if sign not in [-1, 0, 1]:
raise ValueError(
f"The sign of the dovetail must be -1, 0 or 1, got {sign}."
)
self._sign = sign
if not sign:
if self._length >= 0:
self._sign = +1
else:
self._sign = -1
self._length = len(sequence) * self._sign
self._create_strands(sequence=sequence)
[docs]
def set_down_sequence(self, sequence, sign=None):
"""Set the sequence of the bottom strand"""
self.set_up_sequence(sequence=sequence.translate(nucl_to_pair)[::-1], sign=sign)
###
### Protected METHODS
###
def _create_strands(
self,
sequence: Optional[str] = None,
length: int = 0,
return_strands: bool = False,
**kwargs,
) -> Optional[List[Strand]]:
"""
Internal method to generate top and bottom strands with dovetail crossover
features.
Parameters
----------
sequence : str, optional
Sequence to assign to the top strand. If None, generated based on length.
length : int, optional
Length of the stem (used if `sequence` is None).
return_strands : bool, optional
If True, return the strands instead of assigning them (default is False).
**kwargs : dict
Additional arguments for strand creation (e.g., `strong_bases`).
Returns
-------
list of Strand or None
The created strands if `return_strands` is True, otherwise None.
"""
# select the direction of the dovetail
if sequence:
pos = True if self._sign >= 0 else False
seq_len = len(sequence)
else:
pos = True if length >= 0 else False
seq_len = abs(length)
self._sign = +1 if pos else -1
up_cross = self._up_cross
down_cross = self._down_cross
if kwargs.get("strong_bases") is None:
kwargs["strong_bases"] = up_cross and down_cross
### Create stem strands
top_strand, bot_strand = super()._create_strands(
sequence=sequence,
length=length,
compute_coords=False,
return_strands=True,
**kwargs,
)
### Positive dovetail
if pos:
### Top strands
top_strand.strand = (
"──" + top_strand.strand + "╯" * up_cross + "─" * (not up_cross)
)
top_strand1 = top_strand
top_strand2 = Strand(
"╰" * up_cross + "─" * (not up_cross),
start=(top_strand1.end[0] + 1, 0),
direction=(int(not up_cross), int(up_cross)),
)
### Bottom strands
bot_strand1 = Strand(
"╮" * down_cross + "─" * (not down_cross),
start=(0, 2),
direction=(-int(not down_cross), -int(down_cross)),
)
# adjust the stem start position, strand and direction
bot_strand.strand = (
"──" + bot_strand.strand + "╭" * down_cross + "─" * (not down_cross)
)
bot_strand.start = (bot_strand.start[0] + 4, 2)
bot_strand.direction = (-1, 0)
bot_strand2 = bot_strand
### Negative dovetail
else:
### Top strands
top_strand1 = Strand(
"╯" * up_cross + "─" * (not up_cross), start=(0, 0), direction=(1, 0)
)
# adjust the stem start position, strand and direction
top_strand.strand = (
"╰" * up_cross + "─" * (not up_cross) + top_strand.strand + "──"
)
top_strand.start = (1, 0)
top_strand.direction = (int(not up_cross), int(up_cross))
top_strand2 = top_strand
### Bottom strands
bot_strand.strand = (
"╮" * down_cross + "─" * (not down_cross) + bot_strand.strand + "──"
)
bot_strand.start = (bot_strand.start[0] + 3, 2)
bot_strand.direction = (-int(not down_cross), -int(down_cross))
bot_strand1 = bot_strand
bot_strand2 = Strand(
"─" * (not down_cross) + "╭" * down_cross,
start=(bot_strand1.start[0] + 1, 2),
direction=(-1, 0),
)
### set up the coordinates (helix length + dummy ends)
coords = Coords.compute_helix_from_nucl(
(0, 0, 0), # start position
(1, 0, 0), # base vector
(0, 1, 0), # normal vector
length=seq_len + 2,
double=True,
)
# leave out the first and last nucleotide to add the dummy ends
# Here a schematic of the coordinates indexes:
# top_strand1; top_strand2
# | |
# 0; seq_len; seq_len + 1;
# | | |
# -N--NNNNNNN--N->
# : ::::::: :
# <-N--NNNNNNN--N-
# | |
# seq_len * 2 + 3; seq_len + 2
# | |
# bot_strand1; bot_strand2
DAE_T_53, DAE_T_35 = Coords.compute_AE_crossover()
### the dovetail is positive
if pos:
# top strand 1
top_coord1 = Coords(coords[1 : seq_len + 1])
if up_cross:
top_coord1.dummy_ends = (
coords[0], # necessary dummy for 0 DT
np.array(
Coords.apply_transformation(
DAE_T_53,
coords[seq_len][0],
coords[seq_len][1],
coords[seq_len][2],
local=True,
)
),
)
# top strand 2
top_coord2 = Coords(np.array(()))
if up_cross:
top_coord2.dummy_ends = (
np.array(
Coords.apply_transformation(
DAE_T_35,
coords[seq_len + 1][0],
coords[seq_len + 1][1],
coords[seq_len + 1][2],
local=True,
)
),
coords[seq_len + 1],
)
# bot strand 1
bot_coord1 = Coords(np.array(()))
if down_cross:
bot_coord1.dummy_ends = (
np.array(
Coords.apply_transformation(
DAE_T_35,
coords[-1][0],
coords[-1][1],
coords[-1][2],
local=True,
)
),
coords[-1],
)
# bot strand 2
bot_coord2 = Coords(coords[seq_len + 3 : seq_len * 2 + 3])
if down_cross:
bot_coord2.dummy_ends = (
coords[seq_len + 2], # necessary dummy for 0 DT
np.array(
Coords.apply_transformation(
DAE_T_53,
coords[-2][0],
coords[-2][1],
coords[-2][2],
local=True,
)
),
)
### the dovetail is negative
else:
# top strand 1
top_coord1 = Coords(np.array(()))
if up_cross:
top_coord1.dummy_ends = (
coords[0],
np.array(
Coords.apply_transformation(
DAE_T_53,
coords[0][0],
coords[0][1],
coords[0][2],
local=True,
)
),
)
# top strand 2
top_coord2 = Coords(coords[1 : seq_len + 1])
if up_cross:
top_coord2.dummy_ends = (
np.array(
Coords.apply_transformation(
DAE_T_35,
coords[1][0],
coords[1][1],
coords[1][2],
local=True,
)
),
coords[seq_len + 1], # coords[seq_len + 1], useful for ss_assembly
)
# bot strand 1
bot_coord1 = Coords(coords[seq_len + 3 : -1])
if down_cross:
bot_coord1.dummy_ends = (
np.array(
Coords.apply_transformation(
DAE_T_35,
coords[seq_len + 3][0],
coords[seq_len + 3][1],
coords[seq_len + 3][2],
local=True,
)
),
coords[-1], # coords[-1], useful for Origami ss_assembly
)
# bot strand 2
bot_coord2 = Coords(np.array(()))
if down_cross:
bot_coord2.dummy_ends = (
np.array(coords[seq_len + 2]),
np.array(
Coords.apply_transformation(
DAE_T_53,
coords[seq_len + 2][0],
coords[seq_len + 2][1],
coords[seq_len + 2][2],
local=True,
)
),
)
top_strand1._coords = top_coord1
top_strand2._coords = top_coord2
bot_strand2._coords = bot_coord2
bot_strand1._coords = bot_coord1
if return_strands:
return self.join_strands(
[top_strand1, top_strand2, bot_strand1, bot_strand2]
)
self.replace_all_strands(
[top_strand1, top_strand2, bot_strand1, bot_strand2], copy=False, join=True
)