Example #1
0
    def algorithm( ladders, ladder_to_rails_function ):
        """
        [local helper function]
        Given a list of ladders (DnaLadders and/or DnaSingleStrandDomains),
        and ladder_to_rails_function to return a list of certain rails
        of interest to the caller from each ladder, partition the resulting
        rails into connected sets (represented as dicts from id(rail) to rail)
        and return a list of these sets.

        "Connected" means the rails are bonded end-to-end so that they belong
        in the same WholeChain. To find some rail's connected rails, we just use
        atom.molecule.ladder and then look for atom in the rail ends of the same
        type of rail (i.e. the ones found by ladder_to_rails_function).
        """
        # note: this is the 3rd or 4th "partitioner" I've written recently;
        # could there be a helper function for partitioning, like there is
        # for transitive closure (transclose)? [bruce 080119]
        toscan_all = {} # maps id(rail) -> rail, for initial set of rails to scan
        for ladder in ladders:
            for rail in ladder_to_rails_function(ladder):
                toscan_all[id(rail)] = rail
        def collector(rail, dict1):
            """
            function for transclose on a single initial rail:
            remove each rail seen from toscan_all if present;
            store neighbor atom pointers in rail,
            and store neighbor rails themselves into dict1
            """
            toscan_all.pop(id(rail), None)
                # note: forgetting id() made this buggy in a hard-to-notice way;
                # it worked without error, but returned each set multiple times.
            rail._f_update_neighbor_baseatoms() # called exactly once per rail,
                # per dna updater run which encounters it (whether as a new
                # or preexisting rail); implem differs for axis or strand atoms.
                # Notes [080602]:
                # - the fact that we call it even on preexisting rails (not
                #   modified during this dna updater run) might be important,
                #   if any of their neighbor atoms differ. OTOH this might never
                #   happen, since such changes would call changed_structure
                #   on those baseatoms (even if there's an intervening Pl,
                #   as of a recent bugfix).
                # - the order of rail.neighbor_baseatoms doesn't matter here,
                #   but might matter in later code, so it's necessary to make sure
                #   it's consistent for all rails in a length-1 ladder, but ok
                #   to do that either in the above method which sets them,
                #   or in later code which makes them consistent. (As of 080602
                #   it's now done in the above method which sets them.)
            for neighbor_baseatom in rail.neighbor_baseatoms:
                if neighbor_baseatom is not None:
                    rail1 = _find_rail_of_atom( neighbor_baseatom, ladder_to_rails_function )
                    dict1[id(rail1)] = rail1
            return # from collector
        res = [] # elements are data args for WholeChain constructors (or helpers)
        for rail in toscan_all.values(): # not itervalues (modified during loop)
            if id(rail) in toscan_all: # if still there (hasn't been popped)
                toscan = {id(rail) : rail}
                rails_for_wholechain = transclose(toscan, collector)
                res.append(rails_for_wholechain)
        return res
    def ensure_dlist_ready_to_call(
        self, dlist_owner_1
    ):  # e rename the local vars, revise term "owner" in it [070102 cmt]
        """
        [private helper method for use by DisplayListChunk]
           This implements the recursive algorithm described in DisplayListChunk.__doc__.
        dlist_owner_1 should be a DisplistOwner ###term; we use private attrs and/or methods of that class,
        including _key, _recompile_if_needed_and_return_sublists_dict().
           What we do: make sure that dlist_owner_1's display list can be safely called (executed) right after we return,
        with all displists that will call (directly or indirectly) up to date (in their content).
           Note that, in general, we won't know which displists will be called by a given one
        until after we've updated its content (and thereby compiled calls to those displists as part of its content).
           Assume we are only called when our GL context is current and we're not presently compiling a displist in it.
        """
        ###e verify our GL context is current, or make it so; not needed now since only called during some widget's draw call
        assert self.compiling_displist == 0
        toscan = {dlist_owner_1._key: dlist_owner_1}

        def collector(obj1, dict1):
            dlist_owner = obj1
            direct_sublists_dict = (
                dlist_owner._recompile_if_needed_and_return_sublists_dict()
            )  # [renamed from _ensure_self_updated]
            # This says to dlist_owner: if your list is invalid, recompile it (and set your flag saying it's valid);
            # then you know your direct sublists, so we can ask you to return them.
            #   Note: it only has to include sublists whose drawing effects might be invalid.
            # This means, if its own effects are valid, it can optim by just returning {}.
            # [#e Someday it might maintain a dict of invalid sublists and return that. Right now it returns none or all of them.]
            #   Note: during that call, the glpane (self) is modified to know which dlist_owner's list is being compiled.
            dict1.update(direct_sublists_dict)

        seen = transclose(toscan, collector)
        # now, for each dlist_owner we saw, tell it its drawing effects are valid.
        for dlist_owner in seen.itervalues():
            dlist_owner._your_drawing_effects_are_valid()
            # Q: this resets flags which cause inval propogation... does it retain consistency?
            # A: it does it in reverse logic dir and reverse arrow dir (due to transclose) as inval prop, so it's ok.
            # Note: that comment won't be understandable in a month [from 070102]. Need to explain it better. ###doc
        return
