Example #1
0
def update_PAM_atoms_and_bonds(changed_atoms):
    """
    Update PAM atoms and bonds.

    @param changed_atoms: an atom.key -> atom dict of all changed atoms
                          that this update function needs to consider,
                          which includes no killed atoms. THIS WILL BE
                          MODIFIED to include all atoms changed herein,
                          and to remove any newly killed atoms.

    @return: None
    """
    # ==

    # fix atom & bond classes, and break locally-illegal bonds

    # (Note that the dna updater only records changed bonds
    #  by recording both their atoms as changed -- it has no
    #  list of "changed bonds" themselves.)

    # note: these fix_ functions are called again below, on new atoms.

    fix_atom_classes( changed_atoms)

    fix_bond_classes( changed_atoms)
        # Fixes (or breaks if locally-illegal) all bonds of those atoms.
        # ("Locally" means "just considering that bond and its two atoms,
        #  not worrying about other bonds on those atoms. ### REVIEW: true now? desired?)

        # NOTE: new bondpoints must be given correct classes by bond.bust,
        # since we don't fix them in this outer method!
        # (Can this be done incrementally? ### REVIEW)
        # IMPLEM THAT ###

    # depending on implem, fixing classes might record more atom changes;
    # if so, those functions also fixed the changed_atoms dict we passed in,
    # and we can ignore whatever additional changes were recorded:

    ignore_new_changes( "from fixing atom & bond classes")

    # ==

    # fix deprecated elements, and the classes of any new objects this creates
    # (covering all new atoms, and all their bonds)

    # (note: we might also extend this to do PAM 3/3+5/5 conversions. not sure.)

    fix_deprecated_elements( changed_atoms) # changes more atoms;
        # implem is allowed to depend on atom & bond classes being correct

    # NOTE: this may kill some atoms that remain in changed_atoms
    # or get added to it below. Subroutines must tolerate killed atoms
    # until we remove them again.

    # Grab new atoms the prior step made, to include in subsequent steps.
    # (Note also that atoms already in changed_atoms might have been killed
    #  and/or transmuted during the prior step.)

    new_atoms = get_changes_and_clear()

    if new_atoms:

        fix_atom_classes( new_atoms)
            # must tolerate killed atoms
        fix_bond_classes( new_atoms)
            # sufficient, since any changed bonds (if alive) must be new bonds.

            # @@@@ did that break illegal bonds, or do we do that somewhere else? [080117 Q]

        # Note: do changed_atoms.update only after fixing classes on new_atoms,
        # so any new atoms that replace old ones in new_atoms also make it
        # into changed_atoms. Note that this effectively replaces all old atoms
        # in changed_atoms. (Note that this only works because new atoms have
        # the same keys as old ones. Also note that none of this matters as of
        # 071120, since fixing classes doesn't make new objects in the present
        # implem.)

        changed_atoms.update( new_atoms )

        ignore_new_changes( "from fixing classes after fixing deprecated elements")

    # ==

    # delete bare atoms (axis atoms without strand atoms, or vice versa).

    if not pref_permit_bare_axis_atoms():
        delete_bare_atoms( changed_atoms)
        # must tolerate killed atoms; can kill more atoms and break bonds,
        # but we can ignore those changes; BUT it can change neighbor atom
        # structure, and those changes are needed by subsequent steps
        # (though no need to fix their classes or look for bareness again,
        #  as explained inside that function)

        # Grab newly changed atoms from that step (neighbors of deleted atoms),
        # to include in subsequent steps. (But no need to fix their classes
        # or again call delete_bare_atoms, as explained inside that function.)
        # (Note also that atoms already in changed_atoms might have been killed
        #  during that step.)

        new_atoms = get_changes_and_clear()

        if new_atoms:
            if debug_flags.DEBUG_DNA_UPDATER:
                print "dna_updater: will scan %d new changes from delete_bare_atoms" % len(new_atoms)
            changed_atoms.update( new_atoms )

    # ==

    # Fix local directional bond issues:
    # - directional bond chain branches (illegal)
    # - missing bond directions (when fixable locally -- most are not)
    # - inconsistent bond directions
    #
    # The changes caused by these fixes include only:
    # - setting atom._dna_updater__error to a constant error code string on some atoms
    # - setting or unsetting bond direction on open bonds (changes from this could be ignored here)

    # Tentative conclusion: no need to do anything to new changed atoms
    # except scan them later; need to ignore atoms with _dna_updater__error set
    # when encountered in changed_atoms (remove them now? or in that function?)
    # and to stop on them when finding chains or rings.

    # [Note: geometric warnings (left-handed DNA, major groove on wrong side)
    # are not yet implemented. REVIEW whether they belong here or elsewhere --
    # guess: later, once chains and ladders are known. @@@@]

    fix_local_bond_directions( changed_atoms)

    new_atoms = get_changes_and_clear()

    if new_atoms:
        if debug_flags.DEBUG_DNA_UPDATER:
            print "dna_updater: will scan %d new changes from fix_local_bond_directions" % len(new_atoms)
        changed_atoms.update( new_atoms )

    # ==

    remove_killed_atoms( changed_atoms)

    remove_error_atoms( changed_atoms)

    # replaced with code at a later stage [bruce 080408];
    # we might revive this to detect Pl5 atoms that are bridging Ss3 atoms
    # (an error to fix early to prevent bugs)
    # or to make a note of ladders that need automatic conversion
    # due to displaying PAM5 in PAM35 form by default
    #
