summaryrefslogtreecommitdiff
path: root/cad/src/operations/bond_utils.py
blob: 79888cf1b8dc85ff9c880d9f39a737d29a153a00 (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
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
# Copyright 2005-2007 Nanorex, Inc.  See LICENSE file for details.
"""
bond_utils.py -- helper functions for bond-related UI code and its operations

(should be renamed, maybe to bond_menu_helpers.py)

@author: bruce
@version: $Id$
@copyright: 2005-2007 Nanorex, Inc.  See LICENSE file for details.

History:

created by bruce 050705 to help with higher-order bonds for Alpha6.
"""

from geometry.VQT import Q
from utilities.constants import noop
import foundation.env as env
from utilities.Log import greenmsg, redmsg, orangemsg, quote_html
from utilities.debug import print_compact_stack

from model.bond_constants import bonded_atoms_summary
from model.bond_constants import btype_from_v6
from model.bond_constants import v6_from_btype
from model.bond_constants import bond_left_atom

def intersect_sequences(s1, s2):
    """
    Return the intersection of two sequences. If they are sorted
    in a compatible way, so will be the result.
    """
    return filter( lambda s: s in s2, s1)

def complement_sequences(big, little):
    return filter( lambda s: s not in little, big)

def possible_bond_types(bond):
    """
    Return a list of names of possible bond types for the given bond,
    in order of increasing bond order,
    based on its atoms' current elements and atomtypes.
       This list is never empty since single bonds are always deemed possible
    (even if they always induce valence errors, e.g. for H bonded to O(sp2) or in O2).
       For warnings about some choices of bond type (e.g. S=S), see the function bond_type_warning.
       [If you want to permit bonds requiring other atomtypes, when those are reachable
    by altering only open bonds on this bond's actual atoms, see possible_bond_types_for_elements
    (related to that goal, but might not do exactly that).]
       Warning: this ignores geometric issues, so it permits double bonds even if they
    would be excessively twisted, and it ignores bond length, bond arrangement in space
    around each atom, etc.
    """
    s1 = bond.atom1.atomtype.permitted_v6_list # in order of v6
    s2 = bond.atom2.atomtype.permitted_v6_list
    s12 = intersect_sequences( s1, s2 ) # order comes from s1; we depend on its coming from one of them or the other
        #e could be faster (since these lists are prefixes of a standard order), but doesn't need to be
    return map( btype_from_v6, s12)

def possible_bond_types_for_elements(bond):
    "#doc, incl details of what's permitted"
    permitted1 = bond.atom1.permitted_btypes_for_bond(bond) # dict from v6 to atomtypes which permit it
    permitted2 = bond.atom2.permitted_btypes_for_bond(bond)
    poss_v6 = intersect_sequences(permitted1.keys(), permitted2.keys()) # arbitrary order
    poss_v6.sort() # smallest bond order first
    poss2 = map( btype_from_v6, poss_v6)
    return poss2, permitted1, permitted2

#partly-obs comment:
#e should we put element rules into the above possible_bond_types, or do them separately?
# and should bonds they disallow be shown disabled, or not even included in the list?
# and should "unknown" be explicitly in the list?

def bond_type_warning(bond, btype): # 050707
    """
    Return a warning (short text suitable to be added to menu item text), or "" for no warning,
    about the use of btype (bond type name) for bond.
    This can be based on its atomtypes or perhaps on more info about the surroundings
    (#e we might need to add arguments to pass such info).
       Presently, this only warns about S=S being unstable, and about bonds whose type could not
    permit both atoms (using their current atomtypes) to have the right valence
    regardless of their other bonds (which only happens now when they have no other bonds).
       This might return warnings for illegal btypes, even though it's not presently called
    for illegal btypes for the given bond. It doesn't need to return any warning for illegal btypes.
    """
    atype1 = bond.atom1.atomtype
    atype2 = bond.atom2.atomtype
    if btype == 'double' and atype1.is_S_sp2 and atype2.is_S_sp2:
        return "unstable"
    elif btype == 'single' and (atype1.bond1_is_bad or atype2.bond1_is_bad):
        return "bad valence"
    elif btype != 'triple' and (atype1.is_N_sp or atype2.is_N_sp):
        return "bad valence"
    # if there are any other numbonds=1 atoms which show up here, they should be valence-checked too (generalizing the above)
    # (which might be easiest if atomtype stores a "valence-permitted btypes" when numbonds is 1), but I don't think there are others
    return ""

_BOND_DIR_TEXTS = {0:"unset", 1:" --->", -1:"<--- "} # see also _BOND_DIR_NAMES #e refile

def bond_menu_section(bond, quat = Q(1,0,0,0)):
    """
    Return a menu_spec subsection for displaying info about a highlighted bond,
    changing its bond_type, offering commands about it, etc.
    If given, use the quat describing the rotation used for displaying it
    to order the atoms in the bond left-to-right (e.g. in text strings).
    """
    res = []
    res.append(( bonded_atoms_summary(bond, quat = quat), noop, 'disabled' ))
    res.extend( _bond_type_menu_section(bond) )
    if bond.is_directional():
        ### REVIEW: Do we want to do this for open bonds, after mark's 071014 change
        # which allows them here? Or do we want "and not bond.is_open_bond()"?
        # (BTW, I'm not sure this gets called at all, for open bonds.)
        # Guess: open bonds would be safe here, so allow them, though I'm
        # not sure it's always a good idea. Caveat: if we treat them as non-directional
        # when the base atom has three or more directional bonds, we should probably
        # make that exception here too -- probably using a higher-level method in place
        # of is_directional, namely directional_bond_chain_status for both atoms in bond.
        # We'd want a new method on bond to call that for both atoms and look at the
        # results (perhaps there's already code like that elsewhere). Without this,
        # we may get a bug if a user can try to change direction on an open bond
        # that hits a strand but is not in it. But since I suspect the UI never allows
        # an open bond here, I won't bother to write that code just yet. [bruce 071016]
        submenu_contents = bond_direction_submenu_contents(bond, quat)
        left_atom = bond_left_atom(bond, quat) # same one that comes first in bonded_atoms_summary
            #e ideally, for mostly vertical bonds, we'd switch to an up/down distinction for the menu text about directions
            #e and whatever the direction names, maybe we should explore farther along the strand to see what they are...
            # unless it hairpins or crosses over... hmm.
        current_dir = bond.bond_direction_from(left_atom)
        current_dir_str = _BOND_DIR_TEXTS[current_dir]
        text = "strand direction (%s)" % current_dir_str
        item = (text, submenu_contents)
        res.append(item)
    return res

def _bond_type_menu_section(bond): #bruce 050716; replaces bond_type_submenu_spec for Alpha6
    """
    Return a menu_spec for changing the bond_type of this bond
    (as one or more checkmark items, one per permitted bond-type given the atomtypes),
    or if the bond-type is unchangeable, a disabled menu item for displaying the type
    (which looks the same as when the bond type is changeable, except for being disabled).
    (If the current bond type is not permitted, it's still present and checked, but disabled,
     and it might have a warning saying it's illegal.)
    """
    # this assert is true, but it would cause an import loop:
    ## assert isinstance(bond, Bond)
    btype_now = btype_from_v6(bond.v6)
    poss1 = possible_bond_types(bond) # a list of strings which are bond-type names, in order of increasing bond order
    poss, permitted1, permitted2 = possible_bond_types_for_elements(bond) # new feature 060703
    ##e could put weird ones (graphitic, carbomeric) last and/or in parens, in subtext below
    types = list(poss)
    for btype in poss1:
        if btype not in types:
            print "should never happen: %r not in %r" % (btype, poss) # intentional: "not in types" above, "not in poss" here
            types.append(btype)
    if btype_now not in types:
        types.append(btype_now) # put this one last, since it's illegal; warning for it is computed later
    assert len(types) > 0
    # types is the list of bond types for which to make menu items, in order;
    # now make them, and figure out which ones are checked and/or disabled;
    # we disable even legal ones iff there is only one bond type in types
    # (which means, if current type is illegal, it is disabled and the sole legal type is enabled).
    disable_legal_types = (len(types) == 1)
    res = []
    for btype in types: # include current value even if it's illegal
        subtext = "%s bond" % btype # this string might be extended below
        checked = (btype == btype_now)
        command = ( lambda arg1=None, arg2=None, btype=btype, bond=bond: apply_btype_to_bond(btype, bond) )
        warning = warning2 = ""
        if btype not in poss:
            # illegal btype (note: it will be the current one, and thus be the only checked one)
            warning = "illegal"
            disabled = True
        else:
            # legal btype
            warning = bond_type_warning(bond, btype) # might be "" (or None??) for no warning
            if btype not in poss1:
                # new feature 060703
                # try1: too long and boring (when in most menu entries):
                ## warning2 = "would change atomtypes"
                # try2: say which atomtypes we'd change to, in same order of atoms as the bond name
                v6 = v6_from_btype(btype)
                atype1 = best_atype(bond.atom1, permitted1[v6])
                atype2 = best_atype(bond.atom2, permitted2[v6])
                in_order = [atype1, atype2] ##e stub; see code in Bond.__str__
                warning2 = "%s<->%s" % tuple([atype.name for atype in in_order])
            disabled = disable_legal_types
                # might change this if some neighbor bonds are locked (nim), or if we want to show non-possible choices
        if warning2:
            subtext += " (%s)" % warning2
        if warning:
            subtext += " (%s)" % warning
        res.append(( subtext, command,
                         disabled and 'disabled' or None,
                         checked and 'checked' or None ))
    ##e if >1 legal value, maybe we should add a toggleable checkmark item to permit "locking" the bond to its current bond type;
    # this won't be needed until we have better bond inference (except maybe for bondpoints),
    # since right now [still true 060703] we never alter real bond types except when the user does an action on that specific bond.
    if not bond.is_open_bond():
        ## command = ( lambda arg1 = None, arg2 = None, bond = bond: bond.bust() )
        command = ( lambda bond = bond: delete_bond(bond) )
        res.append(None) # separator
        res.append(("Delete Bond", command))
    return res

def delete_bond(bond): #bruce 080228 to fix update bug reported by EricM
    # see also: SelectAtoms_GraphicsMode.bondDelete
    # (should we print to history like it does?)
    assy = bond.atom1.molecule.assy
    if assy.glpane.selobj is bond:
        assy.glpane.selobj = None
    bond.bust()
    assy.changed()
    assy.glpane.gl_update()
    return

##def bond_type_submenu_spec(bond): #bruce 050705 (#e add options??); probably not used in Alpha6
##    """Return a menu_spec for changing the bond_type of this bond,
##    or if that is unchangeable, a disabled menu item for displaying the type.
##    """
##    v6 = bond.v6
##    btype0 = btype_from_v6(v6)
##    poss = possible_bond_types(bond) # a list of strings which are bond-type names
##    ##e could put weird ones (graphitic, carbomeric) last and/or in parens, in subtext below
##    maintext = 'Bond Type: %s' % btype0
##    if btype0 not in poss or len(poss) > 1:
##        # use the menu
##        submenu = []
##        for btype in poss: # don't include current value if it's illegal
##            subtext = btype
##            warning = bond_type_warning(bond, btype)
##            if warning:
##                subtext += " (%s)" % warning
##            command = ( lambda arg1=None, arg2=None, btype=btype, bond=bond: apply_btype_to_bond(btype, bond) )
##            checked = (btype == btype0)
##            disabled = False # might change this if some neighbor bonds are locked, or if we want to show non-possible choices
##            submenu.append(( subtext, command,
##                             disabled and 'disabled' or None,
##                             checked and 'checked' or None ))
##        ##e if >1 legal value could add checkmark item to permit "locking" this bond type
##        return ( maintext, submenu)
##    else:
##        # only one value is possible, and it's the current value -- just show it
##        return ( maintext, noop, 'disabled' )
##    pass


#bruce 060523 unfinished aspects of new more permissive bondtype changing: ####@@@@
# - verify it can't be applied to open bonds from dashboard tools (since not safe yet)
# - make sure changing atomtypes doesn't remove bond (if open)
#   (possible implem of that: maybe remove it, set_atomtype, then add it back, then remake singlets?)
# - then it's safe to let bond cmenu have more entries (since they might be open bonds)

def apply_btype_to_bond(btype,
                        bond,
                        allow_remake_bondpoints = True,
                        suppress_history_message = False): #bruce 060703 added allow_remake_bondpoints for bug 833-1
    """
    Apply the given bond-type name (e.g. 'single') to the given bond, iff this is permitted by its atomtypes
    (or, new feature 060523, if it's permitted by its real atoms' possible atomtypes and their number of real bonds),
    and do whatever inferences are presently allowed [none are implemented as of 050727].
    Emit an appropriate history message. Do appropriate invals/updates.
    [#e should the inference policy and/or some controlling object be another argument? Maybe even a new first arg 'self'?]

    @param suppress_history_message: If True, it quietly converts the bondtypes
            without printing any history message.
    """
    # Note: this can be called either from a bond's context menu, or by using a Build mode dashboard tool to click on bonds
    # (or bondpoints as of 060702) and immediately change their types.

    #This flag will be returned by this function to tell the caller whether the
    #bond type of the given bond was changed
    bond_type_changed = True

    v6 = v6_from_btype(btype)
    oldname = quote_html( str(bond) )
    def changeit(also_atypes = None):
        if v6 == bond.v6:
            bond_type_changed = False
            if not suppress_history_message:
                env.history.message( "bond type of %s is already %s" % (oldname, btype))
        else:
            if also_atypes:
                # change atomtypes first (not sure if doing this first matters)
                atype1, atype2 = also_atypes
                def changeatomtype(atom, atype):
                    if atom.atomtype is not atype:
                        if not suppress_history_message:
                            msg = "changed %r from %s to %s" % (atom,
                                                                atom.atomtype.name,
                                                                atype.name )
                            env.history.message(msg)
                        atom.set_atomtype(atype)
                        ### note[ probably 060523]:
                        # if we're an open bond, we have to prevent this process from removing us!
                        # (this is nim, so we're not yet safe to offer on open bonds.
                        #  Thus in fix for 833-1 [060703], atomtype changes are not allowed.)
                        pass
                    return # from changeatomtype
                changeatomtype(bond.atom1, atype1)
                changeatomtype(bond.atom2, atype2)
            bond.set_v6(v6) # this doesn't affect anything else or do any checks ####k #####@@@@@ check that
            ##e now do inferences on other bonds
            bond.changed() ###k needed?? maybe it's now done by set_v6??
            if not suppress_history_message:
                env.history.message( "changed bond type of %s to %s" % (oldname,
                                                                        btype))
            ###k not sure if it does gl_update when needed... how does menu use of this do that?? ###@@@
        return # from changeit
    poss = poss1 = possible_bond_types(bond) # only includes the ones which don't change the atomtypes -- try these first
    if btype in poss1:
        changeit()
        return bond_type_changed
    # otherwise figure out if we can change the atomtypes to make this work.
    # (The following code is predicted to work for either real or open bonds,
    #  but it is not safe to offer on open bonds for other reasons (commented above in changeatomtype).
    #  But we'll still figure out the situation, so the history message can be more useful.)
    if 1:
        # this is needed for allow_remake_bondpoints,
        # or for history advice about what that could have permitted:
        poss2, permitted1, permitted2 = possible_bond_types_for_elements(bond)
            # the only purpose of having the whole sequence poss2
            # (not just one element of it, equal to btype) is the error message
        if btype in poss2:
            atype1 = best_atype(bond.atom1, permitted1[v6])
            atype2 = best_atype(bond.atom2, permitted2[v6])
    if allow_remake_bondpoints:
        poss = poss2 # poss is whichever of poss1 or poss2 was actually allowed
        if btype in poss2:
            changeit((atype1, atype2))
            return bond_type_changed
    # It failed, but a variety of situations should be handled in the error message.
    # For error messages, sort them all the same way.
    poss1.sort()
    poss2.sort()
    poss.sort() #k not really needed, it's same mutable list, but keep this in case someone changes that
    if poss2 == poss : # note, this happens if poss2 == poss1, or if they differ but allow_remake_bondpoints is true
        # permitting changing of atomtypes wouldn't make any difference
        if not suppress_history_message:
            msg = "can't change bond type of %s to %s" % (oldname, btype)
            msg2 = " -- permitted types are %s" % (poss)
                #e improve message -- %s of list looks like repr (for strings too)
            env.history.message( orangemsg( msg) + msg2 )
        bond_type_changed = False
    elif btype in poss2:
        if allow_remake_bondpoints:
            print_compact_stack( "bug: allow_remake_bondpoints should not be true here: " )
        # the only reason we refused is that the UI won't allow remaking of bondpoints;
        # explain what the user would have to do to make it work (using the things computed above as if it had been permitted)
        # (as of 060703 this happens only when you click a bond type changing tool on a bondpoint,
        #  but following code will try to cover this for a real bond as well)
        unless = ""
        for atom, atype in [(bond.atom1, atype1), (bond.atom2, atype2)]: ##e ideally, in same order as printed in bond name
            if atype != atom.atomtype:
                if atom.is_singlet():
                    # should never happen
                    if env.debug:
                        print "debug: bug: %r is bondpoint but user is advised to change its atomtype" % atom
                if not unless:
                    unless = "change atomtype of %s to %s" % (atom, atype.name)
                else:
                    # this is not expected to ever happen, when called from UI as of 060703; it's untested ##@@
                    unless += ", and of %s to %s" % (atom, atype.name)
        msg = "can't change bond type of %s to %s, " % (oldname, btype,)
        bond_type_changed = False
        if unless:
            unless_msg = greenmsg( "unless you %s" % (unless,) )
        else:
            unless_msg = redmsg( "due to a bug")
        if not suppress_history_message:
            env.history.message( orangemsg( msg) + ( unless_msg) )


    else:
        # changing atomtypes makes a difference, but either way you're not allowed to change to this bond type
        if allow_remake_bondpoints:
            print_compact_stack( "bug: allow_remake_bondpoints should not be true here: " )
        extra = complement_sequences(poss2, poss1)
        if not extra:
            print_compact_stack( "bug: extra should not be empty here: " )


        msg = "can't change bond type of %s to %s" % (oldname, btype)
        msg2 = " -- permitted types are %s, or %s if you change atomtypes" % (poss1, extra)
            #e improve message -- %s of list looks like repr (for strings too)
        bond_type_changed = False
        if not suppress_history_message:
            env.history.message( orangemsg( msg) + msg2 )

    return bond_type_changed # from apply_btype_to_bond

def best_atype(atom, atomtypes = None): #bruce 060523
    """
    Which atomtype for atom is best, among the given or possible ones,
    where best means the fewest number of bondpoints need removing to get to it?
    (Break ties by favoring current one (never matters as presently called, 060523)
    or those earlier in the list.)
    """
    # I don't think we have to consider types for which bondpoints would be *added*...
    # but in case we do, let those be a last resort, but for them, best means fewest added.
    # Note: this is related to Atom.best_atomtype_for_numbonds, but that has a quite different cost function
    # since it assumes it's not allowed to change the number of bondpoints, only to compare severity of valence errors.
    atomtypes = atomtypes or atom.element.atomtypes
    atomhas = len(atom.bonds)
    def cost(atype):
        atypewants = atype.numbonds
        nremove = atomhas - atypewants
        if nremove >= 0:
            cost1 = nremove
        else:
            nadd = - nremove
            cost1 = 100 + nadd
        if atype is atom.atomtype:
            cost2 = -1
        else:
            cost2 = atomtypes.index(atype)
        return (cost1, cost2)
    costitems = [(cost(atype), atype) for atype in atomtypes]
    costitems.sort()
    return costitems[0][1]

_BOND_DIR_NAMES = {0:"unset", 1:"right", -1:"left"} # see also _BOND_DIR_TEXTS #e refile

def bond_direction_submenu_contents(bond, quat): #bruce 070415
    res = []
    left_atom = bond_left_atom(bond, quat)
    direction = bond.bond_direction_from(left_atom)
##    # order will be: if this bond has a dir: this dir, opp dir, unset;
##    # or if not: unset, right, left. So current dir is always first. Not sure this is good! In fact, I'm sure it's not!
##    if direction:
##        dir_order = [direction, - direction, 0]
##    else:
##        dir_order = [0, 1, -1]
    dir_order = [1, -1, 0]
    for dir in dir_order:
        text = "make it %s" % _BOND_DIR_NAMES[dir]
            # how do we say concisely:
            # "make the bond dirs all one way along entire strand,
            #  so they're going (eg) right, when they pass thru this bond"?
        if dir == direction:
            text += " (like this bond)" #e also use checkmark, or is that confusing since it's not a noop? for now, use it.
        command = (lambda _guard = None, bond = bond, left_atom = left_atom, dir = dir:
                   bond.set_bond_direction_from(left_atom, dir, propogate = True))
        checkmark = (dir == direction) and 'checked' or None
        item = (text, command, checkmark)
        res.append(item)
    res.append(('set to fit minor groove (not implemented)', noop, 'disabled'))
    return res

# end