summaryrefslogtreecommitdiff
path: root/cad/src/operations/ops_rechunk.py
blob: 95601e660949638b5ddcac01bf044945f61cb565 (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 2004-2008 Nanorex, Inc.  See LICENSE file for details. 
"""
ops_rechunk.py -- operations for changing the way atoms are divided
into chunks, without altering the atoms or bonds themselves.

@version: $Id$
@copyright: 2004-2008 Nanorex, Inc.  See LICENSE file for details.

History:

bruce 050507 made this by collecting appropriate methods from class Part.
"""

from utilities.Log import greenmsg, redmsg
from platform_dependent.PlatformDependent import fix_plurals
from model.chunk import Chunk
from utilities.constants import gensym
from utilities.prefs_constants import assignColorToBrokenDnaStrands_prefs_key
from dna.model.Dna_Constants import getNextStrandColor
import foundation.env as env
from utilities.debug_prefs import debug_pref, Choice_boolean_False
from utilities.debug import print_compact_stack
from simulation.sim_commandruns import adjustSinglet

class ops_rechunk_Mixin:
    """
    Mixin class for providing "chunking" (i.e. atom grouping) methods to 
    class Part.
    """

    #m modifySeparate needs to be changed to modifySplit.  Need to coordinate
    # this with Bruce since this is called directly from some mode modules.
    # Mark 050209
    #
    # separate selected atoms into a new Chunk
    # (one new mol for each existing one containing any selected atoms)
    # do not break bonds
    def modifySeparate(self, new_old_callback = None):
        """
        For each Chunk (named N) containing any selected atoms,
        move the selected atoms out of N (but without breaking any bonds)
        into a new Chunk which we name N-frag. If N is now empty, remove it.
        
        @param new_old_callback: If provided, then each time we create a new
            (and nonempty) fragment N-frag, call new_old_callback with the
            2 args N-frag and N (that is, with the new and old molecules).
        @type  new_old_callback: function
        
        @warning: we pass the old mol N to that callback, even if it has no 
                  atoms and we deleted it from this assembly.
        """
        # bruce 040929 wrote or revised docstring, added new_old_callback feature
        # for use from Extrude.
        # Note that this is called both from a tool button and for internal uses.
        # bruce 041222 removed side effect on selection mode, after discussion
        # with Mark and Josh. Also added some status messages.
        # Questions: is it good to refrain from merging all moved atoms into one
        # new mol? If not, then if N becomes empty, should we rename N-frag to N?
        
        cmd = greenmsg("Separate: ")
        
        if not self.selatoms: # optimization, and different status msg
            msg =  redmsg("No atoms selected")
            env.history.message(cmd + msg)
            return
        if 1:
            #bruce 060313 mitigate bug 1627, or "fix it by doing something we'd rather not always have to do" --
            # create (if necessary) a new toplevel group right now (before addmol does), avoiding a traceback
            # when all atoms in a clipboard item part consisting of a single chunk are selected for this op,
            # and the old part.topnode (that chunk) disappears from loss of atoms before we add the newly made chunk
            # containing those same atoms.
            # The only things wrong with this fix are:
            # - It's inefficient (so is the main algorithm, and it'd be easy to rewrite it to be faster, as explained below).
            # - The user ends up with a new Group even if one would theoretically not have been needed.
            #   But that's better than a traceback and disabled session, so for A7 this fix is fine.
            # - The same problem might arise in other situations (though I don't know of any), so ideally we'd
            #   have a more general fix.
            # - It's nonmodular for this function to have to know anything about Parts.
            ##e btw, a simpler way to do part of the following is "part = self". should revise this when time to test it. [bruce 060329]
            someatom = self.selatoms.values()[0] # if atoms in multiple parts could be selected, we'd need this for all their mols
            part = someatom.molecule.part
            part.ensure_toplevel_group()
            # this is all a kluge; a better way would be to rewrite the main algorithm to find the mols
            # with selected atoms, only make numol for those, and add it (addmol) before transferring all the atoms to it.
            pass 
        numolist=[]
        for mol in self.molecules[:]: # new mols are added during the loop!
            numol = Chunk(self.assy, gensym(mol.name + "-frag", self.assy)) # (in modifySeparate)
            for a in mol.atoms.values():
                if a.picked:
                    # leave the moved atoms picked, so still visible
                    a.hopmol(numol)
            if numol.atoms:
                numol.setDisplayStyle(mol.display) # Fixed bug 391.  Mark 050710
                numol.setcolor(mol.color, repaint_in_MT = False)
                    #bruce 070425, fix Extrude bug 2331 (also good for Separate in general), "nice to have" for A9
                self.addmol(numol) ###e move it to just after the one it was made from? or, end of same group??
                numolist+=[numol]
                if new_old_callback:
                    new_old_callback(numol, mol) # new feature 040929
        msg = fix_plurals("Created %d new chunk(s)" % len(numolist))
        env.history.message(cmd + msg)
        self.w.win_update() #e do this in callers instead?

    
    #merge selected molecules together  ###@@@ no update -- does caller do it?? [bruce 050223]
    def merge(self):
        """
        Merges selected atoms into a single chunk, or merges the selected
        chunks into a single chunk.
        
        @note: If the selected atoms belong to the same chunk, nothing happens.
        """
        #mark 050411 changed name from weld to merge (Bug 515)
        #bruce 050131 comment: might now be safe for clipboard items
        # since all selection is now forced to be in the same one;
        # this is mostly academic since there's no pleasing way to use it on them,
        # though it's theoretically possible (since Groups can be cut and maybe copied).
        
        cmd = greenmsg("Combine Chunks: ")

        if self.selatoms:
            self.makeChunkFromSelectedAtoms()
            return
            
        if len(self.selmols) < 2:
            msg = redmsg("Need two or more selected chunks to merge")
            env.history.message(cmd + msg)
            return
        self.changed() #bruce 050131 bugfix or precaution
        mol = self.selmols[0]
        for m in self.selmols[1:]:
            mol.merge(m)
    
    def makeChunkFromSelectedAtoms(self):
        """
        Create a new chunk from the selected atoms.
        """
        
        #ninad 070411 moved the original method out of 'merge' method to 
        #facilitate implementation of 'Create New Chunk 
        #from selected atoms' feature
        
        cmd = greenmsg("Create New Chunk: ")
        if not self.selatoms:
            msg1 = "Create New Chunk: "
            msg2 = redmsg('Select some atoms first to create a new chunk')
            env.history.message(msg1+msg2)
            return
        
        #ninad070411 : Following checks if the selected molecules 
        #belong to more than one chunk. If they don't (i.e. if they are a part of
        # a sinle chunk, it returns from the method with proper histry msg
        
        molList = []
        for atm in self.selatoms.values():
            if not len(molList) > 1:
                mol =  atm.molecule            
                if mol not in molList:
                    molList.append(mol)     
            
        if len(molList) < 2:
            msg1 = "Create New Chunk: "
            msg2 = redmsg('Not created as the selected atoms are part of the \
            same chunk.')
            env.history.message(msg1+msg2)
            return
                    
        #bruce 060329 new feature: work on atoms too (put all selected atoms into a new chunk)
        self.ensure_toplevel_group() # avoid bug for part containing just one chunk, all atoms selected
        numol = Chunk(self.assy, gensym("Chunk", self.assy))
        natoms = len(self.selatoms)
        for a in self.selatoms.values():
            # leave the moved atoms picked, so still visible
            a.hopmol(numol)
        self.addmol(numol)
            #e should we add it in the same groups (and just after the chunks) which these atoms used to belong to?
            # could use similar scheme to placing jigs...
        msg = fix_plurals("made chunk from %d atom(s)" % natoms) # len(numol.atoms) would count bondpoints, this doesn't
        msg = msg.replace('chunk', numol.name)
        env.history.message(cmd + msg)
        self.w.win_update()
        
    def makeChunkFromAtomList(self, 
                              atomList, 
                              name = None, 
                              group = None,
                              color = None):
        """
        Creates a new chunk from the given atom list.
        
        @param atomList: List of atoms from which to create the chunk.
        @type  atomList: list
        
        @param name: Name of new chunk. If None, we'll assign one.
        @type  name: str
        
        @param group: The group to add the new chunk to. If None, the new chunk
                      is added to the bottom of the model tree.
        @type  group: L{Group}
        
        @param color: Color of new chunk. If None, no chunk color is assigned
                      (chunk atoms will be drawn in their element colors).
        @type  color: tuple
        
        @return: The new chunk.
        @rtype:  L{Chunk}
        
        """
        assert atomList
        
        if name:
            newChunk = Chunk(self.assy, name)
        else:
            newChunk = Chunk(self.assy, gensym("Chunk", self.assy))
            
        for a in atomList:
            a.hopmol(newChunk)
        
        if group is not None:
            group.addchild(newChunk) #bruce 080318 addmember -> addchild
        else:
            self.addnode(newChunk)
            
        newChunk.setcolor(color, repaint_in_MT = False)
        
        return newChunk 

    def makeStrandChunkFromBrokenStrand(self, x1, x2): # by Mark
        """
        Makes a new strand chunk using the two singlets just created by
        busting the original strand, which is now broken. If the original
        strand was a ring, no new chunk is created.
        
        The new strand chunk, which includes the atoms between the 3' end of
        the original strand and the new 5' end (i.e. the break point), is 
        added to the same DNA group as the original strand and assigned a 
        different color.
        
        @param x1: The first of two singlets created by busting a strand
                   backbone bond. It is either the 3' or 5' open bond singlet,
                   but we don't know yet.
        @type  x1: L{Atom}
        
        @param x2: The second of two singlets created by busting a backbone
                   backbone bond. It is either the 3' or 5' open bond singlet,
                   but we don't know yet.
        @type  x2: L{Atom}
        
        @return: The new strand chunk. Returns B{None} if no new strand chunk
                 is created, as is the case of a ring.
        @rtype:  L{Chunk}
        """
        minimize = debug_pref("Adjust broken strand bondpoints using minimizer?",
                          #bruce 080415 revised text (to not use the developer-
                          # jargon-only term "singlet"), changed prefs_key,
                          # and removed non_debug = True, for .rc2 release,
                          # since the repositioning bug this worked around
                          # is now fixed.
                         Choice_boolean_False,
                         prefs_key = True,
                    )
        
        _five_prime_atom = None
        _three_prime_atom = None
        
        for singlet in (x1, x2):
            adjustSinglet(singlet, minimize = minimize)
            open_bond = singlet.bonds[0]
            if open_bond.isFivePrimeOpenBond():
                _five_prime_atom = open_bond.other(singlet)
            else:
                _three_prime_atom = open_bond.other(singlet)
        
        # Make sure we have exactly one 3' and one 5' singlet.
        # If not, there is probably a direction error on the open bond(s)
        # that x1 and/or x2 are members of.
        if not _five_prime_atom:
            print_compact_stack("No 5' bondpoint.")
            return None
        if not _three_prime_atom:
            print_compact_stack("No 3' bondpoint.")
            return None
            
        atomList = self.o.assy.getConnectedAtoms([_five_prime_atom])
        
        if _three_prime_atom in atomList:
            # The strand was a closed loop strand, so we're done.
            return None # Since no new chunk was created.
        
        # See self.ensure_toplevel_group() docstring for explanation.
        self.ensure_toplevel_group()
        _group_five_prime_was_in = _five_prime_atom.molecule.dad
        if env.prefs[assignColorToBrokenDnaStrands_prefs_key]:
            _new_strand_color = getNextStrandColor(_five_prime_atom.molecule.color)
        else:
            _new_strand_color = _five_prime_atom.molecule.color
        return self.makeChunkFromAtomList(atomList,
                                          group = _group_five_prime_was_in,
                                          name = gensym("Strand"),
                                              # doesn't need "DnaStrand" or self.assy,
                                              # since not normally seen by users
                                              # [bruce 080407 comment]
                                          color = _new_strand_color)

    pass # end of class ops_rechunk_Mixin

# end