from __future__ import annotations
import copy
import uuid
from abc import ABC, ABCMeta, abstractmethod
from dataclasses import dataclass
from enum import Enum
from typing import (
Any,
Callable,
ClassVar,
Collection,
Generator,
Generic,
Iterable,
Iterator,
List,
Literal,
MutableMapping,
Optional,
Sequence,
Tuple,
Type,
TypeVar,
Union,
cast,
overload,
TYPE_CHECKING,
)
from xgrow.xcolors import xcolors
from . import drawing
if TYPE_CHECKING:
from alhambra.tilesets import XgrowGlueOpts, TileSet
import xgrow.tileset as xgt
from .glues import Use
try:
import scadnano
except ImportError:
pass
from .classes import UpdateListD
from .glues import DXGlue, Glue, GlueFactory, GlueList, SSGlue
from .seq import Seq
import logging
[docs]log = logging.getLogger("alhambra")
[docs]class D(Enum):
"""Cardinal directions; also edge directions for single tiles."""
@property
[docs] def complement(self) -> D:
return D((self.value + 2) % 4)
[docs]Offset = Tuple[int, int]
@dataclass(frozen=True)
[docs]class EdgeView:
"""A class to ensure that tile edge glue manipulations are handled through the tile."""
[docs] __slots__ = ("_edges", "_tile")
def __init__(self, _edges: List[Glue], _tile: Tile):
self._edges = _edges
self._tile = _tile
@overload
[docs] def __getitem__(self, k: int | str) -> Glue:
...
@overload
def __getitem__(self, k: slice) -> list[Glue]:
...
def __getitem__(self, k: int | slice | str) -> Glue | list[Glue]:
if isinstance(k, str):
k = self._tile._get_edge_index(k)
return self._edges.__getitem__(k)
[docs] def __setitem__(self, k: int | str, v: Glue) -> None:
if isinstance(k, str):
k = self._tile._get_edge_index(k)
self._tile.set_edge(k, v)
[docs] def insert(self, index: int, value: Glue) -> None:
return self._edges.insert(index, value)
[docs] def __delitem__(self, k: int | str) -> None:
raise NotImplementedError
[docs] def __iter__(self) -> Iterator[Glue]:
return iter(self._edges)
[docs] def __next__(self) -> Generator[Glue, None, None]:
for x in self._edges:
yield x
raise StopIteration
[docs] def __len__(self) -> int:
return self._edges.__len__()
[docs] def __repr__(self) -> str:
return self._edges.__repr__()
[docs] def __str__(self) -> str:
return self._edges.__str__()
[docs]class UseView(MutableMapping[int, Use]):
"""A class to ensure that tile edge use manipulations are handled through the tile."""
def __init__(self, _tile: Tile):
self._tile = _tile
@overload
[docs] def __getitem__(self, k: int) -> Use:
...
@overload
def __getitem__(self, k: slice) -> list[Use]:
...
def __getitem__(self, k: int | slice) -> Use | list[Use]:
x = self._tile._edges[k]
if isinstance(x, Iterable):
return [g.use for g in x]
else:
return x.use
[docs] def __setitem__(self, k: int, v: Use) -> None:
self._tile._edges[k].use = v
[docs] def insert(self, index: int, value: Use) -> None:
raise NotImplementedError
[docs] def __len__(self) -> int:
return self._tile._edges.__len__()
[docs] def __repr__(self) -> str:
return repr([g.use for g in self._tile._edges])
[docs] def __str__(self) -> str:
return str([g.use for g in self._tile._edges])
@dataclass(init=False)
[docs]class Tile:
"""Base class for a tile."""
[docs] note: Optional[str | dict[str, Any]]
[docs] uses: List[List[Use]] # FIXME: should use list of tuples
[docs] __slots__ = ("name", "_edges", "color", "stoic", "note", "fake", "uses")
[docs] def get_concentration(self, base_concentration: float) -> float:
if self.stoic is None:
return base_concentration
else:
return base_concentration * self.stoic
[docs] def get_stoic(self, base_concentration: float) -> float:
"""Returns the stoichiometric ratio of this tile to the base concentration."""
if self.stoic is not None:
return self.stoic
else:
return 1.0
def __init__(
self,
edges: Optional[Iterable[Glue | str]] = None,
name: Optional[str] = None,
color: Optional[Color] = None,
stoic: Optional[float] = None,
note: Optional[str | dict[str, Any]] = None,
use: Sequence[Use | int | str] | None = None,
fake: bool = False,
uses: Sequence[Sequence[Use | int | str]] | None = None,
) -> None:
if edges is None:
raise ValueError
self._edges = [(g if isinstance(g, Glue) else Glue(g)) for g in edges]
self.uses = []
if use is not None:
self.uses += [[Use.from_any(x) for x in use]]
if uses is not None:
self.uses += [[Use.from_any(x) for x in u] for u in uses]
self.name = name
self.color = color
self.stoic = stoic
self.note = note
self.fake = fake
@property
[docs] def structure(self):
return self.__class__.__name__
@property
[docs] def use(self) -> Sequence[Use]:
if self.uses:
# FIXME: combine uses here
return self.uses[0]
else:
return [g.use for g in self._edges]
@use.setter
def use(self, uses: Iterable[Use]) -> None:
# Fixme: make this actually set tile uses as well
for glue, use in zip(self._edges, uses):
glue.use = use
@property
[docs] def edges(self) -> EdgeView:
return EdgeView(self._edges, self)
@edges.setter
def edges(self, e: Iterable[Glue | str]) -> None:
self._edges = [g if isinstance(g, Glue) else Glue(g) for g in e]
@property
[docs] def edge_directions(self) -> List[D]:
raise NotImplementedError
@property
[docs] def edge_locations(self) -> List[EdgeLoc]:
raise NotImplementedError
[docs] def set_edge(self, i: int, glue: Glue) -> None:
self._edges[i] = glue
[docs] def copy(self: T) -> T:
return copy.deepcopy(self)
@property
[docs] def rotations(self) -> List[Tile]:
raise NotImplementedError
@classmethod
[docs] def _get_edge_index(cls, v: str) -> int:
raise NotImplementedError
[docs] def ident(self) -> str:
if self.name:
return self.name
else:
raise ValueError
@property
[docs] def is_fake(self) -> bool:
return self.fake
[docs] def merge(self, other) -> Tile:
if self == other:
return self
raise NotImplementedError
@property
[docs] def xgname(self) -> str:
if self.name:
return self.name
if self._edges:
return f"{self.__class__.__name__}_" + "_".join(
f"{g.ident()}" for g in self._edges
)
else:
raise ValueError
[docs] def to_dict(self, refglues: set[str] = set()) -> dict[str, Any]:
b = {
k: v
for k in ["name", "edges", "color", "stoic", "note", "uses"]
if (v := getattr(self, k)) is not None
}
if self.edges is not None:
b["edges"] = [
(x.name if x.name in refglues else x.to_dict()) for x in self.edges
]
# fixme: deal with None
return b
[docs] def update_glues(self, gluedict: GlueList[Glue]) -> None:
if self.edges is not None:
self._edges = [gluedict.merge_glue(g) for g in self.edges]
[docs] def update_glues_and_list(self, gluedict: GlueList[Glue]) -> None:
if self.edges is not None:
self._edges = [gluedict.merge_glue_and_update_list(g) for g in self.edges]
@staticmethod
[docs] def from_dict(d: dict[str, Any]) -> Tile:
# FIXME: legacy conversion, changs input
if "ends" in d:
assert "edges" not in d
d["edges"] = d["ends"]
del d["ends"]
if "extra" in d:
if "type" in d:
d["type"] += "_" + d.pop("extra")
else:
raise ValueError
if "uses" in d:
d["uses"] = [
tuple(Use(x) if not isinstance(x, Use) else x for x in y)
for y in d["uses"]
]
return tile_factory.from_dict(d)
[docs] def to_xgrow(self, gluenamemap: Callable[[str], str] = lambda x: x) -> xgt.Tile:
import xgrow.tileset as xgt
edges = [gluenamemap(g.ident()) for g in self._edges]
return xgt.Tile(
cast(list[Union[str, int]], edges),
name=self.xgname,
stoic=self.stoic,
color=self.color,
)
[docs] def abstract_diagram(
self,
tileset: TileSet | None = None,
draw_names: bool = True,
draw_glues: bool = True,
) -> drawing.Group:
raise NotImplementedError
[docs]class TileSupportingScadnano(ABC, Tile):
"""Abstract base class for a tile that allows export to scadnano."""
@property
@abstractmethod
[docs] def _scadnano_5p_offset(self) -> tuple[int, int]:
...
@abstractmethod
[docs] def to_scadnano(
self, design: scadnano.Design, helix: int, offset: int
) -> scadnano.Strand:
...
@abstractmethod
def __init__(
self,
edges: Optional[Iterable[Glue | str]] = None,
name: Optional[str] = None,
color: Optional[Color] = None,
stoic: Optional[float] = None,
note: Optional[str] = None,
domains: Optional[Iterable[SSGlue]] = None,
) -> None:
...
[docs]class SingleTile(Tile):
"""A tile with N, E, S and W edges."""
@property
[docs] def edge_directions(self) -> List[D]:
return [D.N, D.E, D.S, D.W]
@property
[docs] def edge_locations(self) -> List[EdgeLoc]:
return [
EdgeLoc(D.N, (0, 0)),
EdgeLoc(D.E, (0, 0)),
EdgeLoc(D.S, (0, 0)),
EdgeLoc(D.W, (0, 0)),
]
@classmethod
[docs] def _get_edge_index(cls, v: str) -> int:
return "NESW".index(v)
[docs] def abstract_diagram(
self,
tileset: TileSet | None = None,
draw_names: bool = True,
draw_glues: bool = True,
) -> drawing.Group:
if (self.color is not None) and (self.color in xcolors):
color = xcolors[self.color]
elif (self.color is not None) and (self.color[0] == "#"):
color = self.color
else:
color = "gray"
g = drawing.Group(id=uuid.uuid4().hex)
g.elements.append(drawing.Rectangle(0, 0, 10, 10, fill=color, stroke="black"))
if draw_glues:
gluetext_locs = [(5, 1, 0), (9, 5, 90), (5, 9, 0), (1, 5, -90)]
for loc, glue in zip(gluetext_locs, self.edges):
g.elements.append(
drawing.Text(
glue.ident(),
0.8,
loc[0],
loc[1],
text_anchor="middle",
transform=f"rotate({loc[2]},{loc[0]},{loc[1]})",
)
)
if self.name is not None and draw_names:
nametext = drawing.Text(
self.name, 1.2, 5, 5, text_anchor="middle", valign="center"
)
g.elements.append(nametext)
return g
[docs]class VDupleTile(Tile):
[docs] def to_xgrow(self, gluenamemap: Callable[[str], str] = lambda x: x) -> xgt.Tile:
d = super().to_xgrow(gluenamemap)
d.shape = "V"
return d
@classmethod
[docs] def _get_edge_index(cls, v: str) -> int:
return ["N", "NE", "SE", "S", "SW", "NW"].index(v)
@property
[docs] def edge_directions(self) -> List[D]:
return [D.N, D.E, D.E, D.S, D.W, D.W]
@property
[docs] def edge_locations(self) -> List[EdgeLoc]:
return [
EdgeLoc(D.N, (0, 0)),
EdgeLoc(D.E, (0, 0)),
EdgeLoc(D.E, (1, 0)),
EdgeLoc(D.S, (1, 0)),
EdgeLoc(D.W, (1, 0)),
EdgeLoc(D.W, (0, 0)),
]
[docs] def abstract_diagram(
self,
tileset: TileSet | None = None,
draw_names: bool = True,
draw_glues: bool = True,
) -> drawing.Group:
if (self.color is not None) and (self.color in xcolors):
color = xcolors[self.color]
elif (self.color is not None) and self.color[0] == "#":
color = self.color
else:
color = "gray"
g = drawing.Group(id=uuid.uuid4().hex)
g.append(drawing.Rectangle(0, 0, 10, 20, fill=color, stroke="black"))
if draw_glues:
gluetext_locs = [
(5, 1, 0),
(9, 5, 90),
(9, 15, 90),
(5, 19, 0),
(1, 15, -90),
(1, 5, -90),
]
for loc, glue in zip(gluetext_locs, self.edges):
g.append(
drawing.Text(
glue.ident(),
0.8,
loc[0],
loc[1],
text_anchor="middle",
transform=f"rotate({loc[2]},{loc[0]},{loc[1]})",
)
)
if self.name is not None and draw_names:
nametext = drawing.Text(
self.name,
1.2,
5,
10,
transform="rotate(-90, 5, 10)",
text_anchor="middle",
valign="center",
)
g.append(nametext)
return g
[docs]class HDupleTile(Tile):
[docs] def to_xgrow(self, gluenamemap: Callable[[str], str] = lambda x: x) -> xgt.Tile:
d = super().to_xgrow(gluenamemap)
d.shape = "H"
return d
@property
[docs] def edge_directions(self) -> List[D]:
return [D.N, D.N, D.E, D.S, D.S, D.W]
@property
[docs] def edge_locations(self) -> List[EdgeLoc]:
return [
EdgeLoc(D.N, (0, 0)),
EdgeLoc(D.N, (0, 1)),
EdgeLoc(D.E, (0, 1)),
EdgeLoc(D.S, (0, 1)),
EdgeLoc(D.S, (0, 0)),
EdgeLoc(D.W, (0, 0)),
]
@classmethod
[docs] def _get_edge_index(cls, v: str) -> int:
return ["NW", "NE", "E", "SE", "SW", "W"].index(v)
[docs] def abstract_diagram(
self,
tileset: TileSet | None = None,
draw_names: bool = True,
draw_glues: bool = True,
) -> drawing.Group:
if (self.color is not None) and (self.color in xcolors):
color = xcolors[self.color]
elif (self.color is not None) and (self.color[0] == "#"):
color = self.color
else:
color = "rgb(150,150,150)"
g = drawing.Group(id=uuid.uuid4().hex)
g.append(drawing.Rectangle(0, 0, 20, 10, fill=color, stroke="black"))
if draw_glues:
gluetext_locs = [
(5, 1, 0),
(10, 1, 0),
(19, 5, 90),
(15, 9, 0),
(5, 9, 0),
(1, 5, -90),
]
for loc, glue in zip(gluetext_locs, self.edges):
g.append(
drawing.Text(
glue.ident(),
0.8,
loc[0],
loc[1],
text_anchor="middle",
transform=f"rotate({loc[2]},{loc[0]},{loc[1]})",
)
)
if self.name is not None and draw_names:
nametext = drawing.Text(
self.name,
1.2,
10,
5,
text_anchor="middle",
valign="center",
)
g.append(nametext)
return g
[docs]class SupportsGuards:
@abstractmethod
[docs] def create_guards(self, directions: Collection[str | D] = (D.E, D.S)) -> list[Glue]:
...
[docs]class BaseSSTile(SupportsGuards, TileSupportingScadnano):
"""Base class for single-stranded tiles."""
[docs] _edges: List[Glue] # actually SSGlue
[docs] def to_dict(self, refglues: set[str] = set()) -> dict[str, Any]:
d = super().to_dict(refglues=refglues)
d["type"] = self.__class__.__name__
d["sequence"] = self.sequence.seq_str
return d
@property
@abstractmethod
[docs] def _base_domains(self) -> List[SSGlue]:
...
@property
@abstractmethod
[docs] def _base_edges(self) -> List[SSGlue]:
...
@property
@abstractmethod
[docs] def domains(self) -> List[SSGlue]:
...
[docs] def _input_neighborhood_domains(
self,
) -> Sequence[Tuple[Sequence[str], Sequence[str]]]:
raise NotImplementedError
@property
@abstractmethod
[docs] def edge_directions(self) -> list[D]:
...
@property
[docs] def _sequence_length(self) -> int:
return sum(x.dna_length for x in self._base_domains)
def __init__(
self,
edges: Optional[List[Union[str, Glue]]] = None,
name: Optional[str] = None,
color: Optional[Color] = None,
stoic: Optional[float] = None,
sequence: Optional[Seq] = None,
domains: Optional[Sequence[SSGlue]] = None,
note: Optional[str] = None,
use: Optional[Sequence[Use]] = None,
uses: Optional[Sequence[Sequence[Use]]] = None,
):
Tile.__init__(self, edges=[], name=name, color=color, stoic=stoic, note=note)
if edges is None and sequence is None and domains is None:
raise ValueError
if edges is not None:
self._edges = [bd.merge(g) for bd, g in zip(self._base_edges, edges)]
else:
self._edges = [bd.copy() for bd in self._base_edges]
if sequence is not None:
self.sequence | sequence
self.sequence = sequence
elif domains is not None:
if len(self.domains) != len(domains):
raise ValueError
for td, nd in zip(self.domains, domains):
td |= nd
if use is not None:
for e, u in zip(self.edges, use, strict=True):
e.use = u
if uses is not None:
self.uses = list(list(u) for u in uses)
else:
self.uses = []
@property
[docs] def sequence(self) -> Seq:
return Seq("-".join(str(glue.sequence) for glue in self.domains))
@sequence.setter
def sequence(self, seq: Seq) -> None:
seq = Seq(seq)
if seq.dna_length != self._sequence_length:
raise ValueError
pos = 0
base_str = seq.base_str
# fixme: should we check whitespace?
for base_domain, domain in zip(self._base_domains, self.domains):
base_domain.sequence | base_str[pos : pos + base_domain.dna_length] # noqa
domain.sequence = Seq(
seq.base_str[pos : pos + base_domain.dna_length]
) # noqa
pos += base_domain.dna_length
@property
[docs] def edges(self):
return super().edges # type: ignore
@edges.setter
def edges(self, edges: Sequence[Glue]) -> None:
self._edges = [bd.merge(g) for bd, g in zip(self._base_edges, edges)]
[docs] def set_edge(self, i: int, glue: Glue) -> None:
self._edges[i] = self._base_edges[i].merge(glue)
[docs] def __repr__(self) -> str:
s: list[str] = []
s.append(self.__class__.__name__)
s.append("(")
if self.name is not None:
s.append(f"name={repr(self.name)}, ")
s.append("edges=[" + ", ".join(repr(x) for x in self.edges) + "]")
s.append(")")
return "".join(s)
[docs] def __str__(self) -> str:
s = [f"<{self.__class__.__name__}: "]
if self.name is not None:
s.append(self.name + " | ")
s.append(", ".join(g.ident() for g in self.edges))
s.append(">")
return "".join(s)
[docs] def create_guards(self, directions: Collection[str | D] = (D.E, D.S)) -> list[Glue]:
guards: list[Glue] = []
directions = set(x if isinstance(x, D) else D[x] for x in directions)
for ei, d in enumerate(self.edge_directions):
if d not in directions:
continue
else:
guards.append(self.edges[ei].complement)
return guards
[docs]def _scadnano_color(color: Optional[str]) -> Optional[scadnano.Color]:
if color is None:
return None
elif color[0] == "#":
return scadnano.Color(hex_string=color)
else:
xc = xcolors[color]
ci = [int(x) for x in xc[4:-1].split(",")]
return scadnano.Color(*ci)
[docs]class BaseSSTSingle(SingleTile, BaseSSTile):
"""Base class for a standard-orientation SST single tile."""
@property
[docs] def domains(self) -> List[SSGlue]:
e = self.edges
return [e[i] for i in [1, 0, 3, 2]] # type: ignore
@property
[docs] def _base_edges(self) -> List[SSGlue]:
return [self._base_domains[i] for i in [1, 0, 3, 2]]
[docs] def _input_neighborhood_domains(
self,
) -> Sequence[Tuple[Sequence[str], Sequence[str]]]:
if not self.uses:
return []
strands = []
for use in self.uses:
if use == [Use.INPUT, Use.OUTPUT, Use.OUTPUT, Use.INPUT]: # NW
strands.append(
(
("W", "N"),
(
self.edges["W"].complement.ident(),
"algo_fake_spacer",
self.edges["N"].complement.ident(),
),
)
)
elif use == [Use.INPUT, Use.INPUT, Use.OUTPUT, Use.OUTPUT]: # NE
strands.append(
(
("N", "E"),
(
self.edges["N"].complement.ident(),
"algo_fake_spacer",
self.edges["E"].complement.ident(),
),
)
)
elif use == [Use.OUTPUT, Use.OUTPUT, Use.INPUT, Use.INPUT]: # SW
strands.append(
(
("S", "W"),
(
self.edges["S"].complement.ident(),
"algo_fake_spacer",
self.edges["W"].complement.ident(),
),
)
)
elif use == [Use.OUTPUT, Use.INPUT, Use.INPUT, Use.OUTPUT]: # SE
strands.append(
( # FIXME: needed to avoid pseudoknot
("S", "E"),
(
self.edges["S"].complement.ident(),
"algo_fake_spacer",
self.edges["E"].complement.ident(),
),
)
)
return strands
[docs] def to_scadnano(
self, design: scadnano.Design, helix: int, offset: int
) -> scadnano.Strand:
s = design.draw_strand(helix, offset + 21)
for e in self.domains[0:2]:
s.move(-e.dna_length)
s.with_domain_name(e.ident())
s.cross(s.current_helix + 1)
for e in self.domains[2:]:
s.move(e.dna_length)
s.with_domain_name(e.ident())
if self.name is not None:
s.with_name(self.name)
if self.color is not None:
s.with_color(_scadnano_color(self.color))
# We generally don't want to assign complements here because (a) we will be assigning
# sequences for every tile, and (b) in some cases, we will have intentional mismatches.
s.with_sequence(self.sequence.base_str, assign_complement=False)
return s.strand
@dataclass(init=False)
[docs]class BaseSSTSingleWithExtensions(BaseSSTSingle):
[docs] mod5: Domain | None = None
[docs] mod3: Domain | None = None
[docs] def to_dict(self, refglues: set[str] = set()) -> dict[str, Any]:
d = super().to_dict(refglues=refglues)
d["type"] = self.__class__.__name__
d["mod5"] = self.mod5.to_dict() if self.mod5 else None
d["mod3"] = self.mod3.to_dict() if self.mod3 else None
return d
@property
[docs] def domains(self) -> List[SSGlue]:
d = super().domains
if self.mod5 is not None:
d.insert(0, self.mod5)
if self.mod3 is not None:
d.append(self.mod3)
return d
@property
[docs] def _sequence_length(self) -> int:
noext = sum(x.dna_length for x in self._base_domains)
if self.mod5 is not None:
noext += self.mod5.dna_length
if self.mod3 is not None:
noext += self.mod3.dna_length
return noext
@property
[docs] def sequence(self) -> Seq:
return Seq("-".join(str(glue.sequence) for glue in self.domains))
@sequence.setter
def sequence(self, seq: Seq) -> None:
seq = Seq(seq)
if seq.dna_length != self._sequence_length:
raise ValueError
pos = 0
base_str = seq.base_str
_base_domains = self._base_domains.copy()
if self.mod5 is not None:
_base_domains.insert(0, self.mod5)
if self.mod3 is not None:
_base_domains.append(self.mod3)
# fixme: should we check whitespace?
for base_domain, domain in zip(_base_domains, self.domains):
base_domain.sequence | base_str[pos : pos + base_domain.dna_length] # noqa
domain.sequence = Seq(
seq.base_str[pos : pos + base_domain.dna_length]
) # noqa
pos += base_domain.dna_length
def __init__(
self,
edges: Optional[list[Union[str, Glue]]] = None,
name: Optional[str] = None,
color: Optional[Color] = None,
stoic: Optional[float] = None,
sequence: Optional[Seq] = None,
domains: Optional[list[Domain]] = None,
note: Optional[str] = None,
use: Optional[Sequence[Use]] = None,
uses: Optional[Sequence[Sequence[Use]]] = None,
mod5: Seq | str | int | dict | None = None,
mod3: Seq | str | int | dict | None = None,
):
# Don't deal with the sequence just yet.
super().__init__(edges, name, color, stoic, None, domains, note, use, uses)
# Now add the mod5 and mod3 domains, if they exist:
if isinstance(mod5, (str, Seq)):
self.mod5 = Domain(sequence=mod5)
elif isinstance(mod5, int):
self.mod5 = Domain(length=mod5)
elif isinstance(mod5, dict):
g = Glue.from_dict(mod5)
assert isinstance(g, SSGlue)
self.mod5 = g
elif mod5 is None:
self.mod5 = None
else:
raise TypeError(f"mod5 must be str, Seq, int, or None, not {type(mod5)}")
if isinstance(mod3, (str, Seq)):
self.mod3 = Domain(sequence=mod3)
elif isinstance(mod3, int):
self.mod3 = Domain(length=mod3)
elif isinstance(mod3, dict):
g = Glue.from_dict(mod3)
assert isinstance(g, SSGlue)
self.mod3 = g
elif mod3 is None:
self.mod3 = None
else:
raise TypeError(f"mod3 must be str, Seq, int, or None, not {type(mod3)}")
if sequence is not None:
self.sequence = sequence
[docs]class SST10_5S(BaseSSTSingle):
"Single SST, with domains (5'→3') of 11, 10, 10, and 11 nt. North edge is 10nt. 5' is S, 3' is E."
[docs] _base_domains: ClassVar[list[SSGlue]] = [SSGlue(length=x) for x in [11, 10, 10, 11]]
[docs] _scadnano_offsets = ((-1, -11), (-1, 10), (1, 10), (1, -11))
[docs] _scadnano_5p_offset = (1, 21)
@property
[docs] def domains(self) -> List[SSGlue]:
e = self.edges
return [e[i] for i in [2, 3, 0, 1]] # type: ignore
@property
[docs] def _base_edges(self) -> List[SSGlue]:
return [self._base_domains[i] for i in [2, 3, 0, 1]]
[docs]class SST11_5S(BaseSSTSingle):
"Single SST, with domains (5'→3') of 10, 11, 11, and 10 nt. North edge is 11nt. 5' is S, 3' is E."
[docs] _base_domains: ClassVar[list[SSGlue]] = [SSGlue(length=x) for x in [10, 11, 11, 10]]
[docs] _scadnano_offsets = ((-1, -10), (-1, 11), (1, 11), (1, -10))
[docs] _scadnano_5p_offset = (1, 21)
@property
[docs] def domains(self) -> List[SSGlue]:
e = self.edges
return [e[i] for i in [2, 3, 0, 1]] # type: ignore
@property
[docs] def _base_edges(self) -> List[SSGlue]:
return [self._base_domains[i] for i in [2, 3, 0, 1]]
[docs]class SST10(BaseSSTSingle):
"Single SST, with domains (5'→3') of 11, 10, 10, and 11 nt. North edge is 10nt. 5' is E, 3' is S."
[docs] _base_domains: ClassVar[list[SSGlue]] = [SSGlue(length=x) for x in [11, 10, 10, 11]]
[docs] _scadnano_offsets = ((-1, -11), (-1, 10), (1, 10), (1, -11))
[docs] _scadnano_5p_offset = (1, 21)
[docs]class SST11(BaseSSTSingle):
"Single SST, with domains (5'→3') of 10, 11, 11, and 10 nt. North edge is 11nt. 5' is E, 3' is S."
[docs] _base_domains: ClassVar[list[SSGlue]] = [SSGlue(length=x) for x in [10, 11, 11, 10]]
[docs] _scadnano_offsets = ((-1, -10), (-1, 11), (1, 11), (1, -10))
[docs] _scadnano_5p_offset = (1, 21)
[docs]class TileFactory:
[docs] types: dict[str, Type[Tile]]
def __init__(self):
self.types = {}
[docs] def register(self, c: Type[Tile], n: Optional[str] = None) -> None:
self.types[n if n is not None else c.__name__] = c
[docs] def from_dict(self, d: dict[str, Any]) -> Tile:
if "ends" in d: # old ends format
d["edges"] = d.pop("ends")
if "fullseqs" in d: # old fullseqs format
d["sequences"] = d.pop("fullseqs")
if "input" in d: # old input format
d["uses"] = [[3 - x for x in d.pop("input")]] # FIXME
if "edges" in d:
for i in range(0, len(d["edges"])):
glue = d["edges"][i]
if isinstance(glue, dict):
glue = Glue.from_dict(glue)
d["edges"][i] = glue
if "type" in d:
c = self.types[d["type"]]
del d["type"]
return c(**d)
elif "structure" in d:
c = self.types[d["structure"]]
del d["structure"]
return c(**d)
else:
return Tile(**d)
@overload
[docs] def from_scadnano(
self,
d: "scadnano.Strand" | Iterable["scadnano.Strand"],
return_position: Literal[True],
) -> tuple[TileSupportingScadnano, tuple[int, int]]:
...
@overload
def from_scadnano(
self,
d: "scadnano.Strand" | Iterable["scadnano.Strand"],
return_position: Literal[False],
) -> TileSupportingScadnano:
...
def from_scadnano(
self,
d: "scadnano.Strand" | Iterable["scadnano.Strand"],
return_position: bool = False,
) -> tuple[TileSupportingScadnano, tuple[int, int]] | TileSupportingScadnano:
if isinstance(d, Iterable):
raise NotImplementedError
name = d.name
domain_names = [x.name for x in d.domains]
domain_seqs = [x.dna_sequence() for x in d.domains]
domains = [
SSGlue(name=n, sequence=s) for n, s in zip(domain_names, domain_seqs)
]
t = None
for tiletype in self.types.values():
try:
if issubclass(tiletype, TileSupportingScadnano):
t = tiletype(name=name, domains=domains) # type: ignore
break
except ValueError:
continue
if t:
if not return_position:
return t
else:
domain = d.first_domain()
return t, (
domain.helix - t._scadnano_5p_offset[0],
domain.offset_5p() - t._scadnano_5p_offset[1] + 1,
)
else:
raise ValueError
[docs]tile_factory = TileFactory()
for ttype in [Tile, SST10, SST10_5S, SST11, SST11_5S]:
tile_factory.register(ttype)
tile_factory.register(SST10, "SST10_5E")
tile_factory.register(SST11, "SST11_5E")
[docs]SomeTile = TypeVar("SomeTile", bound=Tile)
[docs]class TileList(Generic[SomeTile], UpdateListD[SomeTile]):
[docs] def glues_from_tiles(self) -> GlueList:
gl: GlueList[Glue] = GlueList()
for tile in self:
gl |= tile.edges
return gl
[docs] def domains_from_tiles(self) -> GlueList:
gl: GlueList[Glue] = GlueList()
for tile in self:
gl |= tile.domains
return gl
[docs]class DAOETile(Tile):
[docs] _edges: List[Glue] # actually dxglue
[docs] _strand_sequences: List[str] | None
[docs] def to_dict(self, refglues: set[str] = set()) -> dict[str, Any]:
d = super().to_dict(refglues=refglues)
d["type"] = self.__class__.__name__
return d
def __init__(
self,
name: str,
sequence: str | None = None,
sequences: list[str] | None = None,
edges: List[Glue | str] | None = None,
label: str | None = None,
color: str | None = None,
**kwargs,
):
if sequence:
assert not sequences
self._strand_sequences = sequence.split("+")
elif sequences:
self._strand_sequences = sequences
else:
self._strand_sequences = None
if label:
log.warning("label not currently handled")
super().__init__(name=name, edges=edges, color=color, **kwargs)
[docs]class DAOESingle(SingleTile, DAOETile, metaclass=ABCMeta):
# @property
# @abstractmethod
# def _endlocs(self) -> list[tuple[int, slice]]:
# ...
# @property
# @abstractmethod
# def _baseglues(self) -> List[DXGlue]:
# ...
...
[docs]class DAOESingle5p(DAOESingle):
[docs] _baseglues: ClassVar[List[DXGlue]] = [
DXGlue("TD", length=5),
DXGlue("TD", length=5),
DXGlue("DT", length=5),
DXGlue("DT", length=5),
]
[docs] _gluelocs = [
(0, slice(0, 5)),
(3, slice(0, 5)),
(3, slice(21, None)),
(0, slice(21, None)),
]
tile_factory.register(DAOESingle5p, "tile_daoe_5up")
tile_factory.register(DAOESingle5p, "tile_daoe_5up_2h")
[docs]class DAOESingle3p(DAOESingle):
[docs] _baseglues: ClassVar[List[DXGlue]] = [
DXGlue("DT", length=5),
DXGlue("DT", length=5),
DXGlue("TD", length=5),
DXGlue("TD", length=5),
]
[docs] _gluelocs = [
(0, slice(21, None)),
(3, slice(21, None)),
(3, slice(0, 5)),
(0, slice(0, 5)),
]
tile_factory.register(DAOESingle3p, "tile_daoe_3up")
tile_factory.register(DAOESingle3p, "tile_daoe_3up_2h")
[docs]class DAOEHDouble3p(HDupleTile, DAOETile):
...
tile_factory.register(DAOEHDouble3p)
tile_factory.register(DAOEHDouble3p, "tile_daoe_doublehoriz_35up")
tile_factory.register(DAOEHDouble3p, "tile_daoe_doublehoriz_35up_2h3h") # FIXME
tile_factory.register(DAOEHDouble3p, "tile_daoe_doublehoriz_35up_1h2i") # FIXME
[docs]class DAOEHDouble5p(HDupleTile, DAOETile):
...
[docs]class DAOEVDouble3p(VDupleTile, DAOETile):
...
tile_factory.register(DAOEVDouble3p)
tile_factory.register(DAOEVDouble3p, "tile_daoe_doublevert_35up")
tile_factory.register(DAOEVDouble3p, "tile_daoe_doublevert_35up_4h5h") # FIXME
[docs]class DAOEVDouble5p(VDupleTile, DAOETile):
...