Ejemplo n.º 1
0
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
Ejemplo n.º 2
0
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
Ejemplo n.º 3
0
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
Ejemplo n.º 4
0
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
Ejemplo n.º 5
0
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)
Ejemplo n.º 6
0
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)
Ejemplo n.º 7
0
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