##    # ==
##
##    # Convert from PAM5 to PAM3+5, per-atom part. [080312, unfinished]
##
##    # Potential optimization (not known whether it'd be significant):
##    # only do this after operations that might introduce new PAM5 atoms
##    # (like mmp read, mmp insert, or perhaps Dna Generator)
##    # or that change any setting that causes them to become convertable.
##    # This is not practical now, since errors or non-whole base pairs prevent
##    # conversion, and almost any operation can remove those errors or make the
##    # base pairs whole.
##
##    convert_from_PAM5( changed_atoms)
##        # note: this replaces Pl5 with direct bonds, and may do more (undecided),
##        # but some conversion might be done later after ladders are constructed.
##        # So it might be misnamed. ###
##
##    ignore_new_changes( "from converting PAM5 to PAM3+5")
##
##    remove_killed_atoms( changed_atoms)

    return # from update_PAM_atoms_and_bonds
Example #2
0
def _full_dna_update_0( _runcount):
    """
    [private helper for full_dna_update -- do all the work]
    """

    # TODO: process _f_baseatom_wants_pam: (or maybe a bit later, after delete bare, and error finding?)
    # - extend to well-structured basepairs; drop structure error atoms (as whole basepairs)
    # - these and their baseatom neighbors in our changed atoms, maybe even real .changed_structure

    changed_atoms = get_changes_and_clear()

    debug_prints_as_dna_updater_starts( _runcount, changed_atoms)
        # note: this function should not modify changed_atoms.
        # note: the corresponding _ends call is in our caller.

    if not changed_atoms and not _f_are_there_any_homeless_dna_markers():
        # maybe: also check _f_baseatom_wants_pam, invalid ladders, here and elsewhere
        # (or it might be more efficient to officially require _changed_structure on representative atoms,
        #  which we're already doing now as a kluge workaround for the lack of testing those here)
        # [bruce 080413 comment]
        #
        # note: adding marker check (2 places) fixed bug 2673 [bruce 080317]
        return # optimization (might not be redundant with caller)

    # print debug info about the set of changed_atoms (and markers needing update)
    if debug_flags.DEBUG_DNA_UPDATER_MINIMAL:
        print "\ndna updater: %d changed atoms to scan%s" % \
              ( len(changed_atoms),
                _f_are_there_any_homeless_dna_markers() and " (and some DnaMarkers)" or ""
              )
    if debug_flags.DEBUG_DNA_UPDATER and changed_atoms:
        # someday: should be _VERBOSE, but has been useful enough to keep seeing for awhile
        items = changed_atoms.items()
        items.sort()
        atoms = [item[1] for item in items]
        NUMBER_TO_PRINT = 10
        if debug_flags.DEBUG_DNA_UPDATER_VERBOSE or len(atoms) <= NUMBER_TO_PRINT:
            print " they are: %r" % atoms
        else:
            print " the first %d of them are: %r ..." % \
                  (NUMBER_TO_PRINT, atoms[:NUMBER_TO_PRINT])

    if changed_atoms:
        remove_killed_atoms( changed_atoms) # only affects this dict, not the atoms

    if changed_atoms:
        remove_closed_or_disabled_assy_atoms( changed_atoms)
            # This should remove all remaining atoms from closed files.
            # Note: only allowed when no killed atoms are present in changed_atoms;
            # raises exceptions otherwise.

    if changed_atoms:
        update_PAM_atoms_and_bonds( changed_atoms)
            # this can invalidate DnaLadders as it changes various things
            # which call atom._changed_structure -- that's necessary to allow,
            # so we don't change dnaladder_inval_policy until below,
            # inside update_PAM_chunks [bruce 080413 comment]
            # (REVIEW: atom._changed_structure does not directly invalidate
            #  dna ladders, so I'm not sure if this comment is just wrong,
            #  or if it meant something not exactly what it said, like,
            #  this can cause more ladders to be invalidated than otherwise
            #  in an upcoming step -- though if it meant that, it seems
            #  wrong too, since the existence of that upcoming step
            #  might be enough reason to not be able to change the policy yet.
            #  [bruce 080529 addendum/Q])

    if not changed_atoms and not _f_are_there_any_homeless_dna_markers() and not _f_invalid_dna_ladders:
        return # optimization

    homeless_markers = _f_get_homeless_dna_markers() #e rename, homeless is an obs misleading term ####
        # this includes markers whose atoms got killed (which calls marker.remove_atom)
        # or got changed in structure (which calls marker.changed_structure)
        # so it should not be necessary to also add to this all markers noticed
        # on changed_atoms, even though that might include more markers than
        # we have so far (especially after we add atoms from invalid ladders below).
        #
        # NOTE: it can include fewer markers than are noticed by _f_are_there_any_homeless_dna_markers
        # since that does not check whether they are truly homeless.
    assert not _f_are_there_any_homeless_dna_markers() # since getting them cleared them

    new_chunks, new_wholechains = update_PAM_chunks( changed_atoms, homeless_markers)
        # note: at the right time during this or a subroutine, it sets
        # dnaladder_inval_policy to DNALADDER_INVAL_IS_ERROR

    # review: if not new_chunks, return? wait and see if there are also new_markers, etc...

    update_DNA_groups( new_chunks, new_wholechains)
        # review:
        # args? a list of nodes, old and new, whose parents should be ok? or just find them all, scanning MT?
        # the underlying nodes we need to place are just chunks and jigs. we can ignore old ones...
        # so we need a list of new or moved ones... chunks got made in update_PAM_chunks; jigs, in update_PAM_atoms_and_bonds...
        # maybe pass some dicts into these for them to add things to?

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

    if debug_flags.DEBUG_DNA_UPDATER_MINIMAL:
        if _f_are_there_any_homeless_dna_markers():
            print "dna updater fyi: as updater returns, some DnaMarkers await processing by next run"
                # might be normal...don't know. find out, by printing it even
                # in minimal debug output. [bruce 080317]

    if _f_invalid_dna_ladders: #bruce 080413
        print "\n*** likely bug: some invalid ladders are recorded, as dna updater returns:", _f_invalid_dna_ladders
        # but don't clear them, in case this was sometimes routine and we were
        # working around bugs (unknowingly) by invalidating them next time around

    restore_dnaladder_inval_policy( DNALADDER_INVAL_IS_OK)

    return # from _full_dna_update_0