Example #3
0
    def ensure_dlist_ready_to_call(
        self, dlist_owner_1
    ):  #e rename the local vars, revise term "owner" in it [070102 cmt]
        """
        [private helper method for use by DisplayListChunk]
           This implements the recursive algorithm described in DisplayListChunk.__doc__.
        dlist_owner_1 should be a DisplistOwner ###term; we use private attrs and/or methods of that class,
        including _key, _recompile_if_needed_and_return_sublists_dict().
           What we do: make sure that dlist_owner_1's display list can be safely called (executed) right after we return,
        with all displists that will call (directly or indirectly) up to date (in their content).
           Note that, in general, we won't know which displists will be called by a given one
        until after we've updated its content (and thereby compiled calls to those displists as part of its content).
           Assume we are only called when our GL context is current and we're not presently compiling a displist in it.
        """
        ###e verify our GL context is current, or make it so; not needed now since only called during some widget's draw call
        assert self.compiling_displist == 0
        toscan = {dlist_owner_1._key: dlist_owner_1}

        def collector(obj1, dict1):
            dlist_owner = obj1
            direct_sublists_dict = dlist_owner._recompile_if_needed_and_return_sublists_dict(
            )  # [renamed from _ensure_self_updated]
            # This says to dlist_owner: if your list is invalid, recompile it (and set your flag saying it's valid);
            # then you know your direct sublists, so we can ask you to return them.
            #   Note: it only has to include sublists whose drawing effects might be invalid.
            # This means, if its own effects are valid, it can optim by just returning {}.
            # [#e Someday it might maintain a dict of invalid sublists and return that. Right now it returns none or all of them.]
            #   Note: during that call, the glpane (self) is modified to know which dlist_owner's list is being compiled.
            dict1.update(direct_sublists_dict)

        seen = transclose(toscan, collector)
        # now, for each dlist_owner we saw, tell it its drawing effects are valid.
        for dlist_owner in seen.itervalues():
            dlist_owner._your_drawing_effects_are_valid()
            # Q: this resets flags which cause inval propogation... does it retain consistency?
            # A: it does it in reverse logic dir and reverse arrow dir (due to transclose) as inval prop, so it's ok.
            # Note: that comment won't be understandable in a month [from 070102]. Need to explain it better. ###doc
        return
