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
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