Example #3
0
def update_PAM_chunks( changed_atoms, homeless_markers):
    """
    Update chunks containing changed PAM atoms, ensuring that
    PAM atoms remain divided into AxisChunks and StrandChunks
    in the right way. Also update DnaMarkers as needed.

    @param changed_atoms: an atom.key -> atom dict of all changed atoms
                          that this update function needs to consider,
                          which includes no killed atoms. WE ASSUME
                          OWNERSHIP OF THIS DICT and modify it in
                          arbitrary ways.
                          Note: in present calling code [071127]
                          this dict might include atoms from closed files.

    @param homeless_markers: ###doc, ###rename

    @return: the 2-tuple (all_new_chunks, new_wholechains),
             containing a list of all newly made DnaLadderRailChunks
             (or modified ones, if that's ever possible),
             and a list of all newly made WholeChains
             (each of which covers either all new small chains,
              or some new and some old small chains, with each small chain
              also called one "DnaLadder rail" and having its own
              DnaLadderRailChunk).
    """

    # see scratch file for comments to revise and bring back here...

    ignore_new_changes("as update_PAM_chunks starts", changes_ok = False )

    # Move each DnaMarker in which either atom got killed or changed in
    # structure (e.g. rebonded) onto atoms on the same old wholechain
    # (if it has one) which remain alive. We don't yet know if they'll
    # be in the same new wholechain and adjacent in it -- that will be
    # checked when new wholechains are made, and we retain the moved
    # markers so we can assert that they all get covered that way
    # (which they ought to -- todo, doc why).
    #
    # Note that we might find markers which are not on old wholechains
    # (e.g. after mmp read), and we might even find markers like that
    # on killed atoms (e.g. after mmp insert, which kills clipboard).
    # Those markers can't be moved, but if valid without being moved,
    # can be found and used by a new wholechain. [revised 080311]
    #
    # The old wholechain objects still
    # exist within the markers, and can be used to scan their atoms even though
    # some are dead or their bonding has changed. The markers use them to help
    # decide where and how to move. Warning: this will only be true during the early
    # parts of the dna updater run, since the wholechains rely on rail.neighbor_baseatoms
    # to know how their rails are linked, and that will be rewritten later, around the time
    # when new wholechains are made. TODO: assert that we don't rely on that after
    # it's invalid.
    #
    # [code and comment rewritten 080311]

    ## homeless_markers = _f_get_homeless_dna_markers() #e rename # now an arg, 080317
        # this includes markers whose atoms got killed (which calls marker.remove_atom)
        # or got changed in structure (which calls marker.changed_structure)
        # so it should not be necessary to also add to this all markers noticed
        # on changed_atoms, even though that might include more markers than
        # we have so far (especially after we add atoms from invalid ladders below).

    live_markers = []
    for marker in homeless_markers:
        still_alive = marker._f_move_to_live_atompair_step1() #e @@@@ rename (no step1) (also fix @@@)
            # note: we don't yet know if they'll still be alive
            # when this updater run is done.
        if still_alive:
            live_markers.append(marker) # only used for asserts
        if (not not still_alive) != (not marker.killed()):
            print "\n***BUG: still_alive is %r but %r.killed() is %r" % \
                  (still_alive, marker, marker.killed())
        if debug_flags.DEBUG_DNA_UPDATER: # SOON: _VERBOSE
            if still_alive:
                print "dna updater: moved marker %r, still alive after step1" % (marker,)
            else:
                print "dna updater: killed marker %r (couldn't move it)" % (marker,)
    del homeless_markers

    ignore_new_changes("from moving DnaMarkers")
        # ignore changes caused by adding/removing marker jigs
        # to their atoms, when the jigs die/move/areborn

    # make sure invalid DnaLadders are recognized as such in the next step,
    # and dissolved [possible optim: also recorded for later destroy??].
    # also (#e future optim) break long ones at damage points so the undamaged
    # parts needn't be rescanned in the next step.
    #
    # Also make sure that atoms that are no longer in valid ladders
    # (due to dissolved or fragmented ladders) are scanned below,
    # or that the chains they are in are covered. This is necessary so that
    # the found chains below cover all atoms in every "base pair" (Ss-Ax-Ss)
    # they cover any atom in. Presently this is done by
    # adding some or all of their atoms into changed_atoms
    # in the following method.

    dissolve_or_fragment_invalid_ladders( changed_atoms)
        # note: this does more than its name implies:
        # - invalidate all ladders containing changed_atoms
        #   (or touching changed Pl atoms, as of 080529 bugfix);
        # - add all live baseatoms from invalid ladders to changed_atoms
        #   (whether they were invalidated by it or before it was called).
        # see its comments and the comment just before it (above) for details.
        #
        # NOTE: THIS SETS dnaladder_inval_policy to DNALADDER_INVAL_IS_ERROR
        # (at the right time during its side effects/tests on ladders)

    # TODO: make sure _f_baseatom_wants_pam is extended to cover whole basepairs
    # (unless all code which stores into it does that)

    # Find the current axis and strand chains (perceived from current bonding)
    # on which any changed atoms reside, but only scanning along atoms
    # not still part of valid DnaLadders. (I.e. leave existing undamaged
    # DnaLadders alone.) (The found chains or rings will be used below to
    # make new DnaChain and DnaLadder objects, and (perhaps combined with
    # preexisting untouched chains ###CHECK) to make new WholeChains and tell the
    # small chains about them.)

    ignore_new_changes("from dissolve_or_fragment_invalid_ladders", changes_ok = False)

    axis_chains, strand_chains = find_axis_and_strand_chains_or_rings( changed_atoms)

    ignore_new_changes("from find_axis_and_strand_chains_or_rings", changes_ok = False )

    if debug_flags.DNA_UPDATER_SLOW_ASSERTS:
        assert_unique_chain_baseatoms(axis_chains + strand_chains)

    # make ladders

    # Now use the above-computed info to make new DnaLadders out of the chains
    # we just made (which contain all PAM atoms no longer in valid old ladders),
    # and to merge end-to-end-connected ladders (new/new or new/old) into larger
    # ones, when that doesn't make them too long. We'll reverse chains
    # as needed to make the ones in one ladder correspond in direction, and to
    # standardize their strand bond directions. (There is no longer any need to
    # keep track of index_direction -- it might be useful for new/new ladder
    # merging, but that probably doesn't help us since we also need to handle
    # new/old merging. For now, the lower-level code maintains it for individual
    # chains, and when fragmenting chains above, but discards it for merged
    # chains.)

    # Note: we don't need to rotate smallchains that are rings, to align them
    # properly in ladders, since make_new_ladders will break them up as
    # needed into smaller pieces which are aligned.

    new_axis_ladders, new_singlestrand_ladders = make_new_ladders( axis_chains, strand_chains)

    all_new_unmerged_ladders = new_axis_ladders + new_singlestrand_ladders

    ignore_new_changes("from make_new_ladders", changes_ok = False)

    if debug_flags.DNA_UPDATER_SLOW_ASSERTS:
        assert_unique_ladder_baseatoms( all_new_unmerged_ladders)

    # convert pam model of ladders that want to be converted
    # (assume all old ladders that want this were invalidated
    #  and therefore got remade above; do this before merging
    #  in case conversion errors are confined to smaller ladders
    #  that way, maybe for other reasons) [bruce 080401 new feature]

    # note: several of the pam conversion methods might temporarily change
    # dnaladder_inval_policy, but only very locally in the code,
    # and they will change it back to its prior value before returning

    default_pam = pref_dna_updater_convert_to_PAM3plus5() and MODEL_PAM3 or None
        # None means "whatever you already are", i.e. do no conversion
        # except whatever is requested by manual conversion operations.
        # There is not yet a way to say "display everything in PAM5".
        # We will probably need either that, or "convert selection to PAM5",
        # or both. As of 080401 we only have "convert one ladder to PAM5" (unfinished).

    if default_pam or _f_baseatom_wants_pam:
        #bruce 080523 optim: don't always call this
        _do_pam_conversions( default_pam, all_new_unmerged_ladders )

    if _f_invalid_dna_ladders:
        #bruce 080413
        print "\n*** likely bug: _f_invalid_dna_ladders is nonempty " \
              "just before merging new ladders: %r" % _f_invalid_dna_ladders

    # During the merging of ladders, we make ladder inval work normally;
    # at the end, we ignore any invalidated ladders due to the merge.
    # (The discarding is a new feature and possible bugfix, but needs more testing
    #  and review since I'm surprised it didn't show up before, so I might be
    #  missing something) [bruce 080413 1pm PT]

    _old_policy = temporarily_set_dnaladder_inval_policy( DNALADDER_INVAL_IS_OK)
    assert _old_policy == DNALADDER_INVAL_IS_ERROR

    # merge axis ladders (ladders with an axis, and 1 or 2 strands)

    merged_axis_ladders = merge_and_split_ladders( new_axis_ladders,
                                                   debug_msg = "axis" )
        # note: each returned ladder is either entirely new (perhaps merged),
        # or the result of merging new and old ladders.

    ignore_new_changes("from merging/splitting axis ladders", changes_ok = False)

    del new_axis_ladders

    # merge singlestrand ladders (with no axis)
    # (note: not possible for an axis and singlestrand ladder to merge)

    merged_singlestrand_ladders = merge_and_split_ladders( new_singlestrand_ladders,
                                                           debug_msg = "single strand" )
        # not sure if singlestrand merge is needed; split is useful though

    ignore_new_changes("from merging/splitting singlestrand ladders", changes_ok = False)

    del new_singlestrand_ladders

    restore_dnaladder_inval_policy( _old_policy)
    del _old_policy

    _f_clear_invalid_dna_ladders()

    merged_ladders = merged_axis_ladders + merged_singlestrand_ladders

    if debug_flags.DNA_UPDATER_SLOW_ASSERTS:
        assert_unique_ladder_baseatoms( merged_ladders)

    # Now make or remake chunks as needed, so that each ladder-rail is a chunk.
    # This must be done to all newly made or merged ladders (even if parts are old).

    all_new_chunks = []

    for ladder in merged_ladders:
        new_chunks = ladder.remake_chunks()
            # note: this doesn't have an issue about wrongly invalidating the
            # ladders whose new chunks pull atoms into themselves,
            # since when that happens the chunks didn't yet set their .ladder.
        all_new_chunks.extend( new_chunks)
        ladder._f_reposition_baggage() #bruce 080404
            # only for atoms with _f_dna_updater_should_reposition_baggage set
            # (and optimizes by knowing where those might be inside the ladder)
            # (see comments inside it about what might require us
            #  to do it in a separate loop after all chunks are remade)
            ### REVIEW: will this invalidate any ladders?? If so, need to turn that off.

    ignore_new_changes("from remake_chunks and _f_reposition_baggage", changes_ok = True)
        # (changes are from parent chunk of atoms changing;
        #  _f_reposition_baggage shouldn't cause any [#test, using separate loop])

    # Now make new wholechains on all merged_ladders,
    # let them own their atoms and markers (validating any markers found,
    # moved or not, since they may no longer be on adjacent atoms on same wholechain),
    # and choose their controlling markers.
    # These may have existing DnaSegmentOrStrand objects,
    # or need new ones (made later), and in a later step (not in this function)
    # those objects take over their chunks.

    # Note that due to the prior step, any atom in a ladder (new or old)
    # can find its smallchain via its chunk.

    # We'll do axis chains first, in case we want them finalized
    # in figuring out anything about strand markers (not the case for now).
    # For any length-1 axis chains, we'll pick a direction arbitrarily (?),
    # so we can store, for all chains, a pointer to the ladder-index of the
    # chain it connects to (if any).

    # For each kind of chain, the algorithm is handled by this function:
    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

    # Make new wholechains. Note: The constructors call marker methods on all
    # markers found on those wholechains; those methods can kill some of the
    # markers.

    new_wholechains = (
        map( Axis_WholeChain,
             algorithm( merged_axis_ladders,
                        lambda ladder: ladder.axis_rails() ) ) +
        map( Strand_WholeChain,
             algorithm( merged_ladders, # must do both kinds at once!
                        lambda ladder: ladder.strand_rails ) )
     )
    if debug_flags.DEBUG_DNA_UPDATER:
        print "dna updater: made %d new or changed wholechains..." % len(new_wholechains)

    if debug_flags.DNA_UPDATER_SLOW_ASSERTS:
        assert_unique_wholechain_baseatoms(new_wholechains)

    # The new WholeChains should have found and fully updated (or killed)
    # all markers we had to worry about. Assert this -- but only with a
    # debug print, since I might be wrong (e.g. for markers on oldchains
    # of length 1, now on longer ones??) and it ought to be harmless to
    # ignore any markers we missed so far.
    for marker in live_markers:
        marker._f_should_be_done_with_move()
    del live_markers

    # note: those Whatever_WholeChain constructors above also have side effects:
    # - own their atoms and chunks (chunk.set_wholechain)
    # - kill markers no longer on adjacent atoms on same wholechain
    #
    # (REVIEW: maybe use helper funcs so constructors are free of side effects?)
    #
    # but for more side effects we run another loop:
    for wholechain in new_wholechains:
        wholechain.own_markers()
        # - own markers
        # - and (in own_markers)
        #   - choose or make controlling marker,
        #   - and tell markers whether they're controlling (might kill some of them)
    if debug_flags.DEBUG_DNA_UPDATER:
        print "dna updater: owned markers of those %d new or changed wholechains" % len(new_wholechains)

    ignore_new_changes("from making wholechains and owning/validating/choosing/making markers",
                       changes_ok = True)
        # ignore changes caused by adding/removing marker jigs
        # to their atoms, when the jigs die/move/areborn
        # (in this case, they don't move, but they can die or be born)

    # TODO: use wholechains and markers to revise base indices if needed
    # (if this info is cached outside of wholechains)

    return all_new_chunks, new_wholechains # from update_PAM_chunks