def update_DNA_groups(new_chunks, new_wholechains):
    """
    @param new_chunks: list of all newly made DnaLadderRailChunks (or modified
                       ones, if that's ever possible)

    @param new_wholechains: list of all newly made WholeChains (or modified
                       ones, if that's ever possible) @@@ doc what we do to them @@@ use this arg

    Make sure that PAM chunks and jigs are inside the correct
    Groups of the correct structure and classes, according to
    the DNA Data Model. These Groups include Groups (which someday
    might be called Blocks in this context), DnaSegments,
    DnaStrands, and DnaGroups. Move nodes or create new groups
    of these kinds, as needed.

    Since the user can't directly modify the insides of a DnaGroup,
    and the maintaining code can be assumed to follow the rules,
    the main focus here is on newly created objects, made by
    old code or by reading old mmp files which don't already
    have them inside the right groups.

    For reading mmp files (old or new) to work smoothly, we may [###decide this! it may already sort of work for chunks...]
    also ask the lower-level code that runs before this point
    to add new chunks into the model next to some older chunk
    that contained one of its atoms (if there was one),
    so that if the old chunk was already in the right place,
    the new one will be too.

    We may also convert existing plain Groups into DnaGroups
    under some circumstances, but this is NIM to start with,
    and may never be worth doing, since some touchups of
    converted old files will always be required. But at least,
    if converted files contain useless singleton Groups
    around newly made DnaGroups, we might discard them except
    for copying their name down (which is essentially the same
    thing).

    @return: None (??)
    """

    # Note: this is not (yet? ever?) enough to fully sanitize newly read
    # mmp files re Group structure. It might be enough for the results of
    # user operations, though. So it's probably better to process mmp files
    # in a separate step, after reading them and before (or after?) running
    # this updater. @@@

    # Note:
    # - before we're called, markers have moved to the right place, died, been made,
    #   so that every wholechain has one controlling marker. But nothing has moved
    #   into new groups in the internal model tree. Markers that need new DnaSegments
    #   or DnaStrands don't yet have them, and might be inside the wrong ones.

    # revise comment:
    # - for segments: [this] tells you which existing or new DnaSegment owns each marker and DnaSegmentChunk. Move nodes.
    # - for strands: ditto; move markers into DnaStrand, and chunks into that or DnaSegment (decide this soon).

    ignore_new_changes("as update_DNA_groups starts",
                       changes_ok=False,
                       debug_print_even_if_none=_DEBUG_GROUPS)

    old_groups = {}

    # find or make a DnaStrand or DnaSegment for each controlling marker
    # (via its wholechain), and move markers in the model tree as needed
    for wholechain in new_wholechains:
        strand_or_segment = wholechain.find_or_make_strand_or_segment()
        for marker in wholechain.all_markers():
            ##            print "dna updater: debug fyi: subloop on ", marker
            old_group = strand_or_segment.move_into_your_members(marker)
            if old_group:
                old_groups[id(old_group)] = old_group

    ignore_new_changes("from find_or_make_strand_or_segment",
                       changes_ok=False,
                       debug_print_even_if_none=_DEBUG_GROUPS)
    # should not change atoms in the ways we track

    # move chunks if needed
    for chunk in new_chunks:
        wholechain = chunk.wholechain  # defined for DnaLadderRailChunks
        assert wholechain  # set by update_PAM_chunks
        # could assert wholechain is in new_wholechains
        strand_or_segment = wholechain.find_strand_or_segment()
        assert strand_or_segment
        old_group = strand_or_segment.move_into_your_members(chunk)
        # (For a StrandChunk will we place it into a DnaStrand (as in this code)
        #  or based on the attached DnaSegment?
        #  Not yet sure; the latter has some advantages and is compatible with current external code [080111].
        #  If we do it that way, then do that first for the new segment chunks, then another pass for the strand chunks.
        #  Above code assumes it goes into its own DnaStrand object; needs review/revision.)
        #
        # MAYBE TODO: We might do part of this when creating the chunk
        # and only do it now if no home existed.
        if old_group:
            old_groups[id(old_group)] = old_group

    ignore_new_changes("from moving chunks and markers into proper Groups",
                       changes_ok=False,
                       debug_print_even_if_none=_DEBUG_GROUPS)

    # Clean up old_groups:
    #
    # [update 080331: comment needs revision, since Block has been deprecated]
    #
    # For any group we moved anything out of (or are about to delete something
    # from now), we assume it is either a DnaSegment or DnaStrand that we moved
    # a chunk or marker out of, or a Block that we delete all the contents of,
    # or a DnaGroup that we deleted everything from (might happen if we mark it
    # all as having new per-atom errors), or an ordinary group that contained
    # ordinary chunks of PAM DNA, or an ordinary group that contained a DnaGroup
    # we'll delete here.
    #
    # In some of these cases, if the group has become
    # completely empty we should delete it. In theory we should ask the group
    # whether to do this. Right now I think it's correct for all the kinds listed
    # so I'll always do it.
    #
    # If the group now has exactly one member, should we dissolve it
    # (using ungroup)? Not for a 1-member DnaSomething, probably not
    # for a Block, probably not for an ordinary group, so for now,
    # never do this. (Before 080222 we might do this even for a
    # DnaSegment or DnaStrand, I think -- probably a bug.)
    #
    # Note, we need to do the innermost (deepest) ones first.
    # We also need to accumulate new groups that we delete things from,
    # or more simply, just transclose to include all dads from the start.
    # (Note: only correct since we avoid dissolving groups that are equal to
    #  or outside the top of a selection group. Also assumes we never dissolve
    #  singletons; otherwise we'd need to record which groups not in our
    #  original list we delete things from below, and only consider those
    #  (and groups originally in our list) for dissolution.)

    from foundation.state_utils import transclose  # todo: make toplevel import

    def collector(group, dict1):
        # group can't be None, but an optim to earlier code might change that,
        # so permit it here
        if group and group.dad:
            dict1[id(group.dad)] = group.dad

    transclose(old_groups, collector)

    depth_group_pairs = [(group.node_depth(), group)
                         for group in old_groups.itervalues()]
    depth_group_pairs.sort()
    depth_group_pairs.reverse()  # deepest first

    for depth_junk, old_group in depth_group_pairs:
        if old_group.is_top_of_selection_group() or \
           old_group.is_higher_than_selection_group():
            # too high to dissolve
            continue
        if len(old_group.members) == 0:
            # todo: ask old_group whether to do this:
            old_group.kill()
            # note: affects number of members of less deep groups
        # no need for this code when that len is 1:
        ## old_group.ungroup()
        ##     # dissolves the group; could use in length 0 case too
        continue

    ignore_new_changes("from trimming groups we removed things from",
                       changes_ok=False,
                       debug_print_even_if_none=_DEBUG_GROUPS)

    ignore_new_changes("as update_DNA_groups returns", changes_ok=False)

    return  # from update_DNA_groups
