summaryrefslogtreecommitdiff
path: root/cad/src/model_updater/bond_updater.py
blob: c71d65acbd3fe177751b2c8443769aa3f5b0e4d0 (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
# Copyright 2005-2007 Nanorex, Inc.  See LICENSE file for details.
"""
bond_updater.py

Recompute structural bond orders when necessary.

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

This is needed for bonds between atoms whose atomtypes make p orbitals
available for bonding, to check whether pi bonds are formed,
whether they're aromatic or double or triple, to check for
radicals (pi systems containing unpaired electrons),
and to notice graphite.

(Much of the above is not yet implemented.)

History:

bruce 050627 started this as part of supporting higher-order bonds.

bruce 071108 split out the general orchestration and registration
part of this into master_model_updater.py, leaving only the bond-
type- and atom-type- updating code in this file.
"""

from utilities.debug import print_compact_traceback

from model.bond_constants import V_SINGLE
from model.bond_constants import V_DOUBLE
from model.bond_constants import V_AROMATIC
from model.bond_constants import V_GRAPHITE
from model.bond_constants import V_TRIPLE
from model.bond_constants import V_CARBOMERIC

# ==

def update_bonds_after_each_event( changed_structure_atoms):
    """
    [should be called only from _master_model_updater, which is in turn
     called from env.do_post_event_updates]

    This should be called at the end of every user event which might affect
    the atomtypes or bond-sets of any atoms or singlets, which are passed as the
    values of the dict changed_structure_atoms, which we should not modify
    (and which no other code will modify while we use it). (As it happens,
    the caller will clear that dict after we return.)

    This function will either update, or record as needing update, the
    structural bond orders and associated data for all
    real atoms, real bonds, and open bonds which might be in pi systems
    (or which were, but aren't anymore).
    [And it might do more. #k if so, #doc that here.]

    Since it must be fast, it won't do work which leads it away from the actual
    atoms it's passed, and it will leave existing bond orders alone whenever
    they're locally consistent with the current atomtypes. That is, it will only
    act when there are local inconsistencies, and it will only fix them when
    this can be done on just the atoms it was passed (or their bonds), and in
    a reasonably unambiguous way; whenever it thinks a more global update is
    needed, it will record this fact (and the affected atoms) so the user can
    be advised that a global bond-update is needed.

    It will assume that interpart bonds (if any) have already been broken.
    (#e Or we might decide to extend it to break them itself.)
    """
    ###@@@ so far this is called by update_parts (eg mode chgs),
    # and near the start of a GLPane repaint event (should be enough),
    # and only when changed_structure_atoms is nonempty. #k
    # It misses hearing about killed atoms or singlets,
    # but does hear about atoms shown only in the elt selector thumbviews!
    # I guess the latter is good, since that way it can update the bond orders
    # in those views as well! However, debug tests showed that the elt selector
    # thumbviews need a separate redraw to show this... that's ok, they're only
    # supposed to draw single-bond atypes anyway.
    bonds_to_fix = {}
    mols_changed = {} #bruce 060126, so atom._changed_structure() doesn't need
        # to call atom.changed() directly

    for atm in changed_structure_atoms.values():
        #bruce 060405 precaution: itervalues -> values,
        # due to jig.changed_structure calls

        # ignore killed atoms
        # [bruce 071018 bugfix of old bug
        #  "reguess_atomtype of bondpoint with no bonds"]
        if atm._Atom__killed:
            # this inlines atm.killed() for speed, since this will happen a lot
            continue

##        if 'testing kluge':
##            #bruce 071117 -- see if this breaks any of our Python versions.
##            # update 071119: reported to work for Windows- python 2.4,
##            # OS X- python 2.3, Ubuntu- python2.5, so we can assume it
##            # works for all versions unless we find out otherwise.
##            # So this test code is now disabled unless further needed.
##            # I'm commenting it out entirely, so the import from chem can't
##            # mess up our import analysis. Once some real code relies on
##            # class-switching, we can remove this test code entirely.
##            # [bruce 071119]
##            from chem import Atom2, Atom
##            if atm.__class__ is Atom:
##                nc = Atom2
##            else:
##                nc = Atom
##            assert nc is not atm.__class__
##            atm.__class__ = nc
##            print "testing kluge, bruce 071117: set %s.__class__ to %s" % (atm, nc)
##            assert nc is atm.__class__

        # for singlets, just look at their base atoms
        # [I'm not sure where that comment shows up in the code, or whether the
        #  following comment was meant to be a change to it -- bruce 071115]

        # [as of 050707 just look at all bonds of all unkilled atoms]

        #e when info must be recorded for later, do this per-chunk or per-part.
        ##k Do we move existing such info when atoms moved or were killed??

        mol = atm.molecule # might be None or nullMol; check later
        mols_changed[id(mol)] = mol

        atype = atm.atomtype # make sure this is defined; also it will tell us
            # the permitted bond orders for any bond on this atom
        for bond in atm.bonds:
            v6 = bond.v6
            if v6 != V_SINGLE: # this test is just an optim
                if not atype.permits_v6(v6):
                    # this doesn't notice things like S=S (unstable), only the
                    # S= and =S parts (ok taken alone)
                    bonds_to_fix[id(bond)] = bond
                # SOMEDAY: also check legal endcombos for aromatic, graphite
                # (carbomeric?)
                # SOMEDAY: also directly check sp-chain-lengths here,
                # infer double bonds when odd length and connected, etc??

        # Tell perceived structures involving this atom that it changed.
        # (These will include sp-chains and pi-systems, and maybe more.
        #  For Alpha6, probably only sp-chains.)
        # For now, these are stored in jigs; one jig might contain just one
        # perceived structure, or all of one kind in one set of atoms
        # (the following code needn't know which).

        if atm.jigs: # most atoms have no jigs, so initial 'if' is worthwhile
            for jig in atm.jigs[:]: # list copy is necessary, see below
                try:
                    method = jig.changed_structure
                except AttributeError:
                    pass # initial kluge so I don't need to extend class Jig
                         #FIX
                else:
                    try:
                        method(atm)
                            # Note: this is permitted to destroy jig
                            # and (thereby) remove it from atm.jigs
                    except:
                        msg = "ignoring exception in jig.changed_atom(%r) " \
                              "for %r: " % (atm, jig)
                        print_compact_traceback( msg)
                continue

        # atom-valence checks can't be done until we fix illegal bond types,
        # below

        # comments about future atom-valence checks:
        #
        #e should we also check atypes against numbonds and total bond valence
        # (in case no bonds need fixing or they don't change)?
        # note: that's the only way we'll ever notice we need to increase any
        # bonds from single, on sp2 atoms!!!
        # Do we need to separately track needed reductions (from atypes)
        # or certainty-increases (A no longer allowed) or increases
        # (from atom valence)??
        # For certainty-increases, should we first figure out the rest before
        # seeing what to change them to? (guess: no, but not sure)
        #
        # (above cmts are obs, see paper notes about the alg)

    for mol in mols_changed.itervalues():
        if mol is not None:
            mol.changed() # should be safe for nullMol (but not for None)

    if not bonds_to_fix:
        return # optim [will be wrong once we have atom valence checks below]

    for bond in bonds_to_fix.itervalues():
        # every one of these bonds is wrong, in a direct local way
        # (ie due to its atoms)!
        # figure out what to change it to, and [someday] initiate our scan
        # of changes from each end of each bond.
        new_v6 = _best_corrected_v6(bond) ####@@@@ IMPLEM the rest of that...
            # actually this'll all be revised
        bond.set_v6(new_v6) #####@@@@@ ensure this calls _changed_structure
            # on both atoms -- WRONG, needs to call something different,
            # saying it changes bond orders but not bonds themselves
            # (so e.g. sp chains update geom but don't get destroyed).
            # [As of 050725 I think it doesn't do either of those.]
            # WARNING: this might add new atoms to our argument dict,
            # changed_structure_atoms (and furthermore, we might depend on
            # the fact that it does, for doing valence checks on them!)
            #060306 update: as of long before now, it stores these bonds
            # in changed_bond_types.

    return # from update_bonds_after_each_event

##most_permissible_v6_first = ( V_SINGLE, V_DOUBLE, V_AROMATIC, V_GRAPHITE,
##                              V_TRIPLE, V_CARBOMERIC )
##                            # not quite true for graphite?
##    # review this -- all uses should be considered suspicious [050714 comment]
##    # [this seems to be no longer used as of 071108...]

def _best_corrected_v6(bond):
    """
    This bond has an illegal v6 according to its bonded atomtypes
    (and I guess the atomtypes are what has just changed?? ###k --
     [update 060629:] NOT ALWAYS --
     it might be the bond order (and then the atomtypes),
     changed automatically by increase_valence_noupdate
     when the user bonds two bondpoints to increase order of
     existing bond, as in bug 1951).

    Say how we want to fix it (or perhaps fix the atomtypes?? #e
     [i doubt it, they might have just changed -- 060629]).
    """
    # Given that the set of permissible bond types (for each atomtype,
    # ignoring S=S prohibition and special graphite rules)
    # is some prefix of [single, double, aromatic/graphite, triple, carbomeric],
    # I think it's ok to always take the last permissible element of that list
    # (using relaxed rules for graphite)...
    # no, it depends on prior bond (presumably one the user likes, or at least
    # consented to),
    # [note 060629 -- in bug 1951 the prior bond is carbomeric, but the user
    #  never wanted it, they just wanted to increase aromatic
    #  by 1, which numerically gets to carbomeric, but in that bug's example
    #  it's not a legal type and they really want double.
    #  So I will change the weird ordering of _corrected_v6_list[V_CARBOMERIC]
    # to a decreasing one, to fix that bug.]
    # but clearly we move to the left in that list.
    # Like this: c -> a, 3 -> max in list? or 2?
    # or depends on other bonds/valences?
    # ... we might return a list of legal btypes in order of preference,
    # for inference code (see paper notes)

    v6 = bond.v6
    try:
        lis = _corrected_v6_list[v6]
    except KeyError:
        # this happens for illegal v6 values
        return V_SINGLE
    atype1 = bond.atom1.atomtype
        # fyi, see also possible_bond_types() for similar code
    atype2 = bond.atom2.atomtype
    for v6 in lis:
        if v6 == V_SINGLE or atype1.permits_v6(v6) and atype2.permits_v6(v6):
            return v6
    print "bug: no legal replacement for v6 = %r in %r" % (bond.v6, bond)
    return V_SINGLE

# map a now-illegal v6 to the list of replacements to try (legal ones only,
# of course; in the order of preference given by the list)
_corrected_v6_list = {
    V_DOUBLE: (V_SINGLE,),
    V_TRIPLE: (V_DOUBLE, V_SINGLE),
    V_AROMATIC: (V_SINGLE,),
    V_GRAPHITE: (V_AROMATIC, V_SINGLE),
        # note: this temporarily goes up, then down.
    ## V_CARBOMERIC: (V_AROMATIC, V_DOUBLE, V_SINGLE), # there was a reason
        # for this, but it caused bug 1951, so revising it [bruce 060629]
    V_CARBOMERIC: (V_DOUBLE, V_AROMATIC, V_SINGLE),
        # This monotonic decreasing order ought to fix bug 1951,
        # though ideally we might depend on the last bond type
        # specifically chosen by the user... but at least, fractional
        # bond orders never show up unless the user has one
        # somewhere on some other bond, so always including V_AROMATIC
        # in this list might be ok, even if the user never explicitly
        # chose V_CARBOMERIC. [bruce 060629]
 }

# ==

def process_changed_bond_types( changed_bond_types):
    """
    Tell whoever needs to know that some bond types changed.
    For now, that means only bond.pi_bond_obj objects on those very bonds.
    """
    for bond in changed_bond_types.values():
        #bruce 060405 precaution: itervalues -> values, due to calls of code
        # we don't control here
        obj = bond.pi_bond_obj
        if obj is not None:
            obj.changed_bond_type(bond)
    return

# end