class ObjectInstance(CNObject): def __init__(self, reference_object: CNObject, parent=None): super(ObjectInstance, self).__init__(reference_object) self._parent = parent # parent is either a document or assembly self._ref_object = reference_object # self._position = [0, 0, 0, 0, 0, 0] # x, y, z, phi, theta, psi self._properties = {} # end def # SIGNALS # instanceDestroyedSignal = ProxySignal(CNObject, name="instanceDestroyedSignal") instancePropertyChangedSignal = ProxySignal(CNObject, name="instancePropertyChangedSignal") instanceParentChangedSignal = ProxySignal(CNObject, name="instanceParentChangedSignal") # SLOTS # # METHODS # def destroy(self): self.setParent(None) self.deleteLater() # end def def reference(self) -> CNObject: return self._ref_object # end def def parent(self): return self._parent # end def def properties(self) -> dict: return self._properties.copy() # end def def setProperty(self, key: str, val: Any): self._properties[key] = val # end def def getProperty(self, key: str) -> Any: return self._properties[key] # end def def shallowCopy(self) -> ObjectInstanceT: oi = ObjectInstance(self._ref_object, self._parent) oi._properties = oi._properties.copy() return oi # end def def deepCopy(self, reference_object: CNObject, parent) -> ObjectInstanceT: oi = ObjectInstance(reference_object, parent) oi._properties = oi._properties.copy() return oi
class StrandSet(CNObject): """:class:`StrandSet` is a container class for :class:`Strands`, and provides the several publicly accessible methods for editing strands, including operations for creation, destruction, resizing, splitting, and merging strands. Views may also query :class:`StrandSet` for information that is useful in determining if edits can be made, such as the bounds of empty space in which a strand can be created or resized. Internally :class:`StrandSet` uses redundant heap and a list data structures to track :class:`Strands` objects, with the list of length of a virtual helix looking like:: strand_array = [strandA, strandA, strandA, ..., None, strandB, strandB, ...] Where every index strandA spans has a reference to strandA and strand_heap:: strand_heap = [strandA, strandB, strandC, ...] is merely a sorted list from low index to high index of strand objects Args: is_fwd (bool): is this a forward or reverse StrandSet? id_num (int): ID number of the virtual helix this is on part (Part): Part object this is a child of initial_size (int): initial_size to allocate """ def __init__(self, is_fwd: bool, id_num: int, part: NucleicAcidPartT, initial_size: int): self._document = part.document() super(StrandSet, self).__init__(part) self._is_fwd = is_fwd self._is_scaffold = is_fwd if (id_num % 2 == 0) else not is_fwd self._strand_type = StrandEnum.FWD if self._is_fwd else StrandEnum.REV self._id_num = id_num self._part = part self._reset(int(initial_size)) self._undo_stack = None # end def def simpleCopy(self, part: NucleicAcidPartT) -> StrandSetT: """Create an empty copy (no strands) of this strandset with the only a new virtual_helix_group parent TODO: consider renaming this method Args: part (Part): part to copy this into """ return StrandSet(self._is_fwd, self._id_num, part, len(self.strand_array)) # end def def __iter__(self) -> StrandT: """Iterate over each strand in the strands list. Yields: Strand: :class:`Strand` in order from low to high index """ return self.strands().__iter__() # end def def __repr__(self) -> str: if self._is_fwd: st = 'fwd' else: st = 'rev' num = self._id_num return "<%s_StrandSet(%d)>" % (st, num) # end def ### SIGNALS ### strandsetStrandAddedSignal = ProxySignal(CNObject, CNObject, name='strandsetStrandAddedSignal') """pyqtSignal(QObject, QObject): strandset, strand""" ### SLOTS ### ### ACCESSORS ### def part(self) -> NucleicAcidPartT: """Get model :class:`Part` Returns: the :class:`Part` """ return self._part # end def def document(self) -> DocT: """Get model :class:`Document` Returns: the :class:`Document` """ return self._document # end def def strands(self) -> List[StrandT]: """Get raw reference to the strand_heap of this :class:`StrandSet` Returns: the list of strands """ return self.strand_heap def _reset(self, initial_size: int): """Reset this object clearing out references to all :class:`Strand` objects. Exceptional private method to be only used by Parts Args: initial_size: size to revert to """ self.strand_array = [None] * (initial_size) self.strand_heap = [] # end def def resize(self, delta_low: int, delta_high: int): """Resize this StrandSet. Pad each end when growing otherwise don't do anything Args: delta_low: amount to resize the low index end delta_high: amount to resize the high index end """ if delta_low < 0: self.strand_array = self.strand_array[delta_low:] if delta_high < 0: self.strand_array = self.strand_array[:delta_high] self.strand_array = [None] * delta_low + self.strand_array + [ None ] * delta_high # end def ### PUBLIC METHODS FOR QUERYING THE MODEL ### def isForward(self) -> bool: """Is the set 5' to 3' (forward) or is it 3' to 5' (reverse) Returns: ``True`` if is forward, ``False`` otherwise """ return self._is_fwd # end def def strandType(self) -> EnumType: """Store the enum of strand type Returns: :class:`StrandEnum.FWD` if is forward, otherwise :class:`StrandEnum.REV` """ return self._strand_type def isReverse(self) -> bool: """Is the set 3' to 5' (reverse) or 5' to 3' (forward)? Returns: ``True`` if is reverse, ``False`` otherwise """ return not self._is_fwd # end def def isScaffold(self) -> bool: """Is the set (5' to 3' and even parity) or (3' to 5' and odd parity) Returns: ``True`` if is scaffold, ``False`` otherwise """ return self._is_scaffold def isStaple(self) -> bool: """Is the set (5' to 3' and even parity) or (3' to 5' and odd parity) Returns: ``True if is staple, ``False`` otherwise """ return not self._is_scaffold def length(self) -> int: """length of the :class:`StrandSet` and therefore also the associated virtual helix in bases Returns: length of the set """ return len(self.strand_array) def idNum(self) -> int: """Get the associated virtual helix ID number Returns: virtual helix ID number """ return self._id_num # end def def getNeighbors(self, strand: StrandT) -> Tuple[StrandT, StrandT]: """Given a :class:`Strand` in this :class:`StrandSet` find its internal neighbors Args: strand: Returns: of form:: (low neighbor, high neighbor) of types :class:`Strand` or :obj:`None` """ sh = self.strand_heap i = bisect_left(sh, strand) if sh[i] != strand: raise ValueError("getNeighbors: strand not in set") if i == 0: low_strand = None else: low_strand = sh[i - 1] if i == len(sh) - 1: high_strand = None else: high_strand = sh[i + 1] return low_strand, high_strand # end def def complementStrandSet(self) -> StrandSetT: """Returns the complementary strandset. Used for insertions and sequence application. Returns: the complementary :class:`StrandSet` """ fwd_ss, rev_ss = self._part.getStrandSets(self._id_num) return rev_ss if self._is_fwd else fwd_ss # end def def getBoundsOfEmptyRegionContaining(self, base_idx: int) -> Int2T: """Return the bounds of the empty region containing base index <base_idx>. Args: base_idx: the index of interest Returns: tuple of :obj:`int` of form:: (low_idx, high_idx) """ class DummyStrand(object): _base_idx_low = base_idx def __lt__(self, other): return self._base_idx_low < other._base_idx_low ds = DummyStrand() sh = self.strand_heap lsh = len(sh) if lsh == 0: return 0, len(self.strand_array) - 1 # the i-th index is the high-side strand and the i-1 index # is the low-side strand since bisect_left gives the index # to insert the dummy strand at i = bisect_left(sh, ds) if i == 0: low_idx = 0 else: low_idx = sh[i - 1].highIdx() + 1 # would be an append to the list effectively if inserting the dummy strand if i == lsh: high_idx = len(self.strand_array) - 1 else: high_idx = sh[i].lowIdx() - 1 return (low_idx, high_idx) # end def def indexOfRightmostNonemptyBase(self) -> int: """Returns the high base_idx of the last strand, or 0.""" sh = self.strand_heap if len(sh) > 0: return sh[-1].highIdx() else: return 0 # end def def strandCount(self) -> int: """Getter for the number of :class:`Strands` in the set Returns: the number of :class:`Strands` in the set """ return len(self.strand_heap) # end def ### PUBLIC METHODS FOR EDITING THE MODEL ### def createStrand(self, base_idx_low: int, base_idx_high: int, color: str = None, use_undostack: bool = True) -> StrandT: """Assumes a strand is being created at a valid set of indices. Args: base_idx_low: low index of strand base_idx_high: high index of strand color (optional): default is ``None`` use_undostack (optional): default is ``True`` Returns: :class:`Strand` if successful, ``None`` otherwise """ part = self._part # NOTE: this color defaulting thing is problematic for tests if color is None: color = part.getProperty('color') bounds_low, bounds_high = self.getBoundsOfEmptyRegionContaining( base_idx_low) if bounds_low is not None and bounds_low <= base_idx_low and \ bounds_high is not None and bounds_high >= base_idx_high: c = CreateStrandCommand(self, base_idx_low, base_idx_high, color, update_segments=use_undostack) x, y, _ = part.getVirtualHelixOrigin(self._id_num) d = "%s:(%0.2f,%0.2f).%d^%d" % (self.part().getName(), x, y, self._is_fwd, base_idx_low) util.execCommandList(self, [c], desc=d, use_undostack=use_undostack) return c.strand() else: return None # end def def createDeserializedStrand(self, base_idx_low: int, base_idx_high: int, color: str, use_undostack: bool = False) -> int: """Passes a strand to AddStrandCommand that was read in from file input. Omits the step of checking _couldStrandInsertAtLastIndex, since we assume that deserialized strands will not cause collisions. """ c = CreateStrandCommand(self, base_idx_low, base_idx_high, color, update_segments=use_undostack) x, y, _ = self._part.getVirtualHelixOrigin(self._id_num) d = "(%0.2f,%0.2f).%d^%d" % (x, y, self._is_fwd, base_idx_low) util.execCommandList(self, [c], desc=d, use_undostack=use_undostack) return 0 # end def def isStrandInSet(self, strand: StrandT) -> bool: sl = self.strand_array if sl[strand.lowIdx()] == strand and sl[strand.highIdx()] == strand: return True else: return False # end def def removeStrand(self, strand: StrandT, use_undostack: bool = True, solo: bool = True): """Remove a :class:`Strand` from the set Args: strand: the :class:`Strand` to remove use_undostack (optional): default = ``True`` solo ( optional): solo is an argument to enable limiting signals emiting from the command in the case the command is instantiated part of a larger command, default=``True`` """ cmds = [] if not self.isStrandInSet(strand): raise IndexError("Strandset.removeStrand: strand not in set") if strand.sequence() is not None: cmds.append(strand.oligo().applySequenceCMD(None)) cmds += strand.clearDecoratorCommands() cmds.append(RemoveStrandCommand(self, strand, solo=solo)) util.execCommandList(self, cmds, desc="Remove strand", use_undostack=use_undostack) # end def def oligoStrandRemover(self, strand: StrandT, cmds: List[UndoCommand], solo: bool = True): """Used for removing all :class:`Strand`s from an :class:`Oligo` Args: strand: a strand to remove cmds: a list of :class:`UndoCommand` objects to append to solo (:optional): to pass on to ``RemoveStrandCommand``, default=``True`` """ if not self.isStrandInSet(strand): raise IndexError("Strandset.oligoStrandRemover: strand not in set") cmds += strand.clearDecoratorCommands() cmds.append(RemoveStrandCommand(self, strand, solo=solo)) # end def def removeAllStrands(self, use_undostack: bool = True): """Remove all :class:`Strand` objects in the set Args: use_undostack (optional): default=``True`` """ for strand in list(self.strand_heap): self.removeStrand(strand, use_undostack=use_undostack, solo=False) # end def def mergeStrands(self, priority_strand: StrandT, other_strand: StrandT, use_undostack: bool = True) -> bool: """Merge the priority_strand and other_strand into a single new strand. The oligo of priority should be propagated to the other and all of its connections. Args: priority_strand: priority strand other_strand: other strand use_undostack (optional): default=``True`` Returns: ``True`` if strands were merged, ``False`` otherwise """ low_and_high_strands = self.strandsCanBeMerged(priority_strand, other_strand) if low_and_high_strands: strand_low, strand_high = low_and_high_strands if self.isStrandInSet(strand_low): c = MergeCommand(strand_low, strand_high, priority_strand) util.doCmd(self, c, use_undostack=use_undostack) return True else: return False # end def def strandsCanBeMerged(self, strandA, strandB) -> Tuple[StrandT, StrandT]: """Only checks that the strands are of the same StrandSet and that the end points differ by 1. DOES NOT check if the Strands overlap, that should be handled by addStrand Returns: empty :obj:`tuple` if the strands can't be merged if the strands can be merged it returns the strand with the lower index in the form:: (strand_low, strand_high) otherwise ``None`` """ if strandA.strandSet() != strandB.strandSet(): return () if abs(strandA.lowIdx() - strandB.highIdx()) == 1 or \ abs(strandB.lowIdx() - strandA.highIdx()) == 1: if strandA.lowIdx() < strandB.lowIdx(): if not strandA.connectionHigh() and not strandB.connectionLow( ): return strandA, strandB else: if not strandB.connectionHigh() and not strandA.connectionLow( ): return strandB, strandA else: return None # end def def splitStrand(self, strand: StrandT, base_idx: int, update_sequence: bool = True, use_undostack: bool = True) -> bool: """Break strand into two strands. Reapply sequence by default. Args: strand: the :class:`Strand` base_idx: the index update_sequence (optional): whether to emit signal, default=``True`` use_undostack (optional): default=``True`` Returns: ``True`` if successful, ``False`` otherwise TODO consider return strands instead """ if self.strandCanBeSplit(strand, base_idx): if self.isStrandInSet(strand): c = SplitCommand(strand, base_idx, update_sequence) util.doCmd(self, c, use_undostack=use_undostack) return True else: return False else: return False # end def def strandCanBeSplit(self, strand: StrandT, base_idx: int) -> bool: """Make sure the base index is within the strand Don't split right next to a 3Prime end Don't split on endpoint (AKA a crossover) Args: strand: the :class:`Strand` base_idx: the index to split at Returns: ``True`` if can be split, ``False`` otherwise """ # no endpoints lo, hi = strand.idxs() if base_idx == lo or base_idx == hi: return False # make sure the base index within the strand elif lo > base_idx or base_idx > hi: return False elif self._is_fwd: if base_idx - lo > 0 and hi - base_idx > 1: return True else: return False elif base_idx - lo > 1 and hi - base_idx > 0: # reverse return True else: return False # end def def destroy(self): """Destroy this object """ self.setParent(None) self.deleteLater() # QObject will emit a destroyed() Signal # end def ### PUBLIC SUPPORT METHODS ### def strandFilter(self) -> List[str]: """Get the filter type for this set Returns: 'forward' if is_fwd else 'reverse' amd 'scaffold' or 'staple' """ return ["forward" if self._is_fwd else "reverse" ] + ["scaffold" if self._is_scaffold else "staple"] # end def def hasStrandAt(self, idx_low: int, idx_high: int) -> bool: """Check if set has a strand on the interval given Args: idx_low: low index idx_high: high index Returns: ``True`` if strandset has a strand in the region between ``idx_low`` and ``idx_high`` (both included). ``False`` otherwise """ sa = self.strand_array sh = self.strand_heap lsh = len(sh) strand = sa[idx_low] if strand is None: class DummyStrand(object): _base_idx_low = idx_low def __lt__(self, other): return self._base_idx_low < other._base_idx_low ds = DummyStrand() i = bisect_left(sh, ds) while i < lsh: strand = sh[i] if strand.lowIdx() > idx_high: return False elif idx_low <= strand.highIdx(): return True else: i += 1 return False else: return True # end def def getOverlappingStrands(self, idx_low: int, idx_high: int) -> List[StrandT]: """Gets :class:`Strand` list that overlap the given range. Args: idx_low: low index of overlap region idx_high: high index of overlap region Returns: all :class:`Strand` objects in range """ sa = self.strand_array sh = self.strand_heap lsh = len(sh) strand = sa[idx_low] out = [] if strand is None: class DummyStrand(object): _base_idx_low = idx_low def __lt__(self, other): return self._base_idx_low < other._base_idx_low ds = DummyStrand() i = bisect_left(sh, ds) else: out.append(strand) i = bisect_left(sh, strand) + 1 while i < lsh: strand = sh[i] if strand.lowIdx() > idx_high: break elif idx_low <= strand.highIdx(): out.append(strand) i += 1 else: i += 1 return out # end def # def hasStrandAtAndNoXover(self, idx): # """Name says it all # Args: # idx (int): index # Returns: # bool: True if hasStrandAtAndNoXover, False otherwise # """ # sl = self.strand_array # strand = sl[idx] # if strand is None: # return False # elif strand.hasXoverAt(idx): # return False # else: # return True # # end def # def hasNoStrandAtOrNoXover(self, idx): # """Name says it all # Args: # idx (int): index # Returns: # bool: True if hasNoStrandAtOrNoXover, False otherwise # """ # sl = self.strand_array # strand = sl[idx] # if strand is None: # return True # elif strand.hasXoverAt(idx): # return False # else: # return True # # end def def getStrand(self, base_idx: int) -> StrandT: """Returns the :class:`Strand` that overlaps with `base_idx` Args: base_idx: Returns: Strand: :class:`Strand` at `base_idx` if it exists """ try: return self.strand_array[base_idx] except Exception: print(self.strand_array) raise # end def def dump(self, xover_list: list) -> Tuple[List[Int2T], List[str]]: """ Serialize a StrandSet, and append to a xover_list of xovers adding a xover if the 3 prime end of it is founds TODO update this to support strand properties Args: xover_list: A list to append xovers to Returns: tuple of:: (idxs, colors) where idxs is a :obj:`list` of :obj:`tuple`: indices low and high of each strand in the :class:`StrandSet` and colors is a :obj:`list` of color ``str`` """ sh = self.strand_heap idxs = [strand.idxs() for strand in sh] colors = [strand.getColor() for strand in sh] is_fwd = self._is_fwd for strand in sh: s3p = strand.connection3p() if s3p is not None: xover = (strand.idNum(), is_fwd, strand.idx3Prime()) + s3p.dump5p() xover_list.append(xover) return idxs, colors # end def ### PRIVATE SUPPORT METHODS ### def _addToStrandList(self, strand: StrandT, update_segments: bool = True): """Inserts strand into the strand_array at idx Args: strand: the strand to add update_segments (optional): whether to signal default=``True`` """ idx_low, idx_high = strand.idxs() for i in range(idx_low, idx_high + 1): self.strand_array[i] = strand insort_left(self.strand_heap, strand) if update_segments: self._part.refreshSegments(self._id_num) def _updateStrandIdxs(self, strand: StrandT, old_idxs: Int2T, new_idxs: Int2T): """update indices in the strand array/list of an existing strand Args: strand: the strand old_idxs: range (:obj:`int`) to clear new_idxs: range (:obj:`int`) to set to `strand` """ for i in range(old_idxs[0], old_idxs[1] + 1): self.strand_array[i] = None for i in range(new_idxs[0], new_idxs[1] + 1): self.strand_array[i] = strand def _removeFromStrandList(self, strand: StrandT, update_segments: bool = True): """Remove strand from strand_array. Args: strand: the strand update_segments (optional): whether to signal default=``True`` """ self._document.removeStrandFromSelection( strand) # make sure the strand is no longer selected idx_low, idx_high = strand.idxs() for i in range(idx_low, idx_high + 1): self.strand_array[i] = None i = bisect_left(self.strand_heap, strand) self.strand_heap.pop(i) if update_segments: self._part.refreshSegments(self._id_num) def getStrandIndex(self, strand: StrandT) -> Tuple[bool, int]: """Get the 5' end index of strand if it exists for forward strands and the 3' end index of the strand for reverse strands Returns: tuple of form:: (is_existing, index) """ try: ind = self.strand_array.index(strand) return (True, ind) except ValueError: return (False, 0) # end def def _deepCopy(self, virtual_helix: VirtualHelixT): """docstring for deepCopy""" raise NotImplementedError
class Part(CNObject): """A Part is a group of VirtualHelix items that are on the same lattice. Parts are the model component that most directly corresponds to a DNA origami design. Parts are always parented to the document. Parts know about their oligos, and the internal geometry of a part Copying a part recursively copies all elements in a part: StrandSets, Strands, etc PartInstances are parented to either the document or an assembly PartInstances know global position of the part Copying a PartInstance only creates a new PartInstance with the same Part(), with a mutable parent and position field. """ editable_properties = ['name', 'color', 'is_visible', 'grid_type'] def __init__(self, *args, **kwargs): """Sets the parent document, sets bounds for part dimensions, and sets up bookkeeping for partInstances, Oligos, VirtualHelix's, and helix ID number assignment. Args: document uuid: str """ self._document = document = kwargs.get('document', None) super(Part, self).__init__(document) self._instance_count = 0 self._instances = set() # Properties # TODO document could be None self._group_properties = { 'name': "Part%d" % len(document.children()), 'color': "#0066cc", # outlinerview will override from styles 'is_visible': True } self.uuid: str = kwargs['uuid'] if 'uuid' in kwargs else uuid4().hex # Selections self._selections = {} if self.__class__ == Part: e = "This class is abstract. Perhaps you want HoneycombPart." raise NotImplementedError(e) # end def def __repr__(self) -> str: cls_name = self.__class__.__name__ return "<%s %s>" % (cls_name, str(id(self))[-4:]) ### SIGNALS ### partZDimensionsChangedSignal = ProxySignal( CNObject, int, int, bool, name='partZDimensionsChangedSignal') """self, id_min, id_max, zoom_to_fit""" partInstanceAddedSignal = ProxySignal(CNObject, name='partInstanceAddedSignal') """self""" partParentChangedSignal = ProxySignal(CNObject, name='partParentChangedSignal') """self""" partRemovedSignal = ProxySignal(CNObject, name='partRemovedSignal') """self""" partPropertyChangedSignal = ProxySignal(CNObject, object, object, name='partPropertyChangedSignal') """self, property_name, new_value""" partSelectedChangedSignal = ProxySignal(CNObject, object, name='partSelectedChangedSignal') """self, is_selected""" partActiveChangedSignal = ProxySignal(CNObject, bool, name='partActiveChangedSignal') """self, is_active""" partInstancePropertySignal = ProxySignal(CNObject, str, str, object, name='partInstancePropertySignal') """self, view, key, val""" partDocumentSettingChangedSignal = ProxySignal( object, str, object, name='partDocumentSettingChangedSignal') """self, key, value""" ### SLOTS ### ### ACCESSORS ### def document(self) -> DocT: """Get this objects Document Returns: Document """ return self._document # end def def setDocument(self, document: DocT): """set this object's Document Args: document (Document): """ self._document = document # end def def _canRemove(self) -> bool: """If _instance_count == 1 you could remove the part Returns: bool """ return self._instance_count == 1 # end def def _canReAdd(self) -> bool: """If _instance_count == 0 you could re-add the part Returns: bool """ return self._instance_count == 0 # end def def _incrementInstance(self, document: DocT, obj_instance: ObjectInstance): """Increment the instances of this reference object Args: document: obj_instance: """ self._instance_count += 1 self._instances.add(obj_instance) self._document = document document.addInstance(obj_instance) if self._instance_count == 1: document.addRefObj(self) # end def def _decrementInstance(self, obj_instance: ObjectInstance): """Decrement the instances of this reference object Args: obj_instance: """ ic = self._instance_count self._instances.remove(obj_instance) document = self._document document.removeInstance(obj_instance) if ic == 0: raise IndexError("Can't have less than zero instance of a Part") ic -= 1 if ic == 0: document.removeRefObj(self) self._document = None self._instance_count = ic return ic # end def def instanceProperties(self) -> dict: """ Generator yielding all instance properties """ for instance in self._instances: yield instance.properties() # end def def setInstanceProperty(self, part_instance: ObjectInstance, key: str, value: Any): """Set an instance property Args: part_instance (ObjectInstance): key (str): value (object): """ part_instance.setProperty(key, value) # end def def getInstanceProperty(self, part_instance: ObjectInstance, key: str) -> Any: """Get an instance property Args: part_instance (ObjectInstance): key (str): Returns: an object """ return part_instance.getProperty(key) # end def def changeInstanceProperty(self, part_instance: ObjectInstance, view: str, key: str, value: Any, use_undostack: bool = True): c = ChangeInstancePropertyCommand(self, part_instance, view, key, value) util.doCmd(self, c, use_undostack=use_undostack) # end def def getModelProperties(self) -> dict: """ Get the dictionary of model properties Returns: group properties """ return self._group_properties # end def def getProperty(self, key: str) -> Any: """ Args: key (str): """ return self._group_properties[key] # end def def getOutlineProperties(self) -> Tuple[str, str, bool]: """Convenience method for getting the properties used in the outlinerview Returns: tuple: (<name>, <color>, <is_visible>) """ props = self._group_properties return props['name'], props['color'], props['is_visible'] # end def def setProperty(self, key: str, value: Any, use_undostack: bool = True): """ Get the value of the key model properties Args: key: value: use_undostack: default is ``True`` """ if key == 'is_visible': self._document.clearAllSelected() if use_undostack: c = SetPropertyCommand([self], key, value) self.undoStack().push(c) else: self._setProperty(key, value) # end def def _setProperty(self, key: str, value: Any, emit_signals: bool = False): self._group_properties[key] = value if emit_signals: self.partPropertyChangedSignal.emit(self, key, value) # end def def getName(self) -> str: return self._group_properties['name'] # end def def getColor(self) -> str: """ Returns: The part's color. Defaults to #0066cc. """ return self._group_properties['color'] # end def def destroy(self): self.setParent(None) self.deleteLater() # QObject also emits a destroyed() Signal
class Strand(CNObject): """A Strand is a continuous stretch of bases that are all in the same StrandSet (recall: a VirtualHelix is made up of two StrandSets). Every Strand has two endpoints. The naming convention for keeping track of these endpoints is based on the relative numeric value of those endpoints (low and high). Thus, Strand has a '_base_idx_low', which is its index with the lower numeric value (typically positioned on the left), and a '_base_idx_high' which is the higher-value index (typically positioned on the right) Strands can be linked to other strands by "connections". References to connected strands are named "_strand5p" and "_strand3p", which correspond to the 5' and 3' phosphate linkages in the physical DNA strand, respectively. Since Strands can point 5'-to-3' in either the low-to-high or high-to-low directions, connection accessor methods (connectionLow and connectionHigh) are bound during the init for convenience. Args: strandset (StrandSet): base_idx_low (int): low index base_idx_high (int): high index oligo (cadnano.oligo.Oligo): optional, defaults to None. """ def __init__(self, strandset, base_idx_low, base_idx_high, oligo=None): self._document = strandset.document() super(Strand, self).__init__(strandset) self._strandset = strandset self._id_num = strandset.idNum() """Keep track of its own segments. Updated on creation and resizing """ self._base_idx_low = base_idx_low # base index of the strand's left bound self._base_idx_high = base_idx_high # base index of the right bound self._oligo = oligo self._strand5p = None # 5' connection to another strand self._strand3p = None # 3' connection to another strand self._sequence = None self.segments = [] self.abstract_sequence = [] # dynamic methods for mapping high/low connection /indices # to corresponding 3Prime 5Prime is_forward = strandset.isForward() if is_forward: self.idx5Prime = self.lowIdx self.idx3Prime = self.highIdx self.connectionLow = self.connection5p self.connectionHigh = self.connection3p self.setConnectionLow = self.setConnection5p self.setConnectionHigh = self.setConnection3p else: self.idx5Prime = self.highIdx self.idx3Prime = self.lowIdx self.connectionLow = self.connection3p self.connectionHigh = self.connection5p self.setConnectionLow = self.setConnection3p self.setConnectionHigh = self.setConnection5p self._is_forward = is_forward # end def def __repr__(self): s = "%s.<%s(%s, %s)>" % (self._strandset.__repr__(), self.__class__.__name__, self._base_idx_low, self._base_idx_high) return s # end def def __lt__(self, other): return self._base_idx_low < other._base_idx_low def generator5pStrand(self): """Iterate from self to the final _strand5p is None 3' to 5' Includes originalCount to check for circular linked list Yields: Strand: 5' connected :class:`Strand` """ node0 = node = self f = attrgetter('_strand5p') while node: yield node # equivalent to: node = node._strand5p node = f(node) if node0 == node: break # end def def generator3pStrand(self): """Iterate from self to the final _strand3p is None 5prime to 3prime Includes originalCount to check for circular linked list Yields: Strand: 3' connected :class:`Strand` """ node0 = node = self f = attrgetter('_strand3p') while node: yield node # equivalent to: node = node._strand5p node = f(node) if node0 == node: break # end def def strandFilter(self): return self._strandset.strandFilter() ### SIGNALS ### strandHasNewOligoSignal = ProxySignal(CNObject, name='strandHasNewOligoSignal') """pyqtSignal(QObject): strand""" strandRemovedSignal = ProxySignal(CNObject, name='strandRemovedSignal') """pyqtSignal(QObject): strand""" strandResizedSignal = ProxySignal(CNObject, tuple, name='strandResizedSignal') """pyqtSignal(QObject, tuple)""" strandXover5pRemovedSignal = ProxySignal(CNObject, CNObject, name='strandXover5pRemovedSignal') """pyqtSignal(QObject, QObject): (strand3p, strand5p)""" strandConnectionChangedSignal = ProxySignal( CNObject, name='strandConnectionChangedSignal') """pyqtSignal(QObject): strand""" strandInsertionAddedSignal = ProxySignal(CNObject, object, name='strandInsertionAddedSignal') """pyqtSignal(QObject, object): (strand, insertion object)""" strandInsertionChangedSignal = ProxySignal( CNObject, object, name='strandInsertionChangedSignal') """#pyqtSignal(QObject, object): (strand, insertion object)""" strandInsertionRemovedSignal = ProxySignal( CNObject, int, name='strandInsertionRemovedSignal') """#pyqtSignal(QObject, int): # Parameters: (strand, insertion index)""" strandModsAddedSignal = ProxySignal(CNObject, CNObject, str, int, name='strandModsAddedSignal') """pyqtSignal(QObject, object, str, int): (strand, document, mod_id, idx)""" strandModsChangedSignal = ProxySignal(CNObject, CNObject, str, int, name='strandModsChangedSignal') """pyqtSignal(QObject, object, str, int): (strand, document, mod_id, idx)""" strandModsRemovedSignal = ProxySignal(CNObject, CNObject, str, int, name='strandModsRemovedSignal') """pyqtSignal(QObject, object, str, int): (strand, document, mod_id, idx)""" strandSelectedChangedSignal = ProxySignal( CNObject, tuple, name='strandSelectedChangedSignal') """pyqtSignal(QObject, tuple): (strand, value)""" ### SLOTS ### ### ACCESSORS ### def part(self): return self._strandset.part() # end def def idNum(self): return self._id_num # end def def document(self): return self._document # end def def oligo(self): return self._oligo # end def def getColor(self): return self._oligo.getColor() # end def def sequence(self, for_export=False): seq = self._sequence if seq: return util.markwhite(seq) if for_export else seq elif for_export: return ''.join(['?' for x in range(self.totalLength())]) return '' # end def def abstractSeq(self): return ','.join([str(i) for i in self.abstract_sequence]) def strandSet(self): return self._strandset # end def def strandType(self): return self._strandset.strandType() def isForward(self): return self._strandset.isForward() def setSequence(self, sequence_string): """Applies sequence string from 5' to 3' return the tuple (used, unused) portion of the sequence_string Args: sequence_string (str): Returns: tuple: of :obj:`str` of form:: (used, unused) """ if sequence_string is None: self._sequence = None return None, None length = self.totalLength() if len(sequence_string) < length: bonus = length - len(sequence_string) sequence_string += ''.join([' ' for x in range(bonus)]) temp = sequence_string[0:length] self._sequence = temp return temp, sequence_string[length:] # end def def reapplySequence(self): """ """ comp_ss = self.strandSet().complementStrandSet() # the strand sequence will need to be regenerated from scratch # as there are no guarantees about the entirety of the strand moving # i.e. both endpoints thanks to multiple selections so just redo the # whole thing self._sequence = None for comp_strand in comp_ss.getOverlappingStrands( self._base_idx_low, self._base_idx_high): comp_seq = comp_strand.sequence() used_seq = util.comp(comp_seq) if comp_seq else None used_seq = self.setComplementSequence(used_seq, comp_strand) # end for # end def def getComplementStrands(self): """Return the list of complement strands that overlap with this strand. """ comp_ss = self.strandSet().complementStrandSet() return [ comp_strand for comp_strand in comp_ss.getOverlappingStrands( self._base_idx_low, self._base_idx_high) ] # end def def setComplementSequence(self, sequence_string, strand): """This version takes anothers strand and only sets the indices that align with the given complimentary strand. As it depends which direction this is going, and strings are stored in memory left to right, we need to test for is_forward to map the reverse compliment appropriately, as we traverse overlapping strands. We reverse the sequence ahead of time if we are applying it 5' to 3', otherwise we reverse the sequence post parsing if it's 3' to 5' Again, sequences are stored as strings in memory 5' to 3' so we need to jump through these hoops to iterate 5' to 3' through them correctly Perhaps it's wiser to merely store them left to right and reverse them at draw time, or export time Args: sequence_string (str): strand (Strand): Returns: str: the used portion of the sequence_string """ s_low_idx, s_high_idx = self._base_idx_low, self._base_idx_high c_low_idx, c_high_idx = strand.idxs() is_forward = self._is_forward self_seq = self._sequence # get the ovelap low_idx, high_idx = util.overlap(s_low_idx, s_high_idx, c_low_idx, c_high_idx) # only get the characters we're using, while we're at it, make it the # reverse compliment total_length = self.totalLength() # see if we are applying if sequence_string is None: # clear out string for in case of not total overlap use_seq = ''.join([' ' for x in range(total_length)]) else: # use the string as is use_seq = sequence_string[::-1] if is_forward else sequence_string temp = array(ARRAY_TYPE, sixb(use_seq)) if self_seq is None: temp_self = array( ARRAY_TYPE, sixb(''.join([' ' for x in range(total_length)]))) else: temp_self = array( ARRAY_TYPE, sixb(self_seq) if is_forward else sixb(self_seq[::-1])) # generate the index into the compliment string a = self.insertionLengthBetweenIdxs(s_low_idx, low_idx - 1) b = self.insertionLengthBetweenIdxs(low_idx, high_idx) c = strand.insertionLengthBetweenIdxs(c_low_idx, low_idx - 1) start = low_idx - c_low_idx + c end = start + b + high_idx - low_idx + 1 temp_self[low_idx - s_low_idx + a:high_idx - s_low_idx + 1 + a + b] = temp[start:end] # print("old sequence", self_seq) self._sequence = tostring(temp_self) # if we need to reverse it do it now if not is_forward: self._sequence = self._sequence[::-1] # test to see if the string is empty(), annoyingly expensive # if len(self._sequence.strip()) == 0: if not self._sequence: self._sequence = None # print("new sequence", self._sequence) return self._sequence # end def def clearAbstractSequence(self): self.abstract_sequence = [] # end def def applyAbstractSequence(self): """Assigns virtual index from 5' to 3' on strand and its complement location. """ abstract_seq = [] part = self.part() segment_dict = part.segment_dict[self._id_num] # make sure we apply numbers from 5' to 3' strand_order = 1 if self._is_forward else -1 for segment in self.segments[::strand_order]: if segment in segment_dict: seg_id, offset, length = segment_dict[segment] else: seg_id, offset, length = part.getNewAbstractSegmentId(segment) segment_dict[segment] = (seg_id, offset, length) for i in range(length)[::strand_order]: abstract_seq.append(offset + i) self.abstract_sequence = abstract_seq # end def def copyAbstractSequenceToSequence(self): abstract_seq = self.abstract_sequence # self._sequence = ''.join([ascii_letters[i % 52] for i in abstract_seq]) self._sequence = ''.join(['|' for i in abstract_seq]) # end def ### PUBLIC METHODS FOR QUERYING THE MODEL ### def connection3p(self): return self._strand3p # end def def connection5p(self): return self._strand5p # end def def idxs(self): return (self._base_idx_low, self._base_idx_high) # end def def lowIdx(self): return self._base_idx_low # end def def highIdx(self): return self._base_idx_high # end def def idx3Prime(self): """Returns the absolute base_idx of the 3' end of the strand. overloaded in __init__ """ # return self.idx3Prime def idx5Prime(self): """Returns the absolute base_idx of the 5' end of the strand. overloaded in __init__ """ # return self.idx5Prime def dump5p(self): return self._id_num, self._is_forward, self.idx5Prime() # def def getSequenceList(self): """return the list of sequences strings comprising the sequence and the inserts as a tuple with the index of the insertion [(idx, (strandItemString, insertionItemString), ...] This takes advantage of the fact the python iterates a dictionary by keys in order so if keys are indices, the insertions will iterate out from low index to high index """ seqList = [] is_forward = self._is_forward seq = self._sequence if is_forward else self._sequence[::-1] # assumes a sequence has been applied correctly and is up to date tL = self.totalLength() offsetLast = 0 lengthSoFar = 0 iLength = 0 lI, hI = self.idxs() for insertion in self.insertionsOnStrand(): iLength = insertion.length() index = insertion.idx() offset = index + 1 - lI + lengthSoFar if iLength < 0: offset -= 1 # end if lengthSoFar += iLength seqItem = seq[offsetLast:offset] # the stranditem seq # Because skips literally skip displaying a character at a base # position, this needs to be accounted for seperately if iLength < 0: seqItem += ' ' offsetLast = offset else: offsetLast = offset + iLength seqInsertion = seq[offset:offsetLast] # the insertions sequence seqList.append((index, (seqItem, seqInsertion))) # end for # append the last bit of the strand seqList.append((lI + tL, (seq[offsetLast:tL], ''))) if not is_forward: # reverse it again so all sub sequences are from 5' to 3' for i in range(len(seqList)): index, temp = seqList[i] seqList[i] = (index, (temp[0][::-1], temp[1][::-1])) return seqList # end def def canResizeTo(self, new_low, new_high): """Checks to see if a resize is allowed. Similar to getResizeBounds but works for two bounds at once. """ part = self.part() id_num = self._id_num low_neighbor, high_neighbor = self._strandset.getNeighbors(self) low_bound = low_neighbor.highIdx() if low_neighbor else 0 high_bound = high_neighbor.lowIdx( ) if high_neighbor else part.maxBaseIdx(id_num) if new_low > low_bound and new_high < high_bound: return True return False def getResizeBounds(self, idx): """Determines (inclusive) low and high drag boundaries resizing from an endpoint located at idx. When resizing from _base_idx_low:: low bound is determined by checking for lower neighbor strands. high bound is the index of this strand's high cap, minus 1. When resizing from _base_idx_high:: low bound is the index of this strand's low cap, plus 1. high bound is determined by checking for higher neighbor strands. When a neighbor is not present, just use the Part boundary. """ part = self.part() neighbors = self._strandset.getNeighbors(self) if idx == self._base_idx_low: if neighbors[0] is not None: low = neighbors[0].highIdx() + 1 else: low = 0 # print("A", low, self._base_idx_high - 1 ) return low, self._base_idx_high - 1 else: # self._base_idx_high if neighbors[1] is not None: high = neighbors[1].lowIdx() - 1 else: high = part.maxBaseIdx(self._id_num) # print("B", self._base_idx_low+1, high) return self._base_idx_low + 1, high # end def def hasXoverAt(self, idx): """An xover is necessarily at an enpoint of a strand """ if idx == self.highIdx(): return True if self.connectionHigh() is not None else False elif idx == self.lowIdx(): return True if self.connectionLow() is not None else False else: return False # end def def canInstallXoverAt(self, idx, from_strand, from_idx): """Assumes idx is: self.lowIdx() <= idx <= self.highIdx() """ if self.hasXoverAt(idx): return False ss = self.strandSet() is_same_strand = from_strand == self is_forward = ss.isForward() index_diff_H = self.highIdx() - idx index_diff_L = idx - self.lowIdx() idx3p = self.idx3Prime() idx5p = self.idx5Prime() # ensure 2 bps from 3p end if not the 3p end index3_lim = idx3p - 1 if is_forward else idx3p + 1 if is_same_strand: index_diff_strands = from_idx - idx if idx == idx5p or idx == index3_lim: return True elif index_diff_strands > -3 and index_diff_strands < 3: return False # end if for same Strand else: from_idx3p = from_strand.idx3Prime() from_idx5p = from_strand.idx5Prime() if idx == idx5p or idx == index3_lim: if from_idx3p == from_idx: return True elif (abs(from_idx3p - from_idx) > 1 and abs(from_idx5p - from_idx) > 1): return True else: # print("this:", idx, idx3p, idx5p) # print("from:", from_idx, from_idx3p, from_idx5p) return False elif index_diff_H > 2 and index_diff_L > 1: if from_idx3p == from_idx: return True elif (abs(from_idx3p - from_idx) > 1 and abs(from_idx5p - from_idx) > 1): return True else: return False else: # print("default", index_diff_H, index_diff_L) return False # end def def insertionLengthBetweenIdxs(self, idxL, idxH): """includes the length of insertions in addition to the bases """ tL = 0 insertions = self.insertionsOnStrand(idxL, idxH) for insertion in insertions: tL += insertion.length() return tL # end def def insertionsOnStrand(self, idxL=None, idxH=None): """if passed indices it will use those as a bounds """ insertions = [] insertionsDict = self.part().insertions()[self._id_num] sortedIndices = sorted(insertionsDict.keys()) if idxL is None: idxL, idxH = self.idxs() for index in sortedIndices: insertion = insertionsDict[index] if idxL <= insertion.idx() <= idxH: insertions.append(insertion) # end if # end for return insertions # end def def modifersOnStrand(self): """ """ mods = [] modsDict = self.part().mods()['ext_instances'] id_num = self._id_num isstaple = True # self.isStaple() idxL, idxH = self.idxs() keyL = "{},{},{}".format(id_num, isstaple, idxL) keyH = "{},{},{}".format(id_num, isstaple, idxH) if keyL in modsDict: mods.append(modsDict[keyL]) if keyH in modsDict: mods.append(modsDict[keyH]) return mods # end def def length(self): return self._base_idx_high - self._base_idx_low + 1 # end def def totalLength(self): """includes the length of insertions in addition to the bases """ tL = 0 insertions = self.insertionsOnStrand() for insertion in insertions: tL += insertion.length() return tL + self.length() # end def ### PUBLIC METHODS FOR EDITING THE MODEL ### def addMods(self, document, mod_id, idx, use_undostack=True): """Used to add mods during a merge operation.""" cmds = [] idx_low, idx_high = self.idxs() if idx_low == idx or idx == idx_high: check_mid1 = self.part().getModID(self, idx) check_mid2 = document.getMod(mod_id) print("strand.addMods:", check_mid1, check_mid2) if check_mid2 is not None: if check_mid1 != mod_id: if check_mid1 is not None: cmds.append( RemoveModsCommand(document, self, idx, check_mid1)) # print("adding a {} modification at {}".format(mod_id, idx)) cmds.append(AddModsCommand(document, self, idx, mod_id)) util.execCommandList(self, cmds, desc="Add Modification", use_undostack=use_undostack) else: print(check_mid1, mod_id) # end if # end def def removeMods(self, document, mod_id, idx, use_undostack=True): """Used to add mods during a merge operation.""" idx_low, idx_high = self.idxs() print("attempting to remove") if idx_low == idx or idx == idx_high: print("removing a modification at {}".format(idx)) c = RemoveModsCommand(document, self, idx, mod_id) util.doCmd(self, c, use_undostack=use_undostack) # end if # end def def addInsertion(self, idx, length, use_undostack=True): """Adds an insertion or skip at idx. length should be:: >0 for an insertion -1 for a skip Args: idx (int): length (int): use_undostack (bool): optional, default is True """ cmds = [] idx_low, idx_high = self.idxs() if idx_low <= idx <= idx_high: if not self.hasInsertionAt(idx): # make sure length is -1 if a skip if length < 0: length = -1 if use_undostack: # on import no need to blank sequences cmds.append(self.oligo().applySequenceCMD(None)) for strand in self.getComplementStrands(): cmds.append(strand.oligo().applySequenceCMD(None)) cmds.append(AddInsertionCommand(self, idx, length)) util.execCommandList(self, cmds, desc="Add Insertion", use_undostack=use_undostack) # end if # end if # end def def changeInsertion(self, idx, length, use_undostack=True): """ Args: idx (int): length (int): use_undostack (bool): optional, default is True """ cmds = [] idx_low, idx_high = self.idxs() if idx_low <= idx <= idx_high: if self.hasInsertionAt(idx): if length == 0: self.removeInsertion(idx) else: # make sure length is -1 if a skip if length < 0: length = -1 cmds.append(self.oligo().applySequenceCMD(None)) for strand in self.getComplementStrands(): cmds.append(strand.oligo().applySequenceCMD(None)) cmds.append(ChangeInsertionCommand(self, idx, length)) util.execCommandList(self, cmds, desc="Change Insertion", use_undostack=use_undostack) # end if # end if # end def def removeInsertion(self, idx, use_undostack=True): """ Args: idx (int): use_undostack (bool): optional, default is True """ cmds = [] idx_low, idx_high = self.idxs() if idx_low <= idx <= idx_high: if self.hasInsertionAt(idx): if use_undostack: cmds.append(self.oligo().applySequenceCMD(None)) for strand in self.getComplementStrands(): cmds.append(strand.oligo().applySequenceCMD(None)) cmds.append(RemoveInsertionCommand(self, idx)) util.execCommandList(self, cmds, desc="Remove Insertion", use_undostack=use_undostack) # end if # end if # end def def destroy(self): self.setParent(None) self.deleteLater() # QObject also emits a destroyed() Signal # end def def merge(self, idx): """Check for neighbor, then merge if possible. Args: idx (int): Raises: IndexError: """ low_neighbor, high_neighbor = self._strandset.getNeighbors(self) # determine where to check for neighboring endpoint if idx == self._base_idx_low: if low_neighbor: if low_neighbor.highIdx() == idx - 1: self._strandset.mergeStrands(self, low_neighbor) elif idx == self._base_idx_high: if high_neighbor: if high_neighbor.lowIdx() == idx + 1: self._strandset.mergeStrands(self, high_neighbor) else: raise IndexError # end def def resize(self, new_idxs, use_undostack=True, update_segments=True): cmds = [] cmds += self.getRemoveInsertionCommands(new_idxs) cmds.append( ResizeCommand(self, new_idxs, update_segments=update_segments)) util.execCommandList(self, cmds, desc="Resize strand", use_undostack=use_undostack) # end def def setConnection3p(self, strand): self._strand3p = strand # end def def setConnection5p(self, strand): self._strand5p = strand # end def def setIdxs(self, idxs): self._base_idx_low = idxs[0] self._base_idx_high = idxs[1] # end def def setOligo(self, new_oligo, emit_signals=False): self._oligo = new_oligo if emit_signals: self.strandHasNewOligoSignal.emit(self) # end def def split(self, idx, update_sequence=True): """Called by view items to split this strand at idx.""" self._strandset.splitStrand(self, idx, update_sequence) ### PUBLIC SUPPORT METHODS ### def getRemoveInsertionCommands(self, new_idxs): """Removes Insertions, Decorators, and Modifiers that have fallen out of range of new_idxs. For insertions, it finds the ones that have neither Staple nor Scaffold strands at the insertion idx as a result of the change of this strand to new_idxs """ cIdxL, cIdxH = self.idxs() nIdxL, nIdxH = new_idxs # low_out, high_out = False, False insertions = [] if cIdxL < nIdxL < cIdxH: idxL, idxH = cIdxL, nIdxL - 1 insertions += self.insertionsOnStrand(idxL, idxH) else: pass if cIdxL < nIdxH < cIdxH: idxL, idxH = nIdxH + 1, cIdxH insertions += self.insertionsOnStrand(idxL, idxH) else: pass # this only called if both the above aren't true # if low_out and high_out: # if we move the whole strand, just clear the insertions out if nIdxL > cIdxH or nIdxH < cIdxL: idxL, idxH = cIdxL, cIdxH insertions += self.insertionsOnStrand(idxL, idxH) # we stretched in this direction return self.clearInsertionsCommands(insertions, cIdxL, cIdxH) # end def def clearInsertionsCommands(self, insertions, idxL, idxH): """clear out insertions in this range """ commands = [] comp_ss = self.strandSet().complementStrandSet() overlappingStrandList = comp_ss.getOverlappingStrands(idxL, idxH) for insertion in insertions: idx = insertion.idx() removeMe = True for strand in overlappingStrandList: overLapIdxL, overLapIdxH = strand.idxs() if overLapIdxL <= idx <= overLapIdxH: removeMe = False # end if # end for if removeMe: commands.append(RemoveInsertionCommand(self, idx)) else: pass # print "keeping %s insertion at %d" % (self, key) # end for # ADD CODE HERE TO HANDLE DECORATORS AND MODIFIERS return commands # end def def clearDecoratorCommands(self): insertions = self.insertionsOnStrand() return self.clearInsertionsCommands(insertions, *self.idxs()) # end def def hasInsertionAt(self, idx): insts = self.part().insertions()[self._id_num] return idx in insts # end def def shallowCopy(self): """ """ new_s = Strand(self._strandset, *self.idxs()) new_s._oligo = self._oligo new_s._strand5p = self._strand5p new_s._strand3p = self._strand3p # required to shallow copy the dictionary new_s._sequence = None # self._sequence return new_s # end def def _deepCopy(self, strandset, oligo): """ """ new_s = Strand(strandset, *self.idxs()) new_s._oligo = oligo new_s._sequence = self._sequence return new_s
class Assembly(CNObject): """ An Assembly is a collection of components, comprised recursively of various levels of individual parts and sub-assembly modules. The purpose of an Assembly object in radnano is to arrange Parts into larger groups (which may be connected or constrained in specific ways) to facilitate the modeling of more complex designs than a single part. """ def __init__(self, document): super(Assembly, self).__init__(document) self._document = document self._obj_instance_list = [] # This is a list of member parts # This is a list of ObjectInstances of this # particular assembly ONLY # an Assembly can not have an ObjectIntanceList that contains itself # that would be a circular reference self._assembly_instances = [] # end def # SIGNALS # assemblyInstanceAddedSignal = ProxySignal(CNObject, name='assemblyInstanceAddedSignal') assemblyDestroyedSignal = ProxySignal(CNObject, name='assemblyDestroyedSignal') # SLOTS # # METHODS # def undoStack(self): return self._document.undoStack() def destroy(self): # QObject also emits a destroyed() Signal self.setParent(None) self.deleteLater() # end def def document(self): return self._document # end def def objects(self): for obj in self._obj_instance_list: yield obj # end def def instances(self): for inst in self._assembly_instances: yield inst # end def def deepCopy(self): """ Deep copy the assembly by cloning the This leaves alone assemblyInstances, and only To finish the job this deepCopy Assembly should be incorporated into a new ObjectInstance and therefore an assemblyInstance """ doc = self._document asm = Assembly(doc) new_obj_inst_list = asm._obj_instance_list obj_instances = self.objects() # create a dictionary mapping objects (keys) to lists of # ObjectInstances ([value1, value2]) # this uniquifies the creation of new Assemblies object_dict = defaultdict(list) f1 = methodcaller('reference') for x in obj_instances: obj = f1(x) object_dict[obj].append(x) # end for # copy the all the objects f2 = methodcaller('deepCopy') for key, value in object_dict: # create a new object newObj = f2(key) # copy all of the instances relevant to this new object newInsts = [obj_inst.deepCopy(newObj, asm) for obj_inst in value] # add these to the list in the assembly new_obj_inst_list.extend(newInsts) # add Object to the document doc.addObject(newObj) # end for return asm # end def def addInstance(self, assembly_instance): self._assembly_instances.extend(assembly_instance)
class Document(CNObject): """ The Document class is the root of the model. It has two main purposes: 1. Serve as the parent all Part objects within the model. 2. Track all sub-model actions on its undoStack. Args: parent (CNObject): optional, defaults to None Attributes: view_names (list): views the document should support filter_set (set): filters that should be applied when selecting. """ def __init__(self, parent=None): super(Document, self).__init__(parent) self._undostack = us = UndoStack( ) # notice NO parent, what does this mean? us.setUndoLimit(30) self._children = set( ) # for storing a reference to Parts (and Assemblies) self._instances = set( ) # for storing instances of Parts (and Assemblies) self._controller = None # the dictionary maintains what is selected self._selection_dict = {} self._active_part = None self._filename = None # the added list is what was recently selected or deselected self._strand_selected_changed_dict = {} self.view_names = [] self.filter_set: Set[str] = set() self._mods = {} # modifications keyed by mod id this_app = app() this_app.documentWasCreatedSignal.emit(self) # end def # SIGNALS # documentPartAddedSignal = ProxySignal(object, CNObject, name='documentPartAddedSignal') """`Document`, `Part`""" documentAssemblyAddedSignal = ProxySignal( object, CNObject, name='documentAssemblyAddedSignal') """`Document`, `Assembly`""" documentSelectionFilterChangedSignal = ProxySignal( object, name='documentSelectionFilterChangedSignal') documentPreXoverFilterChangedSignal = ProxySignal( str, name='documentPreXoverFilterChangedSignal') documentViewResetSignal = ProxySignal(CNObject, name='documentViewResetSignal') documentClearSelectionsSignal = ProxySignal( CNObject, name='documentClearSelectionsSignal') documentModAddedSignal = ProxySignal(object, object, object, name='documentModAddedSignal') documentModRemovedSignal = ProxySignal(object, object, name='documentModRemovedSignal') documentModChangedSignal = ProxySignal(object, object, object, name='documentModChangedSignal') documentChangeViewSignalingSignal = ProxySignal( int, name='documentChangeViewSignalingSignal') # SLOTS # # ACCESSORS # def undoStack(self) -> UndoStack: """This is the actual undoStack to use for all commands. Any children needing to perform commands should just ask their parent for the undoStack, and eventually the request will get here. """ return self._undostack def children(self) -> Set[CNObject]: """Returns a list of parts associated with the document. Returns: list: list of all child objects """ return self._children def addRefObj(self, child: CNObject): """For adding Part and Assembly object references Args: child (object): """ self._children.add(child) def addInstance(self, instance: ObjectInstance): """Add an ObjectInstance to the list of instances Args: instance: """ self._instances.add(instance) def removeInstance(self, instance: ObjectInstance): """ Remove an ObjectInstance from the list of instances Args: instance: """ self._instances.remove(instance) self.documentClearSelectionsSignal.emit(self) def removeAllChildren(self): """Used to reset the document. Not undoable.""" self.documentClearSelectionsSignal.emit(self) for child in list(self._children): child.remove(use_undostack=True) self.undoStack().clear() self.deactivateActivePart() # end def def setFilterSet(self, filter_list: List[str]): """ Set the Document filter list. Emits `documentSelectionFilterChangedSignal` Args: filter_list: list of filter key names """ assert isinstance(filter_list, list) vhkey = 'virtual_helix' fs = self.filter_set if vhkey in filter_list and vhkey not in fs: self.clearAllSelected() if vhkey in fs and vhkey not in filter_list: self.clearAllSelected() self.filter_set = fs = set(filter_list) self.documentSelectionFilterChangedSignal.emit(fs) # end def def removeRefObj(self, child: CNObject): """ Remove child Part or Assembly Args: child: """ self._children.remove(child) # end def def activePart(self) -> Part: return self._active_part # end def def setActivePart(self, part: Part): self._active_part = part if self._controller: self._controller.toggleNewPartButtons(False) # end def def deactivateActivePart(self): self._active_part = None if self._controller: self._controller.toggleNewPartButtons(True) # end def def changeViewSignaling(self, signal_enum: int = ViewSendEnum.ALL): '''Turn on and off viwe signaling for enabled slots in views. Signals the root item in each view Arg: signal_enum: Default turns on all views signals ''' self.documentChangeViewSignalingSignal.emit(signal_enum) # end def def fileName(self) -> str: return self._filename # end def def setFileName(self, fname: str): self._filename = fname # end def def writeToFile(self, filename: str, legacy: bool = False): """ Convenience wrapper for `encodeToFile` to set the `document` argument to `self` Args: filename: full path file name legacy: attempt to export cadnano2 format """ encodeToFile(filename, self, legacy) # end def def readFile(self, filename: str) -> DocT: """Convenience wrapper for ``decodeFile`` to always emit_signals and set the ``document`` argument to ``self`` Args: filename: full path file name Returns: self ``Document`` object with data decoded from ``filename`` """ print("reading file", filename) return decodeFile(filename, document=self, emit_signals=True) # end def # def assemblies(self): # """Returns a list of assemblies associated with the document.""" # return self._assemblies # PUBLIC METHODS FOR QUERYING THE MODEL # def addStrandToSelection(self, strand: Strand, value: EndsSelected): """ Add `Strand` object to Document selection Args: strand: value: of the form:: (is low index selected, is high index selected) """ ss = strand.strandSet() if ss in self._selection_dict: self._selection_dict[ss][strand] = value else: self._selection_dict[ss] = {strand: value} self._strand_selected_changed_dict[strand] = value # end def def removeStrandFromSelection(self, strand: Strand) -> bool: """Remove ``Strand`` object from Document selection Args: strand: Returns: ``True`` if successful, ``False`` otherwise """ ss = strand.strandSet() if ss in self._selection_dict: temp = self._selection_dict[ss] if strand in temp: del temp[strand] if len(temp) == 0: del self._selection_dict[ss] self._strand_selected_changed_dict[strand] = (False, False) return True else: return False else: return False # end def def addVirtualHelicesToSelection(self, part: Part, id_nums: Iterable[int]): """If the ``Part`` isn't in the ``_selection_dict`` its not going to be in the changed_dict either, so go ahead and add Args: part: The Part id_nums: List of virtual helix ID numbers """ selection_dict = self._selection_dict if part not in selection_dict: selection_dict[part] = s_set = set() else: s_set = selection_dict[part] changed_set = set() for id_num in id_nums: if id_num not in s_set: s_set.add(id_num) changed_set.add(id_num) if len(changed_set) > 0: part.partVirtualHelicesSelectedSignal.emit(part, changed_set, True) # end def def removeVirtualHelicesFromSelection(self, part: Part, id_nums: Iterable[int]): """Remove from the ``Part`` selection the ``VirtualHelix`` objects specified by id_nums. Args: part: id_nums: """ # print("remove called", id(part), id_nums, self._selection_dict.get(part)) selection_dict = self._selection_dict if part in selection_dict: s_set = selection_dict[part] changed_set = set() for id_num in id_nums: if id_num in s_set: s_set.remove(id_num) if len(s_set) == 0: del selection_dict[part] changed_set.add(id_num) if len(changed_set) > 0: part.partVirtualHelicesSelectedSignal.emit( part, changed_set, False) # end def def selectedOligos(self) -> Set[Oligo]: """As long as one endpoint of a strand is in the selection, then the oligo is considered selected. Returns: Set of zero or more selected :obj:`Oligos` """ s_dict = self._selection_dict selected_oligos = set() for ss in s_dict.keys(): for strand in ss: selected_oligos.add(strand.oligo()) # end for # end for return selected_oligos # end def def clearAllSelected(self): """Clear all selections emits documentClearSelectionsSignal """ # print("clearAllSelected") self._selection_dict = {} # the added list is what was recently selected or deselected self._strand_selected_changed_dict = {} self.documentClearSelectionsSignal.emit(self) # end def def isModelStrandSelected(self, strand: Strand) -> bool: ss = strand.strandSet() if ss in self._selection_dict: if strand in self._selection_dict[ss]: return True else: return False else: return False # end def def isVirtualHelixSelected(self, part: Part, id_num: int) -> bool: """For a given ``Part`` Args: part: ``Part`` in question id_num: ID number of a virtual helix Returns: ``True`` if ``id_num`` is selected else ``False`` """ if part in self._selection_dict: return id_num in self._selection_dict[part] else: return False # end def def isOligoSelected(self, oligo: Oligo) -> bool: """Determine if given ``Oligo`` is selected Args: oligo: ``Oligo`` object Returns: ``True`` if ``oligo`` is selected otherwise ``False`` """ strand5p = oligo.strand5p() for strand in strand5p.generator3pStrand(): if self.isModelStrandSelected(strand): return True return False # end def def selectOligo(self, oligo: Oligo): """Select given ``Oligo`` Args: oligo: ``Oligo`` object """ strand5p = oligo.strand5p() both_ends = (True, True) for strand in strand5p.generator3pStrand(): self.addStrandToSelection(strand, both_ends) self.updateStrandSelection() # end def def deselectOligo(self, oligo: Oligo): """Deselect given ``Oligo`` Args: oligo: ``Oligo`` object """ strand5p = oligo.strand5p() for strand in strand5p.generator3pStrand(): self.removeStrandFromSelection(strand) self.updateStrandSelection() # end def def getSelectedStrandValue(self, strand: Strand) -> EndsSelected: """Strand is an object to look up it is pre-vetted to be in the dictionary Args: strand: ``Strand`` object in question Returns: Tuple of the end point selection """ return self._selection_dict[strand.strandSet()][strand] # end def def sortedSelectedStrands(self, strandset: StrandSet) -> List[Strand]: """Get a list sorted from low to high index of `Strands` in a `StrandSet` that are selected Args: strandset: :obj:`StrandSet` to get selected strands from Returns: List of :obj:`Strand`s """ out_list = [x for x in self._selection_dict[strandset].items()] def getLowIdx(x): return Strand.lowIdx(itemgetter(0)(x)) out_list.sort(key=getLowIdx) return out_list # end def def determineStrandSetBounds(self, selected_strand_list: List[Tuple[ Strand, EndsSelected]], strandset: StrandSet) -> Tuple[int, int]: """Determine the bounds of a :class:`StrandSet` ``strandset`` among a a list of selected strands in that same ``strandset`` Args: selected_strand_list: list of ``( Strands, (is_low, is_high) )`` items strandset: of interest Returns: tuple: min low bound and min high bound index """ length = strandset.length() min_high_delta = min_low_delta = max_ss_idx = length - 1 # init the return values ss_dict = self._selection_dict[strandset] for strand, value in selected_strand_list: idx_low, idx_high = strand.idxs() low_neighbor, high_neighbor = strandset.getNeighbors(strand) # print(low_neighbor, high_neighbor) if value[0]: # the end is selected if low_neighbor is None: temp = idx_low - 0 else: if low_neighbor in ss_dict: value_N = ss_dict[low_neighbor] # we only care if the low neighbor is not selected temp = min_low_delta if value_N[ 1] else idx_low - low_neighbor.highIdx() - 1 # end if else: # not selected temp = idx_low - low_neighbor.highIdx() - 1 # end else if temp < min_low_delta: min_low_delta = temp # end if # check the other end of the strand if not value[1]: temp = idx_high - idx_low - 1 if temp < min_high_delta: min_high_delta = temp # end if if value[1]: if high_neighbor is None: temp = max_ss_idx - idx_high else: if high_neighbor in ss_dict: value_N = ss_dict[high_neighbor] # we only care if the low neighbor is not selected temp = min_high_delta if value_N[ 0] else high_neighbor.lowIdx() - idx_high - 1 # end if else: # not selected temp = high_neighbor.lowIdx() - idx_high - 1 # end else # end else if temp < min_high_delta: min_high_delta = temp # end if # check the other end of the strand if not value[0]: temp = idx_high - idx_low - 1 if temp < min_low_delta: min_low_delta = temp # end if # end for return (min_low_delta, min_high_delta) # end def def getSelectionBounds(self) -> Tuple[int, int]: """Get the index bounds of a strand selection Returns: tuple: of :obj:`int` """ min_low_delta = -1 min_high_delta = -1 for strandset in self._selection_dict.keys(): selected_list = self.sortedSelectedStrands(strandset) temp_low, temp_high = self.determineStrandSetBounds( selected_list, strandset) if temp_low < min_low_delta or min_low_delta < 0: min_low_delta = temp_low if temp_high < min_high_delta or min_high_delta < 0: min_high_delta = temp_high return (min_low_delta, min_high_delta) # end def def deleteStrandSelection(self, use_undostack: bool = True): """Delete selected strands. First iterates through all selected strands and extracts refs to xovers and strands. Next, calls removeXover on xoverlist as part of its own macroed command for isoluation purposes. Finally, calls removeStrand on all strands that were fully selected (low and high), or had at least one non-xover endpoint selected. """ xoList = [] strand_dict = {} for strandset_dict in self._selection_dict.values(): for strand, selected in strandset_dict.items(): part = strand.part() idx_low, idx_high = strand.idxs() strand5p = strand.connection5p() strand3p = strand.connection3p() # both ends are selected strand_dict[strand] = selected[0] and selected[1] # only look at 3' ends to handle xover deletion sel3p = selected[0] if idx_low == strand.idx3Prime( ) else selected[1] if sel3p: # is idx3p selected? if strand3p: # is there an xover xoList.append((part, strand, strand3p, use_undostack)) else: # idx3p is a selected endpoint strand_dict[strand] = True else: if not strand5p: # idx5p is a selected endpoint strand_dict[strand] = True if use_undostack and xoList: self.undoStack().beginMacro("Delete xovers") for part, strand, strand3p, useUndo in xoList: NucleicAcidPart.removeXover(part, strand, strand3p, useUndo) self.removeStrandFromSelection(strand) self.removeStrandFromSelection(strand3p) self._selection_dict = {} self.documentClearSelectionsSignal.emit(self) if use_undostack: if xoList: # end xover macro if it was started self.undoStack().endMacro() if True in strand_dict.values(): self.undoStack().beginMacro("Delete selection") else: return # nothing left to do for strand, delete in strand_dict.items(): if delete: strand.strandSet().removeStrand(strand) if use_undostack: self.undoStack().endMacro() # end def def resizeSelection(self, delta: int, use_undostack: bool = True): """Moves the selected idxs by delta by first iterating over all strands to calculate new idxs (method will return if snap-to behavior would create illegal state), then applying a resize command to each strand. Args: delta: use_undostack: optional, default is ``True`` """ resize_list = [] vh_set = set() # calculate new idxs part = None for strandset_dict in self._selection_dict.values(): for strand, selected in strandset_dict.items(): if part is None: part = strand.part() idx_low, idx_high = strand.idxs() new_low, new_high = strand.idxs() delta_low = delta_high = delta # process xovers to get revised delta if selected[0] and strand.connectionLow(): new_low = part.xoverSnapTo(strand, idx_low, delta) if new_low is None: return delta_high = new_low - idx_low if selected[1] and strand.connectionHigh(): new_high = part.xoverSnapTo(strand, idx_high, delta) if new_high is None: return delta_low = new_high - idx_high # process endpoints if selected[0] and not strand.connectionLow(): new_low = idx_low + delta_low if selected[1] and not strand.connectionHigh(): new_high = idx_high + delta_high if new_low > new_high: # check for illegal state return vh_set.add(strand.idNum()) resize_list.append((strand, new_low, new_high)) # end for # end for # execute the resize commands us = self.undoStack() if use_undostack: us.beginMacro("Resize Selection") for strand, idx_low, idx_high in resize_list: Strand.resize(strand, (idx_low, idx_high), use_undostack, update_segments=False) if resize_list: cmd = RefreshSegmentsCommand(part, vh_set) if use_undostack: us.push(cmd) else: cmd.redo() if use_undostack: us.endMacro() # end def def updateStrandSelection(self): """Do it this way in the future when we have a better signaling architecture between views For now, individual objects need to emit signals """ oligos_selected_set = set() oligos_set = set() for obj, value in self._strand_selected_changed_dict.items(): oligo = obj.oligo() oligos_set.add(oligo) if True in value: oligos_selected_set.add(oligo) obj.strandSelectedChangedSignal.emit(obj, value) # end for for oligo in oligos_selected_set: oligo.oligoSelectedChangedSignal.emit(oligo, True) oligos_deselected_set = oligos_set - oligos_selected_set for oligo in oligos_deselected_set: oligo.oligoSelectedChangedSignal.emit(oligo, False) self._strand_selected_changed_dict = {} # end def def resetViews(self): """This is a fast way to clear selections and the views. We could manually deselect each item from the Dict, but we'll just let them be garbage collect the dictionary maintains what is selected """ # print("reset views") self._selection_dict = {} # the added list is what was recently selected or deselected self._strand_selected_changed_dict = {} self.documentViewResetSignal.emit(self) # end def def makeNew(self, fname: str = "untitled.json"): """For use in creating a new ``Document`` Args: fname: new filename, default is ``untitled.json`` """ self.clearAllSelected() self.resetViews() setBatch(True) self.removeAllChildren() # clear out old parts setBatch(False) self.undoStack().clear() # reset undostack self.deactivateActivePart() self._filename = fname # end def def setViewNames(self, view_name_list: List[str], do_clear: bool = False): """Tell the model what views the document should support Allows non-visible views to be used. Intended to be called at application launch only at present. Args: view_name_list: List of view names like `slice`, `path`, or `inspector` do_clear:: optional, clear the names or not? defaults to ``False`` """ view_names = [] if do_clear else self.view_names for view_name in view_name_list: if view_name not in view_names: view_names.append(view_name) self.view_names = view_names # end def # PUBLIC METHODS FOR EDITING THE MODEL # def createNucleicAcidPart( self, use_undostack: bool = True, grid_type: EnumType = GridEnum.NONE) -> NucleicAcidPart: """Create and store a new DnaPart and instance, and return the instance. Args: use_undostack: optional, defaults to True grid_type: optional default to HoneyComb Returns new :obj:`NucleicAcidPart` """ dna_part = NucleicAcidPart(document=self, grid_type=grid_type) self._addPart(dna_part, use_undostack=use_undostack) return dna_part # end def def getParts(self) -> Iterator[Part]: """Get all child :obj:`Part` in the document Yields: the next :obj:`Part` in the the Set of children """ for item in self._children: if isinstance(item, Part): yield item # end def def getPartByUUID(self, uuid: str) -> Part: """Get the part given the uuid string Args: uuid: of the part Returns: Part Raises: KeyError: no part with that UUID """ for item in self._children: if isinstance(item, Part) and item.uuid == uuid: return item raise KeyError("Part with uuid {} not found".format(uuid)) # end def # PUBLIC SUPPORT METHODS # def controller(self) -> DocCtrlT: return self._controller # end def def setController(self, controller: DocCtrlT): """Called by :meth:`DocumentController.setDocument` method.""" self._controller = controller # end def # PRIVATE SUPPORT METHODS # def _addPart(self, part: Part, use_undostack: bool = True): """Add part to the document via AddInstanceCommand. """ c = AddInstanceCommand(self, part) util.doCmd(self, c, use_undostack) # end def def createMod(self, params: dict, mid: str = None, use_undostack: bool = True) -> Tuple[dict, str]: """Create a modification Args: params: mid: optional, modification ID string use_undostack: optional, default is ``True`` Returns: tuple of :obj:`dict`, :obj:`str` of form:: (dictionary of modification paramemters, modification ID string) Raises: KeyError: Duplicate mod ID """ if mid is None: mid = uuid4().hex elif mid in self._mods: raise KeyError("createMod: Duplicate mod id: {}".format(mid)) name = params.get('name', mid) color = params.get('color', '#00FF00') seq5p = params.get('seq5p', '') seq3p = params.get('seq3p', '') seqInt = params.get('seqInt', '') note = params.get('note', '') cmdparams = { 'props': { 'name': name, 'color': color, 'note': note, 'seq5p': seq5p, 'seq3p': seq3p, 'seqInt': seqInt, }, 'ext_locations': set(), # external mods, mod belongs to idx outside of strand 'int_locations': set() # internal mods, mod belongs between idx and idx + 1 } item = { 'name': name, 'color': color, 'note': note, 'seq5p': seq5p, 'seq3p': seq3p, 'seqInt': seqInt } c = AddModCommand(self, cmdparams, mid) util.doCmd(self, c, use_undostack=use_undostack) return item, mid # end def def modifyMod(self, params: dict, mid: str, use_undostack: bool = True): """Modify an existing modification Args: params: mid: optional, modification ID string use_undostack: optional, default is ``True`` """ if mid in self._mods: c = ModifyModCommand(self, params, mid) util.doCmd(self, c, use_undostack=use_undostack) # end def def destroyMod(self, mid: str, use_undostack: bool = True): """Destroy an existing modification Args: mid: optional, modification ID string use_undostack: optional, default is ``True`` """ if mid in self._mods: c = RemoveModCommand(self, mid) util.doCmd(self, c, use_undostack=use_undostack) # end def def getMod(self, mid: str) -> Optional[dict]: """Get an existing modification Args: mid: modification ID string Returns: dict or None """ return self._mods.get(mid) # end def def getModProperties(self, mid: str) -> Optional[dict]: """Get an existing modification properties Args: mid: modification ID string Returns: dict or None """ return self._mods.get(mid)['props'] # end def def getModLocationsSet(self, mid: str, is_internal: bool) -> dict: """Get an existing modifications locations in a ``Document`` (``Part``, Virtual Helix ID, ``Strand``) Args: mid: modification ID string is_internal: Returns: dict """ if is_internal: return self._mods[mid]['int_locations'] else: return self._mods[mid]['ext_locations'] # end def def addModInstance(self, mid: str, is_internal: bool, part: Part, key: str): """Add an instance of a modification to the Document Args: mid: modification id string is_internal: part: associated Part key: key of the modification at the part level """ location_set = self.getModLocationsSet(mid, is_internal) doc_key = ''.join((part.uuid, ',', key)) location_set.add(doc_key) # end def def removeModInstance(self, mid: str, is_internal: bool, part: Part, key: str): """Remove an instance of a modification from the Document Args: mid: modification id string is_internal: part: associated Part key: key of the modification at the part level """ location_set = self.getModLocationsSet(mid, is_internal) doc_key = ''.join((part.uuid, ',', key)) location_set.remove(doc_key) # end def def modifications(self) -> dict: """Get a copy of the dictionary of the modifications in this ``Document`` Returns: dictionary of the modifications """ mods = self._mods res = {} for mid in list(mods.keys()): mod_dict = mods[mid] res[mid] = { 'props': mod_dict['props'].copy(), 'int_locations': list(mod_dict['int_locations']), 'ext_locations': list(mod_dict['ext_locations']) } return res # end def def getModStrandIdx(self, key: str) -> Tuple[Part, Strand, int]: """Convert a key of a mod instance relative to a part to a part, a strand and an index Args: key: Mod key Returns: tuple of the form:: (Part, Strand, and index) """ keylist = key.split(',') part_uuid = keylist[0] id_num = int(keylist[1]) is_fwd = int( keylist[2]) # enumeration of StrandEnum.FWD or StrandEnum.REV idx = int(keylist[3]) part = self.getPartByUUID(part_uuid) strand = part.getStrand(is_fwd, id_num, idx) return part, strand, idx # end def def getModSequence(self, mid: str, mod_type: int) -> Tuple[str, str]: """Getter for the modification sequence give by the arguments Args: mid: mod id or ``None`` mod_type: [ModEnum.END_5PRIME, ModEnum.END_3PRIME] Returns: tuple: of :obj:`str` of form:: (sequence, name) """ mod_dict = self._mods.get(mid) name = '' if mid is None else mod_dict['name'] if mod_type == ModEnum.END_5PRIME: seq = '' if mid is None else mod_dict['seq5p'] elif mod_type == ModEnum.END_3PRIME: seq = '' if mid is None else mod_dict['seq3p'] else: seq = '' if mid is None else mod_dict['seqInt'] return seq, name # end def def setSliceOrGridViewVisible(self, view_type: EnumType): """Set the current SliceView type Args: view_type: enum from the ``OrthoViewEnum`` """ if self.controller(): self.controller().setSliceOrGridViewVisible(view_type) # # end def def getGridType(self) -> EnumType: """Get the current Grid type Returns: The current Grid type """ if self.activePart(): return self.activePart().getGridType() # end def def setGridType(self, grid_type: EnumType): """Set the current Grid type """ if self.activePart(): self.activePart().setGridType(grid_type)
class Oligo(CNObject): """ Oligo is a group of Strands that are connected via 5' and/or 3' connections. It corresponds to the physical DNA strand, and is thus used tracking and storing properties that are common to a single strand, such as its color. Commands that affect Strands (e.g. create, remove, merge, split) are also responsible for updating the affected Oligos. Args: part (Part): the model :class:`Part` color (str): optional, color property of the :class:`Oligo` """ editable_properties = ['name', 'color'] def __init__(self, part, color=None, length=0): super(Oligo, self).__init__(part) self._part = part self._strand5p = None self._is_circular = False self._props = { 'name': "oligo%s" % str(id(self))[-4:], 'color': "#cc0000" if color is None else color, 'length': length, 'is_visible': True } # end def def __repr__(self): _name = self.__class__.__name__ _id = str(id(self))[-4:] if self._strand5p is not None: vh_num = self._strand5p.idNum() idx = self._strand5p.idx5Prime() ss_type = self._strand5p.strandType() else: vh_num = -1 idx = -1 ss_type = -1 oligo_identifier = '(%d.%d[%d])' % (vh_num, ss_type, idx) return '%s_%s_%s' % (_name, oligo_identifier, _id) # end def def __lt__(self, other): return self.length() < other.length() # end def def shallowCopy(self): olg = Oligo(self._part) olg._strand5p = self._strand5p olg._is_circular = self._is_circular olg._props = self._props.copy() # update the name olg._props['name'] = "oligo%s" % str(id(olg))[-4:] return olg # end def def dump(self): """ Return dictionary of this oligo and its properties. It's expected that caller will copy the properties if mutating Returns: dict: """ s5p = self._strand5p key = { 'id_num': s5p.idNum(), 'idx5p': s5p.idx5Prime(), 'is_5p_fwd': s5p.isForward(), 'is_circular': self._is_circular, 'sequence': self.sequence() } key.update(self._props) return key # end def ### SIGNALS ### oligoRemovedSignal = ProxySignal(CNObject, CNObject, name='oligoRemovedSignal') """part, self""" oligoSequenceAddedSignal = ProxySignal(CNObject, name='oligoSequenceAddedSignal') """self""" oligoSequenceClearedSignal = ProxySignal(CNObject, name='oligoSequenceClearedSignal') """self""" oligoPropertyChangedSignal = ProxySignal(CNObject, object, object, name='oligoPropertyChangedSignal') """self, property_name, new_value""" ### SLOTS ### ### ACCESSORS ### def getProperty(self, key): return self._props[key] # end def def getOutlineProperties(self): """Convenience method for the outline view Returns: tuple: (<name>, <color>, <is_visible>) """ props = self._props return props['name'], props['color'], props['is_visible'] # end def def getModelProperties(self): """Return a reference to the property dictionary Returns: dict: """ return self._props # end def def setProperty(self, key, value, use_undostack=True): if use_undostack: c = SetPropertyCommand([self], key, value) self.undoStack().push(c) else: self._setProperty(key, value) # end def def _setProperty(self, key, value, emit_signals=False): self._props[key] = value if emit_signals: self.oligoPropertyChangedSignal.emit(self, key, value) # end def def getName(self): return self._props['name'] # end def def getColor(self): color = self._props['color'] try: if color is None: print(self._props) raise ValueError("Whhat Got None???") except Exception: exc_type, exc_value, exc_traceback = sys.exc_info() traceback.print_tb(exc_traceback, limit=5, file=sys.stdout) traceback.print_stack() sys.exit(0) return color # end def def _setColor(self, color): """Set this oligo color. Args: color (str): format '#ffffff' """ if color is None: raise ValueError("Whhat None???") self._props['color'] = color # end def def _setLength(self, length, emit_signals): before = self.shouldHighlight() key = 'length' self._props[key] = length if emit_signals and before != self.shouldHighlight(): self.oligoSequenceClearedSignal.emit(self) self.oligoPropertyChangedSignal.emit(self, key, length) # end def def locString(self): vh_num = self._strand5p.idNum() idx = self._strand5p.idx5Prime() return "%d[%d]" % (vh_num, idx) # end def def part(self): return self._part # end def def strand5p(self): """The 5' strand is the first contiguous segment of the oligo, representing the region between the 5' base to the first crossover or 3' end. Returns: Strand: the 5'-most strand of the oligo. """ return self._strand5p # end def def strand3p(self): """The 3' strand. The last strand in the oligo. See strand5p(). Returns: Strand: the 3'-most strand of the oligo. """ s5p = self._strand5p if self._is_circular: return s5p._strand5p for strand in s5p.generator3pStrand(): pass return strand # end def def setStrand5p(self, strand): self._strand5p = strand # end def def undoStack(self): return self._part.undoStack() # end def ### PUBLIC METHODS FOR QUERYING THE MODEL ### def isCircular(self): """Used for checking if an oligo is circular, or has a 5' and 3' end. Sequences cannot be exported of oligos for which isCircular returns True. See also: Oligo.sequenceExport(). Returns: bool: True if the strand3p is connected to strand5p, else False. """ return self._is_circular # end def def length(self): """ The oligo length in bases. Returns: int: value from the oligo's property dict. """ return self._props['length'] # end def def sequence(self): """Get the sequence applied to this `Oligo` Returns: str or None """ temp = self.strand5p() if not temp: return None if temp.sequence(): return ''.join([ Strand.sequence(strand) for strand in self.strand5p().generator3pStrand() ]) else: return None # end def def sequenceExport(self, output): """ Iterative appending to argument `output` which is a dictionary of lists Args: output (dict): dictionary with keys given in `NucleicAcidPart.getSequences` Returns: dict: output with this oligo's values appended for each key """ part = self.part() vh_num5p = self.strand5p().idNum() strand5p = self.strand5p() idx5p = strand5p.idx5Prime() seq = [] a_seq = [] if self.isCircular(): # print("A loop exists") raise CircularOligoException("Cannot export circular oligo " + self.getName()) for strand in strand5p.generator3pStrand(): seq.append(Strand.sequence(strand, for_export=True)) a_seq.append(Strand.abstractSeq(strand)) if strand.connection3p() is None: # last strand in the oligo vh_num3p = strand.idNum() idx3p = strand.idx3Prime() a_seq = ','.join(a_seq) a_seq = "(%s)" % (a_seq) modseq5p, modseq5p_name = part.getStrandModSequence( strand5p, idx5p, ModType.END_5PRIME) modseq3p, modseq3p_name = part.getStrandModSequence( strand, idx3p, ModType.END_3PRIME) seq = ''.join(seq) seq = modseq5p + seq + modseq3p # output = "%d[%d]\t%d[%d]\t%s\t%s\t%s\t%s\t(%s)\n" % \ # (vh_num5p, idx5p, vh_num3p, idx3p, self.getColor(), # modseq5p_name, seq, modseq3p_name, a_seq) # these are the keys # keys = ['Start','End','Color', 'Mod5', # 'Sequence','Mod3','AbstractSequence'] output['Start'].append("%d[%d]" % (vh_num5p, idx5p)) output['End'].append("%d[%d]" % (vh_num3p, idx3p)) output['Color'].append(self.getColor()) output['Mod5'].append(modseq5p_name) output['Sequence'].append(seq) output['Mod3'].append(modseq3p_name) output['AbstractSequence'].append(a_seq) return output # end def def shouldHighlight(self): """ Checks if oligo's length falls within the range specified by OLIGO_LEN_BELOW_WHICH_HIGHLIGHT and OLIGO_LEN_BELOW_WHICH_HIGHLIGHT. Returns: bool: True if circular or length outside acceptable range, otherwise False. """ if not self._strand5p: return True if self.length() > MAX_HIGHLIGHT_LENGTH: return False if self.length() < OLIGO_LEN_BELOW_WHICH_HIGHLIGHT: return True if self.length() > OLIGO_LEN_ABOVE_WHICH_HIGHLIGHT: return True return False # end def ### PUBLIC METHODS FOR EDITING THE MODEL ### def remove(self, use_undostack=True): c = RemoveOligoCommand(self) util.doCmd(self, c, use_undostack=use_undostack) # end def def applyAbstractSequences(self): temp = self.strand5p() if not temp: return for strand in temp.generator3pStrand(): strand.applyAbstractSequence() # end def def clearAbstractSequences(self): temp = self.strand5p() if not temp: return for strand in temp.generator3pStrand(): strand.clearAbstractSequence() # end def def displayAbstractSequences(self): temp = self.strand5p() if not temp: return for strand in temp.generator3pStrand(): strand.copyAbstractSequenceToSequence() # end def def applyColor(self, color, use_undostack=True): if color == self.getColor(): return # oligo already has this color c = ApplyColorCommand(self, color) util.doCmd(self, c, use_undostack=use_undostack) # end def def applySequence(self, sequence, use_undostack=True): c = ApplySequenceCommand(self, sequence) util.doCmd(self, c, use_undostack=use_undostack) # end def def applySequenceCMD(self, sequence): return ApplySequenceCommand(self, sequence) # end def def _setLoop(self, bool): self._is_circular = bool # end def def getStrandLengths(self): """ Traverses the oligo and builds up a list of each strand's total length, which also accounts for any insertions, deletions, or modifications. Returns: list: lengths of individual strands in the oligo """ strand5p = self.strand5p() strand_lengths = [] for strand in strand5p.generator3pStrand(): strand_lengths.append(strand.totalLength()) return strand_lengths def getNumberOfBasesToEachXover(self, use_3p_idx=False): """ Convenience method to get a list of absolute distances from the 5' end of the oligo to each of the xovers in the oligo. Args: use_3p_idx: Adds a 1-base office to return 3' xover idx instead of """ strand5p = self.strand5p() num_bases_to_xovers = [] offset = 1 if use_3p_idx else 0 # 3p xover idx is always the next base i = 0 for strand in strand5p.generator3pStrand(): if strand.connection3p(): i = i + strand.totalLength() num_bases_to_xovers.append(i + offset) return num_bases_to_xovers def splitAtAbsoluteLengths(self, len_list): self._part.splitOligoAtAbsoluteLengths(self, len_list) # end def ### PUBLIC SUPPORT METHODS ### def addToPart(self, part, emit_signals=False): self._part = part self.setParent(part) part._addOligoToSet(self, emit_signals) # end def def getAbsolutePositionAtLength(self, len_for_pos): """ Convenience method convert the length in bases from the 5' end of the oligo to the absolute position (vh, strandset, baseidx). Args: len_for_pos: length in bases for position lookup """ strand5p = self.strand5p() temp_len = 0 for strand in strand5p.generator3pStrand(): if (temp_len + strand.totalLength()) > len_for_pos: vh = strand.idNum() strandset = strand.strandType() baseidx = strand.idx5Prime() + len_for_pos - temp_len return (vh, strandset, baseidx) else: temp_len += strand.totalLength() return None # end def def setPart(self, part): self._part = part self.setParent(part) # end def def destroy(self): # QObject also emits a destroyed() Signal # self.setParent(None) # self.deleteLater() cmds = [] s5p = self._strand5p for strand in s5p.generator3pStrand(): cmds += strand.clearDecoratorCommands() # end for cmds.append(RemoveOligoCommand(self)) return cmds # end def def _decrementLength(self, delta, emit_signals): self._setLength(self.length() - delta, emit_signals) # end def def _incrementLength(self, delta, emit_signals): self._setLength(self.length() + delta, emit_signals) # end def def refreshLength(self, emit_signals=False): temp = self.strand5p() if not temp: return length = 0 for strand in temp.generator3pStrand(): length += strand.totalLength() self._setLength(length, emit_signals) # end def def removeFromPart(self, emit_signals=False): """This method merely disconnects the object from the model. It still lives on in the undoStack until clobbered Note: don't set self._part = None because we need to continue passing the same reference around. """ self._part._removeOligoFromSet(self, emit_signals) self.setParent(None) # end def def _strandMergeUpdate(self, old_strand_low, old_strand_high, new_strand): """This method sets the isCircular status of the oligo and the oligo's 5' strand. """ # check loop status if old_strand_low.oligo() == old_strand_high.oligo(): self._is_circular = True self._strand5p = new_strand return # leave the _strand5p as is? # end if # Now get correct 5p end to oligo if old_strand_low.isForward(): if old_strand_low.connection5p() is not None: self._strand5p = old_strand_low.oligo()._strand5p else: self._strand5p = new_strand else: if old_strand_high.connection5p() is not None: self._strand5p = old_strand_high.oligo()._strand5p else: self._strand5p = new_strand # end if # end def def _strandSplitUpdate(self, new_strand5p, new_strand3p, oligo3p, old_merged_strand): """If the oligo is a loop, splitting the strand does nothing. If the oligo isn't a loop, a new oligo must be created and assigned to the new_strand and everything connected to it downstream. """ # if you split it can't be a loop self._is_circular = False if old_merged_strand.oligo().isCircular(): self._strand5p = new_strand3p return else: if old_merged_strand.connection5p() is None: self._strand5p = new_strand5p else: self._strand5p = old_merged_strand.oligo()._strand5p oligo3p._strand5p = new_strand3p