from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Optional, TYPE_CHECKING, Any, Callable, Sequence, Type, TypeVar, cast
import attrs
import scadnano
if TYPE_CHECKING:
from alhambra.tilesets import XgrowGlueOpts
import xgrow.tileset as xgt
from .glues import DXGlue, Glue, glue_factory
[docs]class Seed(ABC):
"""Abstact Base Class for a seed structure.
Generally, seeds need:
- A method to convert the seed to xgrow-usable information.
- Methods to convert the seed to and from a dict, for storage
"""
@abstractmethod
[docs] def to_xgrow(
self,
gluenamemap: Callable[[str], str] = lambda x: x,
offset: tuple[int, int] | None = None,
xgtiles: Optional[Sequence[xgt.Tile]] = None,
) -> tuple[list[xgt.Tile], list[xgt.Bond], xgt.InitState]:
"""Create xgrow implementation of the seed.
Converts the Seed to a list of xgrow tiles to add to a system, a list of bonds to add,
and an initial state.
"""
raise NotImplementedError
@abstractmethod
[docs] def to_dict(self) -> dict:
raise NotImplementedError
@classmethod
@abstractmethod
[docs] def from_dict(cls: Type[T], d: dict) -> T:
raise NotImplementedError
[docs]class SeedSupportingScadnano(Seed):
@abstractmethod
[docs] def ho_from_seed_offset(
self, seed_offset: tuple[int, int], gridysize: int
) -> tuple[int, int]:
...
@abstractmethod
[docs] def to_scadnano(
self, design: scadnano.Design, helix: int, offset: int
) -> list[scadnano.Strand]:
...
[docs]class SeedFactory:
[docs] types: dict[str, Type[Seed]]
def __init__(self):
self.types = {}
[docs] def register(self, c: Type[Seed], n: Optional[str] = None):
self.types[n if n is not None else c.__name__] = c
[docs] def from_dict(self, d: dict[str, Any]) -> Seed:
if "type" in d:
c = self.types[d["type"]]
return c.from_dict({k: v for k, v in d.items() if k != "type"})
else:
raise ValueError
from alhambra.tiles import log
@attrs.define()
[docs]class DXAdapter:
[docs] name: str | None = attrs.field(default=None)
[docs] edges: list[str] | None = attrs.field(default=None)
[docs] tilebase: str | None = attrs.field(default=None)
[docs] loc: int | None = attrs.field(default=None)
[docs] sequences: list[str] | None = attrs.field(default=None)
@classmethod
[docs] def from_dict(cls, d: dict) -> DXAdapter:
if "ends" in d:
d["edges"] = d.pop("ends")
if "seqs" in d:
d["sequences"] = d.pop("seqs")
for k in d.keys():
if k not in ["name", "edges", "tilebase", "loc", "sequences"]:
log.warning(f"Unknown key {k} in DXAdapter.from_dict")
return cls(
name=d.get("name", None),
edges=d.get("edges", None),
tilebase=d.get("tilebase", None),
loc=d.get("loc", None),
sequences=d.get("sequences", None),
)
[docs] def to_dict(self) -> dict:
return {
"name": self.name,
"edges": self.edges,
"tilebase": self.tilebase,
"loc": self.loc,
"sequences": self.sequences,
}
[docs] def xgrow_edges(
self,
i: int,
xgtiles: Optional[Sequence[xgt.Tile]],
gluenamemap: Callable[[str], str],
) -> list[str | int]:
if self.edges:
return cast(
list[str | int],
["seed"] + [gluenamemap(e) for e in self.edges] + ["seed"],
)
assert xgtiles is not None
assert self.tilebase is not None
tile = [t for t in xgtiles if t.name == self.tilebase][0]
match tile.shape:
case "S":
glues_from_tile = tile.edges[1:3] # FIXME
case "H":
glues_from_tile = tile.edges[2:4]
case "V":
glues_from_tile = tile.edges[2:4]
case _:
raise ValueError(f"Unknown tile shape {tile.shape}")
return ["seed"] + glues_from_tile + ["seed"]
@attrs.define()
[docs]class DXOrigamiSeed(Seed):
[docs] adapters: list[DXAdapter] = attrs.field(factory=list)
[docs] use_adapters: list[str] | None = attrs.field(default=None)
@classmethod
[docs] def from_dict(cls, d: dict) -> DXOrigamiSeed:
return cls(
adapters=[DXAdapter.from_dict(x) for x in d["adapters"]],
use_adapters=d.get("use_adapters", None),
)
[docs] def to_dict(self) -> dict:
return {
"type": "DXOrigamiSeed",
"adapters": [x.to_dict() for x in self.adapters],
"use_adapters": self.use_adapters,
}
[docs] def to_xgrow(
self,
gluenamemap: Callable[[str], str] = lambda x: x,
offset: tuple[int, int] | None = None,
xgtiles: Optional[Sequence[xgt.Tile]] = None,
) -> tuple[list[xgt.Tile], list[xgt.Bond], xgt.InitState]:
import xgrow.tileset as xgt
if offset is None:
offset = (0, 0)
ox, oy = offset
tiles: list[xgt.Tile] = []
tiles.append(
xgt.Tile([0, "seed", "seed", "seed"], "seed", stoic=0, color="white")
)
bonds = [xgt.Bond("seed", 100)]
initstate: xgt.InitState = xgt.InitState()
adapters = self.adapters
nadapts = len(adapters)
oy += nadapts + 2
ox = 2
for i, adapter in enumerate(adapters):
if self.use_adapters is not None and adapter.name not in self.use_adapters:
raise NotImplementedError
tile = xgt.Tile(
name=adapter.name or f"adapter_{i}",
edges=adapter.xgrow_edges(i, xgtiles, gluenamemap),
stoic=0,
color="brown",
)
tiles.append(tile)
initstate.append((ox + i, oy - i, adapter.name or f"adapter_{i}"))
if i != 0:
initstate.append((ox + i - 1, oy - i, "seed"))
return tiles, bonds, initstate
[docs]def _convert_adapts(
adapters: Sequence[tuple[str | Glue, str | Glue]]
) -> list[tuple[Glue, Glue]]:
# todo: verify
return [
(
Glue(a[0]) if not isinstance(a[0], Glue) else a[0],
Glue(a[1]) if not isinstance(a[1], Glue) else a[1],
)
for a in adapters
]
@attrs.define()
[docs]class DiagonalSESeed(Seed):
"Tall rectangle origami to DX-tile seed (Barish et al)"
# FIXME: fixed-choice adapters for now
[docs] adapters: Sequence[tuple[Glue, Glue]] = attrs.field(
converter=_convert_adapts, on_setattr=attrs.setters.convert
)
[docs] def to_dict(self) -> dict:
d: dict[str, Any] = {}
d["type"] = self.__class__.__name__
d["adapters"] = [(g1.to_dict(), g2.to_dict()) for g1, g2 in self.adapters]
return d
@classmethod
[docs] def from_dict(cls, d: dict[str, Any]) -> DiagonalSESeed:
d["adapters"] = [
(glue_factory.from_dict(g1), glue_factory.from_dict(g2))
for g1, g2 in d["adapters"]
]
return cls(**d)
[docs] def to_xgrow(
self,
gluenamemap: Callable[[str], str] = lambda x: x,
offset: tuple[int, int] | None = None,
xgtiles: Optional[Sequence[xgt.Tile]] = None,
) -> tuple[list[xgt.Tile], list[xgt.Bond], xgt.InitState]:
import xgrow.tileset as xgt
if offset is None:
offset = (0, 0)
xgtiles = []
locs: list[tuple[int, int, str]] = []
bonds = [xgt.Bond("seed", 100)]
xgtiles.append(
xgt.Tile([0, "seed", "seed", "seed"], "seed", stoic=0, color="white")
)
y = len(self.adapters) + offset[1] # + 1
x = 1 + offset[0]
for i, (eg, sg) in enumerate(self.adapters):
egn, sgn = gluenamemap(eg.ident()), gluenamemap(sg.ident())
xa = xgt.Tile(
["seed", egn, sgn, "seed"],
f"adapter_{i}",
stoic=0,
color="brown",
)
locs.append((x, y, cast(str, xa.name)))
if x != 1:
locs.append((x - 1, y, "seed"))
x += 1
y -= 1
xgtiles.append(xa)
return xgtiles, bonds, xgt.InitState(locs)
[docs]seed_factory = SeedFactory()
seed_factory.register(DiagonalSESeed)
# seed_factory.register(DX_TallRect, "tallrect")
# seed_factory.register(DX_TallRect)
seed_factory.register(DXOrigamiSeed, "triangle_side2")
seed_factory.register(DXOrigamiSeed, "longrect")
seed_factory.register(DXOrigamiSeed, "tallrect")
seed_factory.register(DXOrigamiSeed, "bigseed")