Example #4
0
def _do_pam_conversions( default_pam, all_new_unmerged_ladders):
    """
    #doc
    [private helper]
    """
    #bruce 080523 split this out of its sole caller
    # maybe: put it into its own module, or an existing one?

    number_converted = 0 # not counting failures
    number_failed = 0

    ladders_dict = _f_ladders_with_up_to_date_baseframes_at_ends
    if ladders_dict:
        print "***BUG: _f_ladders_with_up_to_date_baseframes_at_ends was found with leftover garbage; clearing it now"
        ladders_dict.clear()

    locator = _f_atom_to_ladder_location_dict
    if locator:
        print "***BUG: _f_atom_to_ladder_location_dict was found with leftover garbage; clearing it now"
        locator.clear()

    if 1:
        # make errors more obvious, bugs less likely;
        # also make it easy & reliable to locate all atoms in new ladders
        # (this could surely be optimized, but simple & reliable is primary
        #  concern for now)

        for ladder in all_new_unmerged_ladders:
            if not ladder.error:
                ladder.clear_baseframe_data()
                ladder._f_store_locator_data()
                for ladder1 in ladder.strand_neighbor_ladders():
                    ladder.clear_baseframe_data()
                    # no need to store locator data for these
        pass

    for ladder in all_new_unmerged_ladders:
        assert ladder.valid, "bug: new ladder %r not valid!" % self
        wanted, succeeded = ladder._f_convert_pam_if_desired(default_pam)
            # - this checks for ladder.error and won't succeed if set
            # - this sets baseframe data if conversion succeeds,
            #   and stores ladder in ladders_dict, with value False
        assert ladder.valid, "bug: _f_convert_pam_if_desired made %r invalid!" % ladder
        didit = wanted and succeeded
        failed = wanted and not succeeded
        number_converted += not not didit
        number_failed += not not failed
        if didit:
            assert ladders_dict.get(ladder, None) == False
            if debug_flags.DEBUG_DNA_UPDATER_VERBOSE:
                print "converted:", ladder.ladder_string()
        continue

    if number_converted:
