summaryrefslogtreecommitdiff
path: root/cad/src/dna/operations/ops_pam.py
blob: f8dc69c752c2d61aa38ffd0ef3e649e2a47051d2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# Copyright 2008 Nanorex, Inc.  See LICENSE file for details.
"""
ops_pam.py - PAM3+5 <-> PAM5 conversion operations

@author: Bruce
@version: $Id$
@copyright: 2008 Nanorex, Inc.  See LICENSE file for details.
"""

from utilities.Log import greenmsg, redmsg, orangemsg

from platform_dependent.PlatformDependent import fix_plurals
import foundation.env as env

from utilities.GlobalPreferences import debug_pref_enable_pam_convert_sticky_ends
from utilities.GlobalPreferences import debug_pref_remove_ghost_bases_from_pam3

from dna.updater.dna_updater_globals import _f_baseatom_wants_pam

from utilities.constants import MODEL_PAM3, MODEL_PAM5

from model.elements import Singlet, Pl5

# ==

class ops_pam_Mixin:
    """
    Mixin class for providing these methods to class Part
    """

    # these commands have toolbuttons:

    def convertPAM3to5Command(self):
        commandname = "Convert PAM3 to PAM5"
        which_pam = MODEL_PAM5
        self._convert_selection_to_pam_model( which_pam, commandname)
        return

    def convertPAM5to3Command(self):
        commandname = "Convert PAM5 to PAM3"
        which_pam = MODEL_PAM3
        self._convert_selection_to_pam_model( which_pam, commandname)
        return

    # these commands have no toolbuttons, for now,
    # and probably never should. For debugging (or in case they
    # are occasionally useful to users), they'll probably be given
    # debug menu commands.

    def convertPAM3to5_noghosts_Command(self):
        commandname = "Convert PAM3 to PAM5 no ghosts"
        which_pam = MODEL_PAM5
        self._convert_selection_to_pam_model( which_pam, commandname, make_ghost_bases = False)
        return

    ## def convertPAM5to3_leaveghosts_Command(self):
    ##     commandname = "Convert PAM5 to PAM3 leave ghosts"
    ##     which_pam = MODEL_PAM3
    ##     self._convert_selection_to_pam_model( which_pam, commandname, remove_ghost_bases_from_PAM3 = False)
    ##     return

    ## def makePlaceholderBasesCommand(self): ...
    # this command (to make ghost bases but do nothing else)
    # could be factored out from _convert_selection_to_pam_model below,
    # but not trivially, since all the history messages and
    # some of the return-since-no-work conditions need changes

    def _convert_selection_to_pam_model(self,
                                        which_pam,
                                        commandname = "",
                                        make_ghost_bases = True, # only implemented for PAM3, so far
                                        ## remove_ghost_bases_from_PAM3 = True
                                       ): #bruce 080413
        """
        Convert the selected atoms (including atoms into selected chunks),
        which don't have errors (in the atoms or their dnaladders), into
        the specified pam model (some, none, or all might already be
        in that pam model), along with all atoms in the same basepairs,
        but only for kinds of ladders for which conversion is yet
        implemented. Print summaries to history.

        This is a user operation, so the dna updater has run
        and knows which atoms are errors, knows ladders of atoms, etc.
        We take advantage of that to simplify the implementation.
        """
        if not commandname:
            commandname = "Convert to %s" % which_pam # kluge, doesn't matter yet

        # find all selected atoms (including those in selected chunks)
        atoms = dict(self.selatoms)
        for chunk in self.selmols:
            atoms.update(chunk.atoms)

        if not atoms:
            env.history.message( greenmsg(commandname + ": ") + redmsg("Nothing selected.") )
            return

        # expand them to cover whole basepairs -- use ladders to help?
        # (the atoms with errors are not in valid ladders, so that
        #  is also an easy way to exclude those)

        num_atoms_with_good_ladders = 0
        ladders = {}
        ghost_bases = {} # initially only holds PAM5 ghost bases

        for atom in atoms.itervalues():
            try:
                ladder = atom.molecule.ladder
            except AttributeError: # for .ladder
                continue # not the right kind of atom, etc
            if not ladder or not ladder.valid:
                continue
            if ladder.error:
                continue
            if not ladder.strand_rails: # bare axis
                continue
            num_atoms_with_good_ladders += 1
            ladders[ladder] = ladder
            # note: if atom is Pl, its Ss neighbors are treated specially
            # lower down in this method
            if atom.ghost and atom.element.pam == MODEL_PAM5:
                ghost_bases[atom.key] = atom
            continue

        orig_len_atoms = len(atoms) # for history messages, in case we add some

        # now iterate on the ladders, scanning their atoms to find the ones
        # in atoms, noting every touched baseindex

        # BUG: this does not notice Pls which are in atoms without either neighbor Ss being in atoms.
        # Future: fix by noticing them above in atom loop; see comment there.

        atoms_to_convert = {}
        ladders_to_inval = {}

        number_of_basepairs_to_convert = 0
        number_of_unpaired_bases_to_convert = 0

        ladders_needing_ghost_bases = {} # maps ladder to (ladder, list of indices) #bruce 080528

        for ladder in ladders:
            # TODO: if ladder can't convert (nim for that kind of ladder),
            # say so as a summary message, and skip it. (But do we know how
            # many atoms in our dict it had? if possible, say that too.)

            # TODO: if ladder doesn't need to convert (already in desired model),
            # skip it.

            length = len(ladder)
            index_set = {} # base indexes in ladder of basepairs which touch our dict of atoms
            rails = ladder.all_rails()
            if len(ladder.strand_rails) not in (1, 2):
                continue
            for rail in rails:
                for ind in range(length):
                    atom = rail.baseatoms[ind]
                    if atom.key in atoms:
                        # convert this base pair
                        index_set[ind] = ind
                        pass
                    continue
                continue
            # conceivable that for some ladders we never hit them;
            # for now, warn but skip them in that case
            if not index_set:
                print "unexpected: scanned %r but found nothing to convert (only Pls selected??)" % ladder # env.history?
            else:
                if len(ladder.strand_rails) == 2:
                    number_of_basepairs_to_convert += len(index_set)
                else:
                    number_of_unpaired_bases_to_convert += len(index_set)
                        # note: we do this even if the conversion will fail
                        # (as it does initially for single strand domains),
                        # since the summary error message from that is useful.
                    if make_ghost_bases and ladder.can_make_ghost_bases():
                        # initially, this test rules out free floating single strands;
                        # later we might be able to do this for them, which is why
                        # we do the test using that method rather than directly.
                        ladders_needing_ghost_bases[ladder] = (ladder, index_set.values())
                # see related code in _cmd_convert_to_pam method
                # in DnaLadder_pam_conversion.py
                ladders_to_inval[ladder] = ladder
                if 0 in index_set or (length - 1) in index_set:
                    for ladder2 in ladder.strand_neighbor_ladders():
                        # might contain Nones or duplicate entries
                        if ladder2 is not None:
                            ladders_to_inval[ladder2] = ladder2 # overkill if only one ind above was found
                for rail in rails:
                    baseatoms = rail.baseatoms
                    for ind in index_set:
                        atom = baseatoms[ind]
                        atoms_to_convert[atom.key] = atom # note: we also add ghost base atoms, below
                pass
            continue # next ladder

        if not atoms_to_convert:
            assert not number_of_basepairs_to_convert
            assert not number_of_unpaired_bases_to_convert
            assert not ladders_needing_ghost_bases
            if num_atoms_with_good_ladders < orig_len_atoms:
                # warn if we're skipping some atoms [similar code occurs twice in this method]
                msg = "%d atom(s) skipped, since not in valid, error-free DnaLadders"
                env.history.message( greenmsg(commandname + ": ") + orangemsg("Warning: " + fix_plurals(msg)))
            env.history.message( greenmsg(commandname + ": ") + redmsg("Nothing found to convert.") )
            return

        # print a message about what we found to convert
        what1 = what2 = ""
        if number_of_basepairs_to_convert:
            what1 = fix_plurals( "%d basepair(s)" % number_of_basepairs_to_convert )
        if number_of_unpaired_bases_to_convert:
            # doesn't distinguish sticky ends from free-floating single strands (fix?)
            what2 = fix_plurals( "%d unpaired base(s)" % number_of_unpaired_bases_to_convert )
        if what1 and what2:
            what = what1 + " and " + what2
        else:
            what = what1 + what2

        env.history.message( greenmsg(commandname + ": ") + "Will convert %s ..." % what )

        # warn if we're skipping some atoms [similar code occurs twice in this method]
        if num_atoms_with_good_ladders < orig_len_atoms:
            msg = "%d atom(s) skipped, since not in valid, error-free DnaLadders"
            env.history.message( orangemsg("Warning: " + fix_plurals(msg)))

        print "%s will convert %d atoms, touching %d ladders" % \
              ( commandname, len(atoms_to_convert), len(ladders_to_inval) )

        # make ghost bases as needed for this conversion (if enabled -- not by default since not yet working ####)
        # (this must not delete any baseatoms in atoms, or run the dna updater
        #  or otherwise put atoms into different ladders, but it can make new
        #  atoms in new chunks, as it does)

        if debug_pref_enable_pam_convert_sticky_ends():
            for ladder, index_list in ladders_needing_ghost_bases.itervalues():
                baseatoms = ladder.make_ghost_bases(index_list) # note: index_list is not sorted; that's ok
                    # note: this makes them in a separate chunk, and returns them
                    # as an atom list, but doesn't add the new chunk to the ladder.
                    # the next dna updater run will fix that (making a new ladder
                    # that includes all atoms in baseatoms and the old ladder).
                for ind in index_list:
                    atom = baseatoms[ind]
                    atoms_to_convert[atom.key] = atom

        # cause the dna updater (which would normally run after we return,
        #  but is also explicitly run below) to do the rest of the conversion
        # (and report errors for whatever it can't convert)

        for ladder in ladders_to_inval:
            ladder._dna_updater_rescan_all_atoms()

        for atom in atoms_to_convert:
            _f_baseatom_wants_pam[atom] = which_pam

        # run the dna updater explicitly

        print "about to run dna updater for", commandname
        self.assy.update_parts() # not a part method
            # (note: this catches dna updater exceptions and turns them into redmsgs.)
        print "done with dna updater for", commandname

        if debug_pref_remove_ghost_bases_from_pam3():
            # note: in commented out calling code above, this was a flag
            # option, remove_ghost_bases_from_PAM3;
            # that will be revived if we have a separate command for this.
            #
            # actually we only remove the ones we noticed as PAM5 above,
            # and succeeded in converting to PAM3.
            good = bad = 0
            for atom in ghost_bases.values(): # must not be itervalues
                if atom.element.pam == MODEL_PAM3:
                    good += 1
                    for n in atom.neighbors():
                        if n.is_ghost():
                            ghost_bases[n.key] = n
                else:
                    bad += 1
                    del ghost_bases[atom.key]
                continue
            if good:
                print "removing %d ghost base(s) we converted to PAM3" % good
            if bad:
                print "leaving %d ghost base(s) we didn't convert to PAM3" % bad
            if not bool(good) == bool(ghost_bases): # should never happen
                print "bug: bool(good) != bool(ghost_bases), for", good, ghost_bases
            del good, bad
            if ghost_bases:
                for atom in ghost_bases.itervalues():
                    atom.kill()
                    # todo: probably should use prekill code to avoid
                    # intermediate bondpoint creation, even though there
                    # are not usually a lot of atoms involved at once
                    continue
                print "about to run dna updater 2nd time for", commandname
                self.assy.update_parts()
                print "done with dna updater 2nd time for", commandname
            pass

        env.history.message( greenmsg( commandname + ": " + "Done." ))

        self.assy.w.win_update()

        return

    pass # end of class ops_pam_Mixin

# end