Source code for basicsynbio.cam.main

"""Module contains key objects for computer assisted manufacturing (CAM)
within the BASIC DNA assembly framework.
"""

from Bio.Seq import Seq
from basicsynbio.main import (
    BasicAssembly,
    BasicLinker,
    BasicPart,
    BasicUTRRBSLinker,
    LinkerException,
)
from collections import OrderedDict, Counter
import json
from typing import Dict, Generator
import re


[docs]class BasicBuild: """Class provides methods and attributes for building BasicAssembly objects. Attributes: basic_assemblies: Tuple of BasicAssembly objects used to initiate BasicBuild. unique_clips: Tuple of unique ClipReaction objects required to implement the build. clips_data: A dictionary with each ClipReaction as a key and BasicAssembly objects that require it as values. unique_parts: Tuple of the unique BasicPart object required to implement the build. unique_linkers: Tuple of the unique BasicLinker objects required to implement the build. """ def __init__(self, *basic_assemblies: BasicAssembly): """Initiate BasicBuild. Args: *basic_assemblies: BasicAssembly objects. """ self.basic_assemblies = basic_assemblies
[docs] def update_parts(self, *parts: BasicPart) -> None: """Updates BasicBuild instance with parts. Replaces all existing BasicParts used in assemblies with the matching equivalent in parts. Args: parts: parts to replace the BasicParts used in assemblies Raises: ValueError: if length parts is not equal to length this build's unique parts """ if len(parts) != len(self.unique_parts_data): raise ValueError( f"length of *parts is {len(parts)} whereas self.unqiue_parts has {len(self.unique_parts_data)} elements. The two must match." ) parts_dict = self._unique_parts_data(*parts) basic_assemblies = [] for assembly in self.basic_assemblies: parts_linkers = [ part_linker if isinstance(part_linker, BasicLinker) else parts_dict[part_linker.seq]["part"] for part_linker in assembly.parts_linkers ] basic_assemblies.append(BasicAssembly(assembly.id, *parts_linkers)) self.__init__(*basic_assemblies)
def _return_clips_data(self) -> dict: """Analyses the build and returns a dictionary describing ClipReactions and their associated assemblies. Returns: clips_dict: each ClipReaction as a key with values describing associated assemblies. """ clips_dict = OrderedDict( **{ clip_reaction: [] for assembly in self.basic_assemblies for clip_reaction in assembly._clip_reactions } ) for assembly in self.basic_assemblies: for clip_reaction in assembly._clip_reactions: clips_dict[clip_reaction].append(assembly) return clips_dict def _unique_parts_data(self, *parts: BasicPart) -> Dict[Seq, dict]: """Returns a dictionary of unique objects in parts. Includes an empty list for each item to populate with clip_reactions used by each unique part. Args: parts: collection of parts present within build used to search through for uniqueness. Returns: dict: seq attribute of part along with a sub-dictionary containing part and empty list for populating associated clip_reactions. """ return { part.seq: { "part": part, "clip reactions": [], } for part in parts } def _unique_linkers_data(self, *linkers: BasicLinker) -> Dict[Seq, dict]: """Returns a dictionary of unique objects in linkers. Includes an empty lists to populate with clip_reactions using prefix or suffix linkers. Args: linkers: collection of linkers present within build used to search through for uniqueness. Returns: dict: seq attribute of linker along with a sub-dictionary containing linker and empty list for populating associated clip_reactions. """ return { linker.seq: { "linker": linker, "prefix clip reactions": [], "suffix clip reactions": [], } for linker in linkers } def _set_unique_parts_linkers_data(self) -> None: """sets unique_part_data and unique_linker_data attributes.""" self.unique_parts_data = self._unique_parts_data( *(clip_reaction._part for clip_reaction in self.clips_data) ) self.unique_linkers_data = self._unique_linkers_data( *(clip_reaction._prefix for clip_reaction in self.clips_data) ) for clip_reaction in self.unique_clips: self.unique_parts_data[clip_reaction._part.seq]["clip reactions"].append( clip_reaction ) self.unique_linkers_data[clip_reaction._prefix.seq][ "prefix clip reactions" ].append(clip_reaction) self.unique_linkers_data[clip_reaction._suffix.seq][ "suffix clip reactions" ].append(clip_reaction) def _duplicate_assembly_ids(self, assemblies): assemblies_ids = [assembly.id for assembly in assemblies] if len(set(assemblies_ids)) < len(assemblies): top_assembly_id = Counter(assemblies_ids).most_common(1)[0] raise BuildException( f"ID '{top_assembly_id[0]}' has been assigned to {top_assembly_id[1]} BasicAssembly instance/s. All assemblies of a build should have a unique 'id' attribute." ) @property def basic_assemblies(self): return self._basic_assemblies @basic_assemblies.setter def basic_assemblies(self, values): if not all(issubclass(type(value), BasicAssembly) for value in values): raise TypeError("Not all *basic_assemblies are BasicAssembly instances.") self._duplicate_assembly_ids(values) self._basic_assemblies = values self.clips_data = self._return_clips_data() self.unique_clips = tuple(clip for clip in self.clips_data.keys()) self._set_unique_parts_linkers_data() self.unique_parts = tuple( part_dict["part"] for part_dict in self.unique_parts_data.values() ) self.unique_linkers = tuple( linker_dict["linker"] for linker_dict in self.unique_linkers_data.values() )
[docs]class BuildEncoder(json.JSONEncoder): """A Class to encode BasicBuild objects extending `json.JSONEncoder` class"""
[docs] def default(self, obj): if isinstance(obj, BasicBuild): return { "unique_parts": self.unique_parts_json(obj), "unique_linkers": self.unique_linkers_json(obj), "clips_data": self.clips_data_json(obj), "assembly_data": self.assembly_data_json(obj), "__BasicBuild__": True, } return super().default(obj)
[docs] @staticmethod def unique_parts_json(obj): """A function to create machine readable json objects, completely describing unique parts. Args: obj: BasicBuild object to be decoded Returns: dictionary: Each item describes a unique BasicPart object required for the build. """ return { "UP" + str(index): { "sequence": str(value["part"].seq), "id": value["part"].id, "name": value["part"].name, "description": value["part"].description, "Part mass for 30 μL clip reaction (ng)": value["part"].clip_mass(), "clip reactions": [ "CR" + str(list(obj.clips_data.keys()).index(clip_reaction)) for clip_reaction in value["clip reactions"] ], } for index, value in enumerate(obj.unique_parts_data.values()) }
[docs] @staticmethod def unique_linkers_json(obj): """A function to create machine readable json objects, completely describing unique linkers. Args: obj: BasicBuild object to be decoded Returns: dictionary: Each item describes a given unique BasicLinker object required for the build. """ return { "UL" + str(index): { "id": value["linker"].id, "linker_class": str(type(value["linker"])), "name": value["linker"].name, "sequence": str(value["linker"].seq), "prefix_id": value["linker"].prefix_id, "suffix_id": value["linker"].suffix_id, "prefix clip reactions": [ "CR" + str(list(obj.clips_data.keys()).index(clip_reaction)) for clip_reaction in value["prefix clip reactions"] ], "suffix clip reactions": [ "CR" + str(list(obj.clips_data.keys()).index(clip_reaction)) for clip_reaction in value["suffix clip reactions"] ], } for index, value in enumerate(obj.unique_linkers_data.values()) }
[docs] @staticmethod def clips_data_json(obj): """A function to create machine readable json objects, completely describing ClipReaction objects within BasicBuild. Args: obj: BasicBuild object to be decoded Returns: dictionary: Each item describes a given unique ClipReaction object required for the build. """ linker_seqs = [linker_seq for linker_seq in obj.unique_linkers_data.keys()] part_seqs = [part_seq for part_seq in obj.unique_parts_data.keys()] return { "CR" + str(index): { "prefix": { "key": "UL" + str(linker_seqs.index(value[0]._prefix.seq)), "prefix_id": value[0]._prefix.prefix_id, }, "part": { "key": "UP" + str(part_seqs.index(value[0]._part.seq)), "id": value[0]._part.id, "name": value[0]._part.name, }, "suffix": { "key": "UL" + str(linker_seqs.index(value[0]._suffix.seq)), "suffix_id": value[0]._suffix.suffix_id, }, "total assemblies": len(value[1]), "assembly keys": [ "A" + str(obj.basic_assemblies.index(assembly)) for assembly in value[1] ], } for index, value in enumerate(obj.clips_data.items()) }
[docs] @staticmethod def assembly_data_json(obj): """A function to create machine readable json objects, completely describing BasicAssembly objects within BasicBuild. Args: obj: BasicBuild object to be decoded Returns: list: returns a list of dictionaries populated by dictionaries with items describing BasicAssemblies generated by the build. """ return { "A" + str(index): { "id": assembly.id, "clip reactions": [ "CR" + str(list(obj.clips_data.keys()).index(clip_reaction)) for clip_reaction in assembly._clip_reactions ], } for index, assembly in enumerate(obj.basic_assemblies) }
[docs]class BuildDecoder(json.JSONDecoder): """A Class to decode json dictionary to basicsynbio objects, extending `json.JSONDecoder` class """ def __init__(self): json.JSONDecoder.__init__(self, object_hook=self.decode_build)
[docs] def decode_build(self, dictionary: dict) -> BasicBuild: """A method to return BasicBuild from encoded json object Args: dictionary: json object encoded by BuildEncoder Returns: BasicBuild: BasicBuild object built from encoded json """ if "__BasicBuild__" in dictionary: self.unique_parts_data = self.return_unqiue_parts(dictionary) self.unique_linkers_data = self.return_unique_linkers(dictionary) basic_assemblies = self.return_basic_assemblies(dictionary) return BasicBuild(*basic_assemblies) return dictionary
[docs] def return_basic_assemblies( self, dictionary: dict ) -> Generator[BasicAssembly, None, None]: """A method to yield BasicAssembly objects from encoded json object Args: dictionary: json object encoded by BuildEncoder Yields: BasicAssembly: BasicAssembly objects within encoded BasicBuild """ for assembly in dictionary["assembly_data"].values(): parts_linkers = [] for clip_reaction in assembly["clip reactions"]: parts_linkers += [ self.unique_linkers_data[ dictionary["clips_data"][clip_reaction]["prefix"]["key"] ], self.unique_parts_data[ dictionary["clips_data"][clip_reaction]["part"]["key"] ], ] print("part/linkers", parts_linkers) yield BasicAssembly(assembly["id"], *parts_linkers)
[docs] @staticmethod def return_unqiue_parts(dictionary): """A method to return unique BasicPart objects from encoded json object Args: dictionary: json object encoded by BuildEncoder Returns: dictionary: containing unique BasicPart objects """ return { key: BasicPart( seq=Seq(value["sequence"]), id=value["id"], name=value["name"], description=value["description"], ) for key, value in dictionary["unique_parts"].items() }
[docs] @staticmethod def return_unique_linkers(dictionary): """A method to return unqiue BasicLinker objects from encoded json object Args: dictionary: json object encoded by BuildEncoder Returns: dictionary: containing unique BasicLinker objects """ unique_linkers = {} for key, value in dictionary["unique_linkers"].items(): if re.match(".*BasicLinker", value["linker_class"]): unique_linker = BasicLinker( seq=Seq(value["sequence"]), id=value["id"], name=value["name"], ) elif re.match(".*BasicUTRRBSLinker", value["linker_class"]): unique_linker = BasicUTRRBSLinker( seq=Seq(value["sequence"]), id=value["id"], name=value["name"], ) else: raise LinkerException( f"unique linker '{key}' does not have a recognised 'linker_class' attribute." ) unique_linker.prefix_id = value["prefix_id"] unique_linker.suffix_id = value["suffix_id"] unique_linkers[key] = unique_linker return unique_linkers
[docs]class BuildException(Exception): pass