##        for ladder in all_new_unmerged_ladders:
##            ladders_dict[ladder] = None # TODO: refactor this -- see above comment
        for ladder in all_new_unmerged_ladders:
            if not ladder.error:
                ladder._f_finish_converting_bridging_Pl_atoms()
                    # this assert not ladder.error
                assert ladder.valid, "bug: _f_finish_converting_bridging_Pl_atoms made %r invalid!" % ladder
                ladder.fix_bondpoint_positions_at_ends_of_rails()
                    # (don't pass ladders_dict, it's accessed as the global which
                    #  is assigned to it above [080409 revision])
                    # the ladders in ladders_dict are known to have valid baseframes
                    # (as we start this loop) or valid baseframes at the ends
                    # (as we continue this loop);
                    # this method needs to look at neighboring ladders
                    # (touching ladder at corners) and use end-baseframes from
                    # them; if it sees one not in the dict, it computes its
                    # baseframes (perhaps just at both ends as an optim)
                    # and also stores that ladder in the dict so this won't
                    # be done to it again.
            continue
        pass
    ladders_dict.clear()
    del ladders_dict
    locator.clear()
    del locator
    _f_baseatom_wants_pam.clear()

    # Note: if ladders were converted, their chains are still ok,
    # since those only store baseatoms (for strand and axis),
    # and those transmuted and moved but didn't change identity.
    # So the ladders also don't change identity, and remain valid
    # unless they had conversion errors. If not valid, they are ok
    # in those lists since this was already possible in other ways
    # (I think). (All this needs test and review.)
    #
    # But lots of atoms got changed in lots of ways (transmute, move,
    # rebond, create Pl, kill Pl). Ignore all that.
    # (review: any changes to ignore if conversion wanted and failed?)

    msg = "from converting %d ladders" % number_converted
    if number_failed:
        msg += " (%d desired conversions failed)" % number_failed
        # msg is just for debug, nevermind calling fix_plurals

    if number_converted:
        ignore_new_changes(msg, changes_ok = True)
    else:
        ignore_new_changes(msg, changes_ok = False)

    return # from _do_pam_conversions
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