Source code for alhambra.nuad

from __future__ import annotations
from typing import (
    TYPE_CHECKING,
    Callable,
    Dict,
    Iterable,
    Literal,
    Mapping,
    Optional,
    Tuple,
    cast,
)
import itertools
import logging

from alhambra.glues import GlueList, SSGlue
from alhambra.tiles import BaseSSTile
from alhambra.tiles import Tile, BaseSSTSingle

if TYPE_CHECKING:
    from alhambra.tilesets import TileSet
    import nuad.constraints as nc

[docs]log = logging.getLogger(__name__)
[docs]def load_nuad_design( tileset: "TileSet", design: "nc.Design", update_tiles: bool = True, inplace: bool = False, ) -> "TileSet": try: import nuad.constraints as nc except ImportError: raise ImportError("nuad must be installed for this function.") if not inplace: new_ts = tileset.copy() else: new_ts = tileset for domain in design.domains: new_ts.glues.add( SSGlue(domain.name, sequence=domain.sequence()) ) # FIXME: determine domain type from pool if update_tiles: for t in new_ts.tiles: t.update_glues(new_ts.glues) for strand in design.strands: # FIXME: handle multi-strand tiles, etc # FIXME: handle non-update case if strand.name in new_ts.tiles: tile: BaseSSTile = new_ts.tiles[strand.name] assert isinstance(tile, BaseSSTile) assert str(tile.sequence) == strand.sequence( delimiter="-" ) # FIXME: should not need str here. return new_ts
[docs]def update_nuad_design( tileset: "TileSet", old_design: "nc.Design", groups: Literal["structure"] | Mapping[str, str] | Callable[[Tile], str] = "structure", ) -> "nc.Design": """ From an Alhambra tileset and an existing Nuad Design. IN-PLACE """ try: import nuad.constraints as nc except ImportError: raise ImportError("nuad must be installed for this function.") # Determine group-setting function if groups == "structure": get_group = lambda tile: tile.structure elif isinstance(groups, Mapping): get_group = lambda tile: cast(Mapping, groups)[tile] else: get_group = groups def filter_strand(s: "nc.Strand"): if s.name not in tileset.tiles.asdict(): log.info(f"Removing tile {s.name} from existing design.") return False return True old_design.compute_derived_fields() old_design.strands = [s for s in old_design.strands if filter_strand(s)] strands_by_name = {s.name: s for s in old_design.strands} pools = {(pool.name, pool.length): pool for pool in old_design.domain_pools()} new_ncdomains: dict[str, "nc.Domain"] = dict() for tile in tileset.tiles: if tile.ident() in strands_by_name: continue log.info(f"Adding new tile {tile.ident()} to existing design.") strand_group = get_group(tile) td: list[SSGlue] = tile.domains ncdomains = [] for domain in td: try: ncdomains.append(old_design.domains_by_name[domain.basename()]) except KeyError: try: ncdomains.append(new_ncdomains[domain.basename()]) except KeyError: log.info(f"Adding domain {domain.basename()}.") ncdomain = nc.Domain(domain.basename()) if domain.sequence.is_definite: ncdomain.set_fixed_sequence(domain.sequence.base_str) else: assert domain.sequence.is_null try: pool = pools[ (f"SSGlue{domain.dna_length}", domain.dna_length) ] # FIXME: determine type except KeyError: pool = nc.DomainPool( f"SSGlue{domain.dna_length}", domain.dna_length ) # FIXME: determine type pools[ (f"SSGlue{domain.dna_length}", domain.dna_length) ] = pool # FIXME: determine type ncdomain.pool = pool new_ncdomains[ncdomain.name] = ncdomain ncdomains.append(ncdomain) old_design.strands.append( nc.Strand( domains=ncdomains, starred_domain_indices=[i for i, d in enumerate(td) if d.is_complement], name=tile.name, group=strand_group, ) ) # Nuad needs only non-complementary (ie, non-starred) domains. Alhambra may contain # complementary domains that don't have a non-complementary counterpart. So we'll need # to compile these. FIXME: move out to other function? non_complementary_domains: GlueList[SSGlue] = GlueList() for domain in tileset.alldomains: # We only want domains that actually have sequences, and can be designed. # FIXME: should have more general sequence-containing class if not isinstance(domain, SSGlue): log.warning(f"Not adding domain {domain.name} to Nuad design: {domain}") continue if domain.is_complement: non_complementary_domains.add(domain.complement) else: non_complementary_domains.add(domain) old_design.compute_derived_fields() for ncdomain in old_design.domains: if not ncdomain.has_pool(): log.warn(f"Domain {ncdomain.name} has no pool.") else: domain = non_complementary_domains[ncdomain.name] if ncdomain.pool.length != domain.dna_length: try: ncdomain._pool = pools[ (f"SSGlue{domain.dna_length}", domain.dna_length) ] except KeyError: ncdomain._pool = nc.DomainPool( f"SSGlue{domain.dna_length}", domain.dna_length ) # FIXME: determine type pools[ (f"SSGlue{domain.dna_length}", domain.dna_length) ] = ncdomain._pool # FIXME: determine type return old_design
[docs]def tileset_to_nuad_design( tileset: "TileSet", groups: Literal["structure"] | Mapping[str, str] | Callable[[Tile], str] = "structure", ) -> "nc.Design": """ From an Alhambra tileset, generate a Nuad Design with all of its domains. groups: Methods for setting the group of each strand. """ try: import nuad.constraints as nc except ImportError: raise ImportError("nuad must be installed for this function.") # Determine group-setting function if groups == "structure": get_group = lambda tile: tile.structure elif isinstance(groups, Mapping): get_group = lambda tile: cast(Mapping, groups)[tile] else: get_group = groups # Convert all tiles to strands. strands = [] for tile in tileset.tiles: strand_group = get_group(tile) strands.append( nc.Strand( [domain.ident() for domain in tile.domains], name=tile.name, group=strand_group, ) ) # Nuad needs only non-complementary (ie, non-starred) domains. Alhambra may contain # complementary domains that don't have a non-complementary counterpart. So we'll need # to compile these. FIXME: move out to other function? non_complementary_domains: GlueList[SSGlue] = GlueList() for domain in tileset.alldomains: # We only want domains that actually have sequences, and can be designed. # FIXME: should have more general sequence-containing class if not isinstance(domain, SSGlue): log.warning(f"Not adding domain {domain.name} to Nuad design: {domain}") continue if domain.is_complement: non_complementary_domains.add(domain.complement) else: non_complementary_domains.add(domain) pools: dict[tuple[str, int], nc.DomainPool] = {} des = nc.Design(strands=strands) ncdomains = [] for ncdomain in des.domains: domain = non_complementary_domains[ncdomain.name] if domain.sequence.is_definite: ncdomain.set_fixed_sequence(domain.sequence.base_str) else: assert domain.sequence.is_null try: pool = pools[ (f"SSGlue{domain.dna_length}", domain.dna_length) ] # FIXME: determine type except KeyError: pool = nc.DomainPool( "SSGlue", domain.dna_length ) # FIXME: determine type pools[ (f"SSGlue{domain.dna_length}", domain.dna_length) ] = pool # FIXME: determine type ncdomain.pool = pool ncdomains.append(ncdomain) # FIXME: do we need to do something with the pools? des.store_domain_pools() return des
[docs]def group_strand_pairs_by_groups_and_complementary_domains( design: "nc.Design", strands: Optional["Optional[Iterable[nc.Strand]]"] = None ) -> "Dict[Tuple[str, str, int], list[Tuple[nc.Strand, nc.Strand]]]": """ Group pairs of strands by their groups (sorted) and number of complementary domains. """ try: import nuad.constraints as nc except ImportError: raise ImportError("nuad must be installed for this function.") pairs: Dict[Tuple[str, str, int], list[Tuple[nc.Strand, nc.Strand]]] = {} if strands is None: strands = design.strands for strand1, strand2 in itertools.combinations_with_replacement(strands, 2): domains1_unstarred = strand1.unstarred_domains_set() domains2_unstarred = strand2.unstarred_domains_set() domains1_starred = strand1.starred_domains_set() domains2_starred = strand2.starred_domains_set() comp_domains = (domains1_unstarred & domains2_starred) | ( domains2_unstarred & domains1_starred ) comp_domain_names = [domain.name for domain in comp_domains] num_comp_domains = len(comp_domain_names) g1sorted, g2sorted = tuple(sorted([strand1.group, strand2.group])) key = (g1sorted, g2sorted, num_comp_domains) if key not in pairs: pairs[key] = [] pairs[key].append((strand1, strand2)) return pairs