from __future__ import annotations
from abc import ABC, abstractmethod
import copy
from io import TextIOWrapper
import logging
from os import PathLike
from pathlib import Path
import warnings
from dataclasses import dataclass, field
from typing import (
TYPE_CHECKING,
Any,
Callable,
Collection,
Iterable,
Literal,
Mapping,
Optional,
Type,
TypeVar,
Union,
cast,
)
from typing_extensions import Self, TypeAlias
import numpy as np
from alhambra.seq import MergeConflictError
from . import drawing
from numpy import isin
from alhambra.classes import Serializable
from alhambra.grid import (
AbstractLattice,
Lattice,
LatticeSupportingScadnano,
ScadnanoLattice,
_skip_polyT_and_inertname,
lattice_factory,
)
# from . import fastreduceD as fastreduce
from .glues import Glue, GlueList, SSGlue
from .seeds import Seed, seed_factory
from .tiles import (
D,
EdgeLoc,
SupportsGuards,
Tile,
TileList,
TileSupportingScadnano,
tile_factory,
)
import logging
[docs]log = logging.getLogger(__name__)
if TYPE_CHECKING:
import matplotlib.pyplot as plt
import scadnano
import xgrow.parseoutput
import xgrow.tileset as xgt
import stickydesign as sd
[docs]_gl = {
EdgeLoc(D.N, (0, 0)): (0, 0, 10, 0),
EdgeLoc(D.E, (0, 0)): (10, 0, 10, 10),
EdgeLoc(D.S, (0, 0)): (0, 10, 10, 10),
EdgeLoc(D.W, (0, 0)): (0, 0, 0, 10),
EdgeLoc(D.N, (0, 1)): (10, 0, 20, 0),
EdgeLoc(D.E, (1, 0)): (10, 10, 10, 20),
EdgeLoc(D.E, (0, 1)): (20, 0, 20, 10),
EdgeLoc(D.S, (1, 0)): (0, 20, 10, 20),
EdgeLoc(D.S, (0, 1)): (10, 10, 20, 10),
EdgeLoc(D.W, (1, 0)): (0, 10, 0, 20),
}
[docs]class XgrowGlueOpts(ABC):
[docs] def get_xgrow_gse(self, tileset: "TileSet") -> float | None:
return None
@abstractmethod
[docs] def calculate_gses(
self, tileset: "TileSet"
) -> tuple[list[xgt.Bond], list[xgt.Glue]]:
...
[docs] def glue_name_map(self, tileset: "TileSet") -> Callable[[str], str]:
return lambda x: x
@classmethod
[docs] def from_str(cls, glueopts: str) -> XgrowGlueOpts:
if glueopts == "self-complementary":
return GrowSelfComplementaryGlues()
elif glueopts == "perfect":
return GrowPerfectGlues()
elif glueopts == "orthogonal":
return GrowOrthogonalGlues()
elif glueopts == "full":
return GrowFullGlues()
else:
raise ValueError(f"Unknown glueopts: {glueopts}")
[docs]class GrowSelfComplementaryGlues(XgrowGlueOpts):
[docs] def get_xgrow_gse(self, tileset: "TileSet") -> float | None:
return None
[docs] def glue_name_map(self, tileset: "TileSet") -> Callable[[str], str]:
return lambda x: x[:-1] if x.endswith("*") else x
[docs] def calculate_gses(
self, tileset: "TileSet"
) -> tuple[list[xgt.Bond], list[xgt.Glue]]:
allglues = tileset.allglues
bonds = [
xgt.Bond(g.name, 0)
for g in allglues
if g.name and ("null" in g.name or "inert" in g.name or "hairpin" in g.name)
]
return bonds, []
[docs]SD_ENERGETICS_CLASSES: dict[str, "sd.EnergeticsBasic"] = {}
[docs]HAS_SD_ACCEL: bool = False
[docs]def _generate_stickydesign_energetic_classes() -> None:
global SD_ENERGETICS_CLASSES
global HAS_SD_ACCEL
if SD_ENERGETICS_CLASSES:
return
import stickydesign as sd
SD_ENERGETICS_CLASSES = {"SSGlue": sd.EnergeticsBasic, "DXGlue": sd.EnergeticsDAOE}
HAS_SD_ACCEL = sd.energetics_basic.ACCEL
[docs]class GrowPerfectGlues(XgrowGlueOpts):
[docs] def get_xgrow_gse(self, tileset: "TileSet") -> float | None:
return None
[docs] def calculate_gses(
self, tileset: "TileSet"
) -> tuple[list[xgt.Bond], list[xgt.Glue]]:
import xgrow.tileset as xgt
allglues = tileset.allglues
bonds = [xgt.Bond(g.name, 0) for g in allglues]
bonds.extend(
xgt.Bond(g.complement.name, 0)
for g in allglues
if g.complement.name not in allglues
)
xg_glues = [
xgt.Glue(
g.name,
g.complement.name,
g.abstractstrength if g.abstractstrength is not None else 1,
)
for g in allglues
]
return bonds, xg_glues
@dataclass
[docs]class GrowOrthogonalGlues(XgrowGlueOpts):
[docs] temperature: float | None = None
[docs] alpha: float | None = None
[docs] def get_xgrow_gse(self, tileset: "TileSet") -> float | None:
return 1.0
[docs] def calculate_gses(
self, tileset: "TileSet"
) -> tuple[list[xgt.Bond], list[xgt.Glue]]:
import xgrow.tileset as xgt
import stickydesign as sd
alpha = tileset.params["alpha"] if self.alpha is None else self.alpha
temperature = (
tileset.params["temperature"]
if self.temperature is None
else self.temperature
)
allglues = tileset.allglues
_generate_stickydesign_energetic_classes()
R = 1.9872041e-3 # kcal/mol/K
T_in_K = temperature + 273.15
RT = R * T_in_K
xgbonds = [xgt.Bond(g.name, 0) for g in allglues]
xgbonds.extend(
xgt.Bond(g.complement.name, 0)
for g in allglues
if g.complement.name not in allglues
)
sd_energetics = {
k: v(temperature=temperature) for k, v in SD_ENERGETICS_CLASSES.items()
}
sg: dict[
tuple[str, str, int], list[SSGlue]
] = {} # FIXME: should support DXGlue
for g in allglues:
k = (g.__class__.__name__, g.etype, g.dna_length)
sg[k] = sg.get(k, [])
sg[k].append(g)
sge = {
k: (
[x.ident() for x in v],
sd.endarray([x.sequence.base_str.lower() for x in v], k[1]),
)
for k, v in sg.items()
}
bonds: dict[str, float] = {}
for k, v in sge.items():
m = sd_energetics[k[0]].matching_uniform(v[1]) / RT + alpha
bonds |= {n: x for n, x in zip(v[0], m) if not n.endswith("*")}
return xgbonds, [xgt.Glue(n, n + "*", v) for n, v in bonds.items()]
@dataclass
[docs]class GrowFullGlues(XgrowGlueOpts):
[docs] temperature: float | None = None
[docs] alpha: float | None = None
[docs] def get_xgrow_gse(self, tileset: "TileSet") -> float | None:
return 1.0
[docs] def calculate_gses(
self, tileset: "TileSet"
) -> tuple[list[xgt.Bond], list[xgt.Glue]]:
import xgrow.tileset as xgt
import stickydesign as sd
alpha = tileset.params["alpha"] if self.alpha is None else self.alpha
temperature = (
tileset.params["temperature"]
if self.temperature is None
else self.temperature
)
allglues = tileset.allglues
_generate_stickydesign_energetic_classes()
if not HAS_SD_ACCEL:
warnings.warn(
"StickyDesign acceleration not available: full glue calculations may be very slow."
)
R = 1.9872041e-3 # kcal/mol/K
T_in_K = temperature + 273.15
RT = R * T_in_K
bonds = [xgt.Bond(g.name, 0) for g in allglues]
bonds.extend(
xgt.Bond(g.complement.name, 0)
for g in allglues
if g.complement.name not in allglues
)
sd_energetics = {
k: v(temperature=temperature) for k, v in SD_ENERGETICS_CLASSES.items()
}
sg: dict[tuple[str, str, int], list[SSGlue]] = {}
for g in allglues:
k = (g.__class__.__name__, g.etype, g.dna_length)
sg[k] = sg.get(k, [])
sg[k].append(g)
sge = {
k: (
[x.ident() for x in v],
sd.endarray([x.sequence.base_str.lower() for x in v], k[1]),
)
for k, v in sg.items()
}
gluelinks: dict[tuple[str, str], float] = {}
for k, v in sge.items():
glm = (
sd_energetics[k[0]]
.uniform(
np.repeat(v[1], v[1].shape[0], 0), np.tile(v[1], (v[1].shape[0], 1))
)
.reshape((v[1].shape[0], v[1].shape[0]))
/ RT
+ alpha
)
for i in range(0, len(v[0])):
for j in range(0, len(v[0])):
gluelinks[(v[0][i], v[0][j])] = glm[i, j]
return bonds, [
xgt.Glue(n[0], n[1], v) for n, v in gluelinks.items() if v > self.threshold
]
from alhambra_mixes import Mix, Q_, nM, ureg
from alhambra_mixes.units import _parse_conc_required, _ratio
from alhambra_mixes.logging import log as log_mix
@dataclass(init=False)
[docs]class TileSet(Serializable):
"Class representing a tileset, whether abstract or sequence-level."
[docs] seeds: dict[str | int, Seed]
[docs] lattices: dict[str | int, Lattice]
[docs] guards: dict[str | int, list[str]]
def __init__(
self,
tiles: Iterable[Tile] = tuple(),
glues: Iterable[Glue] = tuple(),
seeds: Mapping[str | int, Seed] | None = None,
*,
seed: Seed | None = None,
lattices: Mapping[str | int, Lattice] | None = None,
guards: Mapping[str | int, list[str]] = dict(),
params: dict | None = None,
) -> None:
if seed is not None:
if seeds is not None:
raise ValueError("Cannot specify both seed and seeds")
seeds = {'default': seed}
self.tiles = TileList(tiles)
self.glues = GlueList(glues)
self.seeds = dict(seeds) if seeds else dict()
self.lattices = dict(lattices) if lattices else dict()
self.guards = dict(guards)
if params is not None:
self.params = params
else:
self.params = dict()
@classmethod
[docs] def from_mix(
cls: Type[TileSet],
mix: Mix,
tilesets_or_lists: TileSet | TileList | Iterable[TileSet | TileList],
*,
seed: bool | Seed = False,
base_conc: ureg.Quantity | str = Q_(100.0, nM),
) -> TileSet:
"""
Given some :any:`TileSet`\ s, or lists of :any:`Tile`\ s from which to
take tiles, generate an TileSet from the mix.
"""
from .tiles import BaseSSTile
base_conc = _parse_conc_required(base_conc)
newts = cls()
if isinstance(tilesets_or_lists, (TileList, TileSet)):
tilesets_or_lists = [tilesets_or_lists]
for name, row in mix.all_components().iterrows():
new_tile = None
for tl_or_ts in tilesets_or_lists:
try:
if isinstance(tl_or_ts, TileSet):
tile = tl_or_ts.tiles[name]
else:
tile = tl_or_ts[name]
new_tile = tile.copy()
if isinstance(new_tile, BaseSSTile) and (
(seq := getattr(row["component"], "sequence", None)) is not None
):
try:
new_tile.sequence |= seq
except MergeConflictError as e:
# FIXME: we should keep track of all of these and have
# an error at the end.
raise ValueError(
f"Component {name} has sequence that does not match "
f"tileset tile {tile.name}. First has {seq}, second has {tile.sequence}."
) from None
new_tile.stoic = float(
_ratio(Q_(row["concentration_nM"], nM), base_conc)
)
newts.tiles.add(new_tile)
break
except KeyError:
pass
if new_tile is None:
log_mix.warn(f"Component {name} not found in tile lists.")
firstts = next(iter(tilesets_or_lists))
assert isinstance(firstts, TileSet)
if seed is True:
newts.seeds["default"] = firstts.seeds["default"]
elif seed is False:
pass
elif isinstance(seed, Seed):
newts.seeds["default"] = seed
elif isinstance(seed, str):
newts.seeds["default"] = firstts.seeds[seed]
if len(newts.tiles) == 0:
raise ValueError("No mix components match tiles.")
return newts
### XGROW METHODS
[docs] def run_xgrow(
self: TileSet,
to_lattice: bool = True,
_include_out: bool = False,
glues: XgrowGlueOpts | str | None = None,
seed: str | int | Seed | None | Literal[False] = None,
seed_offset: tuple[int, int] | None = None,
xgrow_seed: tuple[int, int, int | str] | None = None,
**kwargs: Any,
) -> Any: # FIXME
"""Run the tilesystem in Xgrow."""
import xgrow
import xgrow.parseoutput
from xgrow.parseoutput import XgrowOutput
xgrow_tileset = self.to_xgrow(
seed=seed, seed_offset=seed_offset, glue_handling=glues
)
if xgrow_seed is not None:
kwargs["seed"] = xgrow_seed
if not to_lattice:
return (xgrow.run(xgrow_tileset, **kwargs),)
out = cast(
XgrowOutput,
xgrow.run(
xgrow_tileset,
outputopts="array",
**kwargs,
),
)
assert out.tiles is not None
newarray = np.full_like(out.tiles[1:-1, 1:-1], "", dtype=object)
for ix, xti in np.ndenumerate(out.tiles[1:-1, 1:-1]):
if xti == 0:
continue
if xti > len(xgrow_tileset.tiles):
continue
tile_name = xgrow_tileset.tiles[int(xti) - 1].name
if tile_name in self.tiles.data.keys():
newarray[ix] = tile_name
if seed is None:
if len(self.seeds) > 0:
seed = next(iter(self.seeds.values()))
elif seed is False:
seed = None
elif isinstance(seed, (int, str)):
seed = self.seeds[cast(Union[int, str], seed)]
if seed is not None and hasattr(seed, "_lattice"):
lattice_type: Type[AbstractLattice] = seed._lattice # type: ignore
else:
lattice_type = AbstractLattice
a = lattice_type(newarray, cast(Optional[Seed], seed), seed_offset)
if not _include_out:
return a
else:
return a, out
[docs] def to_rgrow(
self,
glue_handling: XgrowGlueOpts | str | None = None,
seed: str | int | Seed | None | Literal[False] = None,
seed_offset: tuple[int, int] | None = None,
**kwargs,
):
import rgrow as rg
import xgrow.tileset as xgt
d = self.to_rgrow_dict(
glue_handling=glue_handling, seed=seed, seed_offset=seed_offset, **kwargs
)
return rg.TileSet.from_dict(d) # type: ignore
# (FIXME: rgrow needs a fix here)
[docs] def to_rgrow_dict(
self,
glue_handling: XgrowGlueOpts | str | None = None,
seed: str | int | Seed | None | Literal[False] = None,
seed_offset: tuple[int, int] | None = None,
**kwargs,
):
import rgrow as rg
import xgrow.tileset as xgt
d = self.to_xgrow(
glue_handling=glue_handling, seed=seed, seed_offset=seed_offset
).to_dict()
d["options"] = d.pop("xgrowargs")
if "initstate" in d:
d["options"]["seed"] = d.pop("initstate")
if "k" in d["options"]:
del d["options"]["k"] # k value for xgrow is modified; FIXME
for k in [
#"concentration",
"alpha",
#"temperature",
"model",
"chunk_handling",
"chunk_size",
"canvas-type",
"fission",
"threshold",
"size",
]:
if k in self.params:
d["options"][k] = self.params[k]
for k, v in kwargs.items():
if v is not None:
d["options"][k] = v
else:
d["options"].pop(k, None)
return d
# def to_rgrow(
# self,
# glue_handling: XgrowGlueOpts | str | None = None,
# seed: str | int | Seed | None | Literal[False] = None,
# seed_offset: tuple[int, int] | None = None,
# ) -> xgt.TileSet:
# "Convert Alhambra TileSet to an XGrow TileSet"
# import rgrow as rg
# if glue_handling is None:
# glue_handling = self.params.get("glue-handling", "perfect")
# glue_handling = (
# XgrowGlueOpts.from_str(glue_handling)
# if isinstance(glue_handling, str)
# else cast(XgrowGlueOpts, glue_handling)
# )
# gluenamemap = glue_handling.glue_name_map(self)
# self.tiles.refreshnames()
# self.glues.refreshnames()
# tiles = [t.to_xgrow(gluenamemap) for t in self.tiles]
# bonds, rg_glues = glue_handling.calculate_gses(self)
# if seed is None:
# if self.seeds:
# seed = next(iter(self.seeds.values()))
# else:
# seed = False
# if seed is not False and (isinstance(seed, str) or isinstance(seed, int)):
# seed = self.seeds[seed]
# if seed is False:
# seed_tiles: list[rg.Tile] = []
# seed_bonds: list[rg.Bond] = []
# initstate = None
# else:
# seed_tiles, seed_bonds, initstate = cast(Seed, seed).to_xgrow(
# gluenamemap, offset=seed_offset, xgtiles=tiles
# )
# xgrow_tileset = rg.TileSet(
# seed_tiles + tiles, seed_bonds + bonds, seed=initstate, glues=rg_glues
# )
# ggse = glue_handling.get_xgrow_gse(self)
# if ggse is not None:
# xgrow_tileset.gse = ggse
# gconc = self.params.get("concentration", None)
# galpha = self.params.get("alpha", None)
# if gconc is not None and galpha is not None:
# xgrow_tileset.gmc = galpha - np.log(gconc * 1e-9)
# xgrow_tileset.kf = 1e6 * np.exp(galpha)
# xgrow_tileset.alpha = galpha
# return xgrow_tileset
[docs] def to_xgrow(
self,
glue_handling: XgrowGlueOpts | str | None = None,
seed: str | int | Seed | None | Literal[False] = None,
seed_offset: tuple[int, int] | None = None,
) -> xgt.TileSet:
"Convert Alhambra TileSet to an XGrow TileSet"
import xgrow.tileset as xgt
if glue_handling is None:
glue_handling = self.params.get("glue-handling", "perfect")
glue_handling = (
XgrowGlueOpts.from_str(glue_handling)
if isinstance(glue_handling, str)
else cast(XgrowGlueOpts, glue_handling)
)
gluenamemap = glue_handling.glue_name_map(self)
self.tiles.refreshnames()
self.glues.refreshnames()
tiles = [t.to_xgrow(gluenamemap) for t in self.tiles]
bonds, xg_glues = glue_handling.calculate_gses(self)
if seed is None:
if self.seeds:
seed = next(iter(self.seeds.values()))
else:
seed = False
if seed is not False and (isinstance(seed, str) or isinstance(seed, int)):
seed = self.seeds[seed]
if seed is False:
seed_tiles: list[xgt.Tile] = []
seed_bonds: list[xgt.Bond] = []
initstate = None
else:
seed_tiles, seed_bonds, initstate = cast(Seed, seed).to_xgrow(
gluenamemap, offset=seed_offset, xgtiles=tiles
)
xgrow_tileset = xgt.TileSet(
seed_tiles + tiles, seed_bonds + bonds, initstate=initstate, glues=xg_glues
)
ggse = glue_handling.get_xgrow_gse(self)
if ggse is not None:
xgrow_tileset.xgrowargs.Gse = ggse
gconc = self.params.get("concentration", None)
galpha = self.params.get("alpha", None)
if gconc is not None and galpha is not None:
xgrow_tileset.xgrowargs.Gmc = galpha - np.log(gconc * 1e-9)
xgrow_tileset.xgrowargs.k = 1e6 * np.exp(galpha)
return xgrow_tileset
[docs] def _to_xgrow_dict(self) -> dict:
"""DEPRECATED: to xgrow dict"""
return self.to_xgrow().to_dict()
[docs] def summary(self):
"""Returns a short summary line about the TileSet"""
self.tiles.refreshnames()
self.glues.refreshnames()
# self.check_consistent()
info = {
"ntiles": len(self.tiles),
"nrt": len([x for x in self.tiles if not x.is_fake]),
"nft": len([x for x in self.tiles if x.is_fake]),
"nends": len(self.glues),
"ntends": len(self.tiles.glues_from_tiles()),
"tns": " ".join(x.name for x in self.tiles if x.name),
"ens": " ".join(x.name for x in self.glues if x.name)
# if ("info" in self.keys() and "name" in self["info"].keys())
# else "",
}
tun = sum(1 for x in self.tiles if x.name is None)
if tun > 0:
info["tns"] += " ({} unnamed)".format(tun)
eun = sum(1 for x in self.glues if x.name is None)
if eun > 0:
info["ens"] += " ({} unnamed)".format(eun)
if info["nft"] > 0:
info["nft"] = " (+ {} fake)".format(info["nft"])
else:
info["nft"] = ""
return "TileSet: {nrt} tiles{nft}, {nends} ends, {ntends} ends in tiles.\nTiles: {tns}\nEnds: {ens}".format(
**info
)
[docs] def __repr__(self) -> str:
self.tiles.refreshnames()
self.glues.refreshnames()
return f"TileSet({len(self.tiles)} tiles, {len(self.glues)} glues)"
[docs] def __str__(self) -> str:
return self.summary()
@classmethod
[docs] def from_scadnano(
cls: Type[TileSet], des: scadnano.Design, ret_fails: bool = False
) -> TileSet:
"""Create TileSet from Scadnano Design."""
import scadnano
ts = cls()
tiles: TileList[TileSupportingScadnano] = TileList()
ts.glues = GlueList()
positions: dict[tuple[int, int], str] = {}
for strand in des.strands:
try:
t, o = tile_factory.from_scadnano(strand, return_position=True)
except ValueError:
warnings.warn(f"Failed to import strand {strand.name}.")
except NotImplementedError:
warnings.warn(f"Failed to import strand {strand.name}.")
else:
positions[o] = t.ident()
if t.name in tiles.data.keys():
if t != tiles[t.ident()]:
warnings.warn(f"Skipping unequal duplicate strand {t.name}.")
else:
tiles.add(t)
ts.tiles = cast(TileList[Tile], tiles)
ts.lattices = {0: cast(Lattice, ScadnanoLattice(positions))}
return ts
[docs] def to_scadnano(
self, lattice: LatticeSupportingScadnano | None = None
) -> scadnano.Design:
"""Export TileSet (with lattice) as Scadnano Design."""
import scadnano
self.tiles.refreshnames()
self.glues.refreshnames()
if lattice is not None:
if hasattr(lattice, "seed") and hasattr(lattice.seed, "update_details"):
lattice.seed.update_details(self.allglues, self.tiles) # type: ignore
return lattice.to_scadnano(self)
for tlattice in self.lattices:
if isinstance(tlattice, LatticeSupportingScadnano):
return tlattice.to_scadnano(self)
raise ValueError
[docs] def to_dict(self) -> dict:
d: dict[str, Any] = {}
self.tiles.refreshnames()
self.glues.refreshnames()
allglues = self.glues | self.tiles.glues_from_tiles()
refglues = set(allglues.data.keys()) # FIXME
if self.tiles:
d["tiles"] = [t.to_dict(refglues=refglues) for t in self.tiles.aslist()]
if allglues:
d["glues"] = [g.to_dict() for g in allglues.aslist()]
if self.seeds:
d["seeds"] = {k: v.to_dict() for k, v in self.seeds.items()}
if self.lattices:
d["lattices"] = {k: v.asdict() for k, v in self.lattices.items()}
if self.guards:
d["guards"] = self.guards
if self.params:
d["params"] = self.params.copy()
return d
@classmethod
[docs] def from_dict(cls: Type[TileSet], d: dict) -> TileSet:
ts = cls()
ts.tiles = TileList(Tile.from_dict(x) for x in d.get("tiles", []))
ts.glues = GlueList(Glue.from_dict(x) for x in d.get("glues", []))
ts.seeds = {k: seed_factory.from_dict(v) for k, v in d.get("seeds", {}).items()}
try:
ts.guards = {k: v for k, v in d.get("guards", {}).items()}
except AttributeError:
log.warning("Failed to load guards.")
if not ts.seeds and "seed" in d:
ts.seeds = {0: seed_factory.from_dict(d["seed"])}
ts.lattices = {
k: lattice_factory.from_dict(v) for k, v in d.get("lattices", {}).items()
}
if "params" in d:
ts.params = copy.deepcopy(d["params"])
return ts
[docs] def _serialize(self) -> Any:
return self.to_dict()
@classmethod
[docs] def _deserialize(cls, input: Any) -> TileSet:
return cls.from_dict(input)
@property
[docs] def allglues(self) -> GlueList:
return self.tiles.glues_from_tiles() | self.glues
@property
[docs] def alldomains(self) -> GlueList:
return self.tiles.domains_from_tiles() | self.glues
[docs] def lattice_tiles(
self,
lattice: AbstractLattice | int | str | np.ndarray,
*,
x: int | slice | None = None,
y: int | slice | None = None,
copy: bool = False,
) -> list[Tile]:
"""Return a list of (unique) tiles in a lattice, potentially taking a slice of the lattice.
Parameters
----------
lattice : AbstractLattice | int | str
Lattice or reference to a lattice in the tileset
x : int | slice | None, optional
index in the lattice, by default None
y : int | slice | None, optional
index in the lattice, by default None
copy : bool, optional
return copies if True (useful for creating a new set) or tiles in the set if False (useful for modifying the set), by default False
"""
if isinstance(lattice, (int, str)):
lattice = cast(AbstractLattice, self.lattices[lattice])
elif not isinstance(lattice, AbstractLattice):
lattice = AbstractLattice(lattice)
tilenames = np.unique(lattice.grid[x, y])
if copy:
return [self.tiles[t].copy() for t in tilenames]
else:
return [self.tiles[t] for t in tilenames]
[docs] def create_guards_square(
self,
lattice: AbstractLattice,
square_size: int,
init_x: int = 0,
init_y: int = 0,
skip: Callable[[Glue], bool] = _skip_polyT_and_inertname,
) -> list[str]:
glues: set[str] = set()
for xi in range(init_x, lattice.grid.shape[0], square_size):
glues.update(
g.ident()
for tile in lattice.grid[xi, :]
if isinstance(self.tiles[tile], SupportsGuards)
for g in self.tiles[tile].create_guards("S")
if not skip(g)
)
for yi in range(init_y, lattice.grid.shape[1], square_size):
glues.update(
g.ident()
for tile in lattice.grid[:, yi]
if isinstance(self.tiles[tile], SupportsGuards)
for g in self.tiles[tile].create_guards("E")
if not skip(g)
)
return list(glues)
[docs] def create_abstract_diagram(
self,
lattice: AbstractLattice | str | int | np.ndarray | None,
filename=None,
scale=1,
guards: Collection[str] | str | int = tuple(),
seed: str | bool | Seed = True,
seed_offset: tuple[int, int] = (0, 0),
**options,
):
"""Create an SVG layout diagram from a lattice.
This currently uses the abstract diagram bases to create the
layout diagrams.
Parameters
----------
xgrowarray : ndarray or dict
Xgrow output. This may be a numpy array of
an xgrow state, obtained in some way, or may be the 'array'
output of xgrow.run.
filename : string
File name / path of the output file.
"""
if isinstance(lattice, str) or isinstance(lattice, int):
lt = self.lattices[lattice]
assert isinstance(lt, AbstractLattice)
lattice = lt
elif lattice is None:
lt = next(iter(self.lattices.values()))
assert isinstance(lt, AbstractLattice)
lattice = lt
elif not isinstance(lattice, AbstractLattice):
lattice = AbstractLattice(lattice)
if isinstance(guards, str) or isinstance(guards, int):
guards = self.guards[guards]
d = drawing.Drawing(600, 600)
svgtiles = {}
for tile in self.tiles:
svgtiles[tile.name] = tile.abstract_diagram(**options)
d.defs.append(svgtiles[tile.name])
minxi = 10000
minyi = 10000
maxxi = 0
maxyi = 0
for (yi, xi), tn in np.ndenumerate(lattice.grid):
if not tn in svgtiles.keys():
continue
minxi = min(minxi, xi)
minyi = min(minyi, yi)
maxxi = max(maxxi, xi)
maxyi = max(maxyi, yi)
d.elements.append(drawing.Use(svgtiles[tn], xi * 10, yi * 10))
if seed is True:
try:
seed = next(iter(self.seeds.values()))
except StopIteration:
seed = False
elif isinstance(seed, str):
seed = self.seeds[seed]
# if len(guards) > 0:
# for (yi, xi), tn in np.ndenumerate(lattice.grid):
# if tn == "":
# continue
# t = self.tiles[tn]
# for g, pos in zip(t.edges, t.edge_locations): # FIXME: deal with duples
# if g.complement.ident() in guards:
# d.elements.append(
# drawing.Line(
# xi * 10 + _gl[pos][0],
# yi * 10 + _gl[pos][1],
# xi * 10 + _gl[pos][2],
# yi * 10 + _gl[pos][3],
# stroke="black",
# stroke_width=2.5,
# )
# )
# d.elements.append(
# drawing.Line(
# xi * 10 + _gl[pos][0],
# yi * 10 + _gl[pos][1],
# xi * 10 + _gl[pos][2],
# yi * 10 + _gl[pos][3],
# stroke="red",
# stroke_width=1.0,
# )
# )
d.viewBox = (
minxi * 10,
minyi * 10,
(2 + maxxi - minxi) * 10,
(2 + maxyi - minyi) * 10,
)
# d.pixelScale = 3
if filename:
d.save_svg(filename)
else:
return d
[docs] def reduce_tiles(
self,
preserve=("s22", "ld"),
tries=10,
threads=1,
returntype="equiv",
best=1,
key=None,
initequiv=None,
):
"""
Apply tile reduction algorithm, preserving some set of properties, and using a multiprocessing pool.
Parameters
----------
tileset: TileSet
The system to reduce.
preserve: a tuple or list of strings, optional
The properties to preserve. Currently supported are 's1' for first order
sensitivity, 's2' for second order sensitivity, 's22' for two-by-two sensitivity,
'ld' for small lattice defects, and 'gs' for glue sense (to avoid spurious
hierarchical attachment). Default is currently ('s22', 'ld').
tries: int, optional
The number of times to run the algorithm.
threads: int, optional
The number of threads to use (using multiprocessing).
returntype: 'TileSet' or 'equiv' (default 'equiv')
The type of object to return. If 'equiv', returns an array of glue equivalences
(or list, if best != 1) that can be applied to the tileset with apply_equiv, or used
for further reduction. If 'TileSet', return a TileSet with the equiv already applied
(or a list, if best != 1).
best: int or None, optional
The number of systems to return. If 1, the result will be returned
directly; if k > 1, a list will be returned of the best k results (per cmp);
if k = None, a list of *all* results will be returned, sorted by cmp. (default 1)
key: function (ts, equiv1, equiv2) -> some number/comparable
A comparison function for equivs, to sort the results. FIXME: documentation needed.
Default (if None) here is to sort by number of glues in the system, regardless of number
of tiles.
initequiv: equiv
If provided, the equivalence array to start from. If None, start from the tileset without
any merged glues.
Returns
-------
reduced: single TileSet or equiv, or list
The reduced system/systems
"""
raise NotImplementedError
# from fastreduceD import fastreduce
# return fastreduce.reduce_tiles(
# self, preserve, tries, threads, returntype, best, key, initequiv
# )
[docs] def reduce_ends(
self,
preserve=["s22", "ld"],
tries=10,
threads=1,
returntype="equiv",
best=1,
key=None,
initequiv=None,
):
"""
Apply end reduction algorithm, preserving some set of properties, and using a multiprocessing pool.
Parameters
----------
tileset: TileSet
The system to reduce.
preserve: a tuple or list of strings, optional
The properties to preserve. Currently supported are 's1' for first order
sensitivity, 's2' for second order sensitivity, 's22' for two-by-two sensitivity,
'ld' for small lattice defects, and 'gs' for glue sense (to avoid spurious
hierarchical attachment). Default is currently ('s22', 'ld').
tries: int, optional
The number of times to run the algorithm.
threads: int, optional
The number of threads to use (using multiprocessing).
returntype: 'TileSet' or 'equiv' (default 'equiv')
The type of object to return. If 'equiv', returns an array of glue equivalences
(or list, if best != 1) that can be applied to the tileset with apply_equiv, or used
for further reduction. If 'TileSet', return a TileSet with the equiv already applied
(or a list, if best != 1).
best: int or None, optional
The number of systems to return. If 1, the result will be returned
directly; if k > 1, a list will be returned of the best k results (per cmp);
if k = None, a list of *all* results will be returned, sorted by cmp. (default 1)
key: function (ts, equiv1, equiv2) -> some number/comparable
A comparison function for equivs, to sort the results. FIXME: documentation needed.
Default (if None) here is to sort by number of glues in the system, regardless of number
of tiles.
initequiv: equiv
If provided, the equivalence array to start from. If None, start from the tileset without
any merged glues.
Returns
-------
reduced: single TileSet or equiv, or list
The reduced system/systems
"""
raise NotImplementedError
# from fastreduceD import fastreduce
# return fastreduce.reduce_ends(
# self, preserve, tries, threads, returntype, best, key, initequiv
# )
[docs] def latticedefects(self, direction="e", depth=2, pp=True, rotate=False):
"""
Calculate and show possible small lattice defect configurations.
"""
raise NotImplementedError
# from . import latticedefect
# return latticedefect.latticedefects(
# self, direction=direction, depth=depth, pp=pp, rotate=rotate
# )
# FIXME: disabled temporarily for mypy main branch
from ._tilesets_dx import ( # type: ignore
dx_plot_adjacent_regions,
dx_plot_se_hists,
dx_plot_se_lv,
dx_plot_side_strands,
) # type: ignore
from .nuad import tileset_to_nuad_design as to_nuad_design # type: ignore
from .nuad import update_nuad_design as update_nuad_design # type: ignore
from .nuad import load_nuad_design as load_nuad_design # type: ignore
[docs] def apply_equiv(self, equiv):
"""
Apply an equivalence array (from, eg, `TileSet.reduce_ends` or `TileSet.reduce_tiles`).
Parameters
----------
equiv : ndarray
An equivalence array, *for this tileset*, generated by reduction functions.
Returns
-------
TileSet
A tileset with the equivalence array, and thus the reduction, applied.
"""
raise NotImplementedError
# return fastreduce._FastTileSet(self).applyequiv(self, equiv)
[docs] def check_consistent(self):
"""Check the TileSet consistency.
Check a number of properties of the TileSet for consistency.
In particular:
* Each tile must pass Tile.check_consistent()
* TileSet.ends and TileSet.tiles.endlist() must not contain conflicting
ends or end sequences.
* If there is a seed:
* It must be of an understood type (it must be in seeds.seedtypes)
* All adapter locations must be valid.
* The seed must pass its check_consistent and check_sequence.
"""
# * END LIST The end list itself must be consistent.
# ** Each end must be of understood type
# ** Each end must have a valid sequence or no sequence
# ** There must be no more than one instance of each name
# ** WARN if there are ends with no namecounts
# * TILE LIST
# ** each tile must be of understood type (must parse)
# ** ends in the tile list must be consistent (must merge)
# ** there must be no more than one tile with each name
# self.tiles.check_consistent()
endsfromtiles = self.tiles.glues_from_tiles()
# ** WARN if any end that appears does not have a complement used or vice versa
# ** WARN if there are tiles with no name
# * TILE + END
# ** The tile and end lists must merge validly
# (checks sequences, adjacents, types, complements)
self.glues | endsfromtiles
# ** WARN if tilelist has end references not in ends
# ** WARN if merge is not equal to the endlist
# ** WARN if endlist has ends not used in tilelist
# * ADAPTERS / SEEDS
# SEED stuff was here
[docs] def copy(self):
"""Return a full (deep) copy of the TileSet"""
return copy.deepcopy(self)
@classmethod
[docs] def from_file(
cls,
path_or_stream: TextIOWrapper | str | PathLike[str],
format: Literal["json", "yaml", None] = None,
) -> "TileSet":
if isinstance(path_or_stream, str) or isinstance(path_or_stream, PathLike):
p = Path(path_or_stream)
if format is None:
if p.suffix == ".json":
format = "json"
elif p.suffix in [".yaml", ".yml"]:
format = "yaml"
stream = p.open("r")
if format is None:
log.warning("No format specified, trying json, then yaml.")
try:
return cls.from_json(stream)
except:
stream.seek(0)
return cls.from_yaml(stream)
if format == "json":
return cls.from_json(stream)
elif format == "yaml":
return cls.from_yaml(stream)
else:
raise ValueError(f"Unknown format {format}")
[docs] def to_file(self, path_or_stream: str | PathLike[str] | TextIOWrapper):
if isinstance(path_or_stream, (str, PathLike)):
stream = open(path_or_stream, "w")
else:
stream = path_or_stream
return self.to_json(stream)