def update_DNA_groups( new_chunks, new_wholechains ):
    """
    @param new_chunks: list of all newly made DnaLadderRailChunks (or modified
                       ones, if that's ever possible)

    @param new_wholechains: list of all newly made WholeChains (or modified
                       ones, if that's ever possible) @@@ doc what we do to them @@@ use this arg

    Make sure that PAM chunks and jigs are inside the correct
    Groups of the correct structure and classes, according to
    the DNA Data Model. These Groups include Groups (which someday
    might be called Blocks in this context), DnaSegments,
    DnaStrands, and DnaGroups. Move nodes or create new groups
    of these kinds, as needed.

    Since the user can't directly modify the insides of a DnaGroup,
    and the maintaining code can be assumed to follow the rules,
    the main focus here is on newly created objects, made by
    old code or by reading old mmp files which don't already
    have them inside the right groups.

    For reading mmp files (old or new) to work smoothly, we may [###decide this! it may already sort of work for chunks...]
    also ask the lower-level code that runs before this point
    to add new chunks into the model next to some older chunk
    that contained one of its atoms (if there was one),
    so that if the old chunk was already in the right place,
    the new one will be too.

    We may also convert existing plain Groups into DnaGroups
    under some circumstances, but this is NIM to start with,
    and may never be worth doing, since some touchups of
    converted old files will always be required. But at least,
    if converted files contain useless singleton Groups
    around newly made DnaGroups, we might discard them except
    for copying their name down (which is essentially the same
    thing).

    @return: None (??)
    """

    # Note: this is not (yet? ever?) enough to fully sanitize newly read
    # mmp files re Group structure. It might be enough for the results of
    # user operations, though. So it's probably better to process mmp files
    # in a separate step, after reading them and before (or after?) running
    # this updater. @@@
    
    # Note:
    # - before we're called, markers have moved to the right place, died, been made,
    #   so that every wholechain has one controlling marker. But nothing has moved
    #   into new groups in the internal model tree. Markers that need new DnaSegments
    #   or DnaStrands don't yet have them, and might be inside the wrong ones.

    # revise comment:
    # - for segments: [this] tells you which existing or new DnaSegment owns each marker and DnaSegmentChunk. Move nodes.
    # - for strands: ditto; move markers into DnaStrand, and chunks into that or DnaSegment (decide this soon).

    ignore_new_changes("as update_DNA_groups starts", changes_ok = False,
                       debug_print_even_if_none = _DEBUG_GROUPS)

    old_groups = {}

    # find or make a DnaStrand or DnaSegment for each controlling marker
    # (via its wholechain), and move markers in the model tree as needed
    for wholechain in new_wholechains:
        strand_or_segment = wholechain.find_or_make_strand_or_segment()
        for marker in wholechain.all_markers():
##            print "dna updater: debug fyi: subloop on ", marker
            old_group = strand_or_segment.move_into_your_members(marker)
            if old_group:
                old_groups[id(old_group)] = old_group

    ignore_new_changes("from find_or_make_strand_or_segment",
                       changes_ok = False,
                       debug_print_even_if_none = _DEBUG_GROUPS )
        # should not change atoms in the ways we track
    
    # move chunks if needed
    for chunk in new_chunks:
        wholechain = chunk.wholechain # defined for DnaLadderRailChunks
        assert wholechain # set by update_PAM_chunks
        # could assert wholechain is in new_wholechains
        strand_or_segment = wholechain.find_strand_or_segment()
        assert strand_or_segment
        old_group = strand_or_segment.move_into_your_members(chunk)
            # (For a StrandChunk will we place it into a DnaStrand (as in this code)
            #  or based on the attached DnaSegment?
            #  Not yet sure; the latter has some advantages and is compatible with current external code [080111].
            #  If we do it that way, then do that first for the new segment chunks, then another pass for the strand chunks.
            #  Above code assumes it goes into its own DnaStrand object; needs review/revision.)
            #
            # MAYBE TODO: We might do part of this when creating the chunk
            # and only do it now if no home existed.
        if old_group:
            old_groups[id(old_group)] = old_group

    ignore_new_changes("from moving chunks and markers into proper Groups",
                       changes_ok = False,
                       debug_print_even_if_none = _DEBUG_GROUPS )

    # Clean up old_groups:
    #
    # [update 080331: comment needs revision, since Block has been deprecated]
    # 
    # For any group we moved anything out of (or are about to delete something
    # from now), we assume it is either a DnaSegment or DnaStrand that we moved
    # a chunk or marker out of, or a Block that we delete all the contents of,
    # or a DnaGroup that we deleted everything from (might happen if we mark it
    # all as having new per-atom errors), or an ordinary group that contained
    # ordinary chunks of PAM DNA, or an ordinary group that contained a DnaGroup
    # we'll delete here.
    #
    # In some of these cases, if the group has become
    # completely empty we should delete it. In theory we should ask the group
    # whether to do this. Right now I think it's correct for all the kinds listed
    # so I'll always do it.
    #
    # If the group now has exactly one member, should we dissolve it
    # (using ungroup)? Not for a 1-member DnaSomething, probably not
    # for a Block, probably not for an ordinary group, so for now,
    # never do this. (Before 080222 we might do this even for a
    # DnaSegment or DnaStrand, I think -- probably a bug.)
    #
    # Note, we need to do the innermost (deepest) ones first.
    # We also need to accumulate new groups that we delete things from,
    # or more simply, just transclose to include all dads from the start.
    # (Note: only correct since we avoid dissolving groups that are equal to
    #  or outside the top of a selection group. Also assumes we never dissolve
    #  singletons; otherwise we'd need to record which groups not in our
    #  original list we delete things from below, and only consider those
    #  (and groups originally in our list) for dissolution.)

    from foundation.state_utils import transclose # todo: make toplevel import
    def collector(group, dict1):
        # group can't be None, but an optim to earlier code might change that,
        # so permit it here
        if group and group.dad:
            dict1[id(group.dad)] = group.dad
    transclose( old_groups, collector)

    depth_group_pairs = [ (group.node_depth(), group)
                          for group in old_groups.itervalues() ]
    depth_group_pairs.sort()
    depth_group_pairs.reverse() # deepest first
    
    for depth_junk, old_group in depth_group_pairs:
        if old_group.is_top_of_selection_group() or \
           old_group.is_higher_than_selection_group():
            # too high to dissolve
            continue
        if len(old_group.members) == 0:
            # todo: ask old_group whether to do this:
            old_group.kill()
            # note: affects number of members of less deep groups
        # no need for this code when that len is 1:
        ## old_group.ungroup()
        ##     # dissolves the group; could use in length 0 case too
        continue

    ignore_new_changes("from trimming groups we removed things from",
                       changes_ok = False,
                       debug_print_even_if_none = _DEBUG_GROUPS )
    
    ignore_new_changes("as update_DNA_groups returns", changes_ok = False )

    return # from update_DNA_groups