def modifyHydrogenate(self): """ Add hydrogen atoms to bondpoints on selected chunks/atoms. """ cmd = greenmsg("Hydrogenate: ") fixmols = {} # helps count modified mols for statusbar if self.selmols: counta = countm = 0 for m in self.selmols: changed = m.Hydrogenate() if changed: counta += changed countm += 1 fixmols[id(m)] = m if counta: didwhat = "Added %d atom(s) to %d chunk(s)" \ % (counta, countm) if len(self.selmols) > countm: didwhat += \ " (%d selected chunk(s) had no bondpoints)" \ % (len(self.selmols) - countm) didwhat = fix_plurals(didwhat) else: didwhat = "Selected chunks contain no bondpoints" elif self.selatoms: count = 0 for a in self.selatoms.values(): ma = a.molecule for atm in a.neighbors(): matm = atm.molecule changed = atm.Hydrogenate() if changed: count += 1 fixmols[id(ma)] = ma fixmols[id(matm)] = matm if fixmols: didwhat = \ "Added %d atom(s) to %d chunk(s)" \ % (count, len(fixmols)) didwhat = fix_plurals(didwhat) # Technically, we *should* say ", affected" instead of "from" # since the count includes mols of neighbors of # atoms we removed, not always only mols of atoms we removed. # Since that's rare, we word this assuming it didn't happen. # [#e needs low-pri fix to be accurate in that rare case; # might as well deliver that as a warning, since that case is # also "dangerous" in some sense.] else: didwhat = "No bondpoints on selected atoms" else: didwhat = redmsg("Nothing selected") if fixmols: self.changed() self.w.win_update() env.history.message(cmd + didwhat) return
def unselectConnected(self, atomlist=None): """ Unselect any atom that can be reached from any currently selected atom through a sequence of bonds. If <atomlist> is supplied, use it instead of the currently selected atoms. """ cmd = greenmsg("Unselect Connected: ") if atomlist is None and not self.selatoms: msg = redmsg("No atoms selected") env.history.message(cmd + msg) return if atomlist is None: # test for None since atomlist can be an empty list. atomlist = self.selatoms.values() catoms = self.getConnectedAtoms(atomlist) if not len(catoms): return natoms = 0 for atom in catoms[:]: if atom.picked: atom.unpick() if not atom.picked: # Just in case a selection filter was applied to this atom. natoms += 1 from platform_dependent.PlatformDependent import fix_plurals info = fix_plurals( "%d atom(s) unselected." % natoms) env.history.message( cmd + info) self.o.gl_update()
def selectDoubly(self): """ Select any atom that can be reached from any currently selected atom through two or more non-overlapping sequences of bonds. Also select atoms that are connected to this group by one bond and have no other bonds. """ ###@@@ same comment about interspace bonds as in selectConnected cmd = greenmsg("Select Doubly: ") if not self.selatoms: msg = redmsg("No atoms selected") env.history.message(cmd + msg) return alreadySelected = len(self.selatoms.values()) from operations.op_select_doubly import select_doubly # new code, bruce 050520 #e could also reload it now to speed devel! select_doubly(self.selatoms.values()) #e optim totalSelected = len(self.selatoms.values()) from platform_dependent.PlatformDependent import fix_plurals info = fix_plurals("%d new atom(s) selected (besides the %d initially selected)." % \ (totalSelected - alreadySelected, alreadySelected) ) env.history.message( cmd + info) if totalSelected > alreadySelected: ## otherwise, means nothing new selected. Am I right? ---Huaicai, not analyze the markdouble() algorithm yet #self.w.win_update() self.o.gl_update() return
def _getToolTipInfo(self): #ninad060825 "Return a string for display in Dynamic Tool tip " from platform_dependent.PlatformDependent import fix_plurals attachedAtomCount = fix_plurals("Attached to %d atom(s)"%(len(self.atoms))) return str(self.name) + "<br>" + "<font color=\"#0000FF\"> Jig Type:</font>Linear Motor"\ + "<br>" + "<font color=\"#0000FF\">Force: </font>" + str(self.force) + " pN " \ + "<br>" + "<font color=\"#0000FF\">Stiffness:</font> " + str(self.stiffness) + " N/m" \ + "<br>" + str(attachedAtomCount)
def _getToolTipInfo(self): #ninad060825 "Return a string for display in Dynamic Tool tip " from platform_dependent.PlatformDependent import fix_plurals attachedAtomCount = fix_plurals("Attached to %d atom(s)"%(len(self.atoms))) return str(self.name) + "<br>" + "<font color=\"#0000FF\"> Jig Type:</font>Rotary Motor"\ + "<br>" + "<font color=\"#0000FF\">Torque: </font>" + str(self.torque) + " nN-nm " \ + "<br>" + "<font color=\"#0000FF\">Speed:</font> " + str(self.speed) + " GHz" \ + "<br>" + str(attachedAtomCount)
def cm_select_jigs_atoms(self): #bruce 050504 nodeset = self.topmost_selected_nodes() otherpart = {} #bruce 050505 to fix bug 589 did_these = {} nprior = len(self.assy.selatoms) for jig in nodeset: assert isinstance(jig, Jig) # caller guarantees they are all jigs # If we didn't want to desel the jig, I'd have to say: # note: this does not deselect the jig (good); and permit_pick_atoms would deselect it (bad); # so to keep things straight (not sure this is actually needed except to avoid a debug message), # just set SELWHAT_ATOMS here; this is legal because no chunks are selected. Actually, bugs might occur # in which that's not true... I forget whether I fixed those recently or only analyzed them (due to delays # in update event posting vs processing)... but even if they can occur, it's not high-priority to fix them, # esp since selection rules might get revised soon. ## self.assy.set_selwhat(SELWHAT_ATOMS) # but (I forgot when I wrote that) we *do* desel the jig, # so instead I can just say: self.assy.part.permit_pick_atoms( ) # changes selwhat and deselects all chunks, jigs, and groups # [bruce 050519 8pm] for atm in jig.atoms: if atm.molecule.part == jig.part: atm.pick() did_these[atm.key] = atm else: otherpart[atm.key] = atm ## jig.unpick() # not done by picking atoms [no longer needed since done by permit_pick_atoms] msg = fix_plurals("Selected %d atom(s)" % len(did_these)) # might be 0, that's ok if nprior: #bruce 050519 #e msg should distinguish between atoms already selected and also selected again just now, # vs already and not now; for now, instead, we just try to be ambiguous about that msg += fix_plurals(" (%d atom(s) remain selected from before)" % nprior) if otherpart: msg += fix_plurals( " (skipped %d atom(s) which were not in this Part)" % len(otherpart)) msg = orangemsg(msg) # the whole thing, I guess env.history.message(msg) self.win.win_update() # note: caller (which puts up context menu) does # self.win.update_select_mode(); we depend on that. [### still true??] return
def add_basepair_handles_to_selected_atoms(glpane): #bruce 080515 assy = glpane.assy goodcount, badcount = add_basepair_handles_to_atoms(assy.selatoms.values()) msg = "adding handles to %d duplex Gv5 atom(s)" % (goodcount,) if badcount: msg += " (but not %d other selected atom(s))" % (badcount,) msg = fix_plurals(msg) env.history.message(greenmsg( "Add basepair handles:") + " " + msg) assy.w.win_update() return
def add_basepair_handles_to_selected_atoms(glpane): #bruce 080515 assy = glpane.assy goodcount, badcount = add_basepair_handles_to_atoms(assy.selatoms.values()) msg = "adding handles to %d duplex Gv5 atom(s)" % (goodcount, ) if badcount: msg += " (but not %d other selected atom(s))" % (badcount, ) msg = fix_plurals(msg) env.history.message(greenmsg("Add basepair handles:") + " " + msg) assy.w.win_update() return
def mark_selected_atoms_command(glpane): # untested """ current part only... """ assy = glpane.win.assy atoms = assy.selatoms.values() mark_atoms(atoms) msg = "marked %d selected atom(s)" % len(atoms) #e could use part of this string in jig name too msg = fix_plurals(msg) env.history.message(quote_html(msg)) return
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 _emit_one_summary_message(self, format, count): msg = format.replace("[N]", "%s" % count) # note: not an error if format doesn't contain "[N]" # assume msg contains '(s)' -- could check this instead to avoid debug print msg = fix_plurals(msg, between=3) # kluge: if a large between option is generally safe, # it ought to be made the default in fix_plurals # # todo: review for safety in case msg contains HTML (e.g. colors) # todo: ideally format would be a class instance which knew warning status, etc, # rather than containing html self.message(msg) return
def cm_select_jigs_atoms(self): #bruce 050504 nodeset = self.topmost_selected_nodes() otherpart = {} #bruce 050505 to fix bug 589 did_these = {} nprior = len(self.assy.selatoms) for jig in nodeset: assert isinstance( jig, Jig) # caller guarantees they are all jigs # If we didn't want to desel the jig, I'd have to say: # note: this does not deselect the jig (good); and permit_pick_atoms would deselect it (bad); # so to keep things straight (not sure this is actually needed except to avoid a debug message), # just set SELWHAT_ATOMS here; this is legal because no chunks are selected. Actually, bugs might occur # in which that's not true... I forget whether I fixed those recently or only analyzed them (due to delays # in update event posting vs processing)... but even if they can occur, it's not high-priority to fix them, # esp since selection rules might get revised soon. ## self.assy.set_selwhat(SELWHAT_ATOMS) # but (I forgot when I wrote that) we *do* desel the jig, # so instead I can just say: self.assy.part.permit_pick_atoms() # changes selwhat and deselects all chunks, jigs, and groups # [bruce 050519 8pm] for atm in jig.atoms: if atm.molecule.part == jig.part: atm.pick() did_these[atm.key] = atm else: otherpart[atm.key] = atm ## jig.unpick() # not done by picking atoms [no longer needed since done by permit_pick_atoms] msg = fix_plurals("Selected %d atom(s)" % len(did_these)) # might be 0, that's ok if nprior: #bruce 050519 #e msg should distinguish between atoms already selected and also selected again just now, # vs already and not now; for now, instead, we just try to be ambiguous about that msg += fix_plurals(" (%d atom(s) remain selected from before)" % nprior) if otherpart: msg += fix_plurals(" (skipped %d atom(s) which were not in this Part)" % len(otherpart)) msg = orangemsg(msg) # the whole thing, I guess env.history.message(msg) self.win.win_update() # note: caller (which puts up context menu) does # self.win.update_select_mode(); we depend on that. [### still true??] return
def _emit_one_summary_message(self, format, count): msg = format.replace("[N]", "%s" % count) # note: not an error if format doesn't contain "[N]" # assume msg contains '(s)' -- could check this instead to avoid debug print msg = fix_plurals(msg, between = 3) # kluge: if a large between option is generally safe, # it ought to be made the default in fix_plurals # # todo: review for safety in case msg contains HTML (e.g. colors) # todo: ideally format would be a class instance which knew warning status, etc, # rather than containing html self.message( msg) return
def getAngleHighlightedAtomAndSelAtoms(self, ppa2, ppa3, selectedAtomList, bendAngPrecision): """ Returns the angle between the last two selected atoms and the current highlighted atom. If the highlighed atom is also one of the selected atoms and there are only 2 selected atoms other than the highlighted one then it returns None.(then the function calling this routine needs to handle that case.) """ glpane = self.glpane lastSelAtom = None secondLastSelAtom = None ppa3Exists = self.lastTwoPickedInSelAtomList( ppa2, ppa3, selectedAtomList) #checks if *both* ppa2 and ppa3 exist if len(selectedAtomList) == 3 and glpane.selobj in selectedAtomList: if ppa3Exists and not (glpane.selobj is ppa2 or glpane.selobj is ppa3): lastSelAtom = ppa2 secondLastSelAtom = ppa3 else: #ninad060825 revised the following. Earlier code in v1.8 was correct but this one is simpler. Suggested by Bruce. I have tested it and is safe. tempAtomList = list(selectedAtomList) tempAtomList.remove(glpane.selobj) lastSelAtom = tempAtomList[0] secondLastSelAtom = tempAtomList[1] if len( selectedAtomList ) == 2: #here I (ninad) don't care about whether itselected atom is also highlighted. It is handled below. if ppa3Exists: lastSelAtom = ppa2 secondLastSelAtom = ppa3 else: lastSelAtom = selectedAtomList[0] secondLastSelAtom = selectedAtomList[1] #ninad060821 No need to display angle info if highlighed object and lastpicked or secondlast picked # object are identical if glpane.selobj in selectedAtomList: return False if lastSelAtom and secondLastSelAtom: angle = atom_angle_radians(glpane.selobj, lastSelAtom, secondLastSelAtom) * 180 / math.pi roundedAngle = str(round(angle, bendAngPrecision)) angleStr = fix_plurals( "<font color=\"#0000FF\">Angle %s-%s-%s:</font> %s degree(s)" % (glpane.selobj, lastSelAtom, secondLastSelAtom, roundedAngle)) return angleStr else: return False
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 deleteConnected(self, atomlist=None): # by mark """ Delete any atom that can be reached from any currently selected atom through a sequence of bonds, and that is acceptable to the current selection filter. If <atomlist> is supplied, use it instead of the currently selected atoms. """ cmd = greenmsg("Delete Connected: ") if atomlist is None and not self.selatoms: msg = redmsg("No atoms selected") env.history.message(cmd + msg) return if atomlist is None: # test for None since atomlist can be an empty list. atomlist = self.selatoms.values() catoms = self.getConnectedAtoms(atomlist) if not len(catoms): return natoms = 0 for atom in catoms[:]: if atom.killed(): continue #bruce 060331 precaution, to avoid counting bondpoints twice # (once when atom is them, once when they die when we kill their base atom) # if they can be in the passed-in list or the getConnectedAtoms retval # (I don't know if they can be) if atom.is_singlet(): continue #bruce 060331 precaution, related to above but different (could conceivably have valence errors w/o it) if atom.filtered(): continue #bruce 060331 fix a bug (don't know if reported) by doing 'continue' rather than 'return'. # Note, the motivation for 'return' might have been (I speculate) to not traverse bonds through filtered atoms # (so as to only delete a connected set of atoms), but the old code's 'return' was not a correct # implementation of that, in general; it might even have deleted a nondeterministic set of atoms, # depending on python dict item order and/or their order of deposition or their order in the mmp file. natoms += 1 atom.kill() from platform_dependent.PlatformDependent import fix_plurals info = fix_plurals("%d connected atom(s) deleted." % natoms) #bruce 060331 comment: this message is sometimes wrong, since caller has deleted some atoms on click 1 of # a double click, and then calls us on click 2 to delete the atoms connected to the neighbors of those. # To fix this, the caller ought to pass us the number of atoms it deleted, for us to add to our number, # or (better) we ought to return the number we delete so the caller can print the history message itself. env.history.message(cmd + info) ## self.o.gl_update() self.w.win_update( ) #bruce 060331 possible bugfix (bug is unconfirmed) -- update MT too, in case some chunk is gone now return
def _make_bonds_4(self): msg = fix_plurals( "%d bond(s) made" % self.total_bonds_made) env.history.message(msg) # Update the slider tolerance label. This fixed bug 502-14. Mark 050407 self.reset_tolerance_label() if self.bondable_pairs_atoms: # This must be done before gl_update, or it will try to draw the # bondable singlets again, which generates errors. self.bondable_pairs = [] self.ways_of_bonding = {} self.w.win_update()
def _make_bonds_4(self): msg = fix_plurals("%d bond(s) made" % self.total_bonds_made) env.history.message(msg) # Update the slider tolerance label. This fixed bug 502-14. Mark 050407 self.reset_tolerance_label() if self.bondable_pairs_atoms: # This must be done before gl_update, or it will try to draw the # bondable singlets again, which generates errors. self.bondable_pairs = [] self.ways_of_bonding = {} self.w.win_update()
def deleteConnected(self, atomlist=None): # by mark """ Delete any atom that can be reached from any currently selected atom through a sequence of bonds, and that is acceptable to the current selection filter. If <atomlist> is supplied, use it instead of the currently selected atoms. """ cmd = greenmsg("Delete Connected: ") if atomlist is None and not self.selatoms: msg = redmsg("No atoms selected") env.history.message(cmd + msg) return if atomlist is None: # test for None since atomlist can be an empty list. atomlist = self.selatoms.values() catoms = self.getConnectedAtoms(atomlist) if not len(catoms): return natoms = 0 for atom in catoms[:]: if atom.killed(): continue #bruce 060331 precaution, to avoid counting bondpoints twice # (once when atom is them, once when they die when we kill their base atom) # if they can be in the passed-in list or the getConnectedAtoms retval # (I don't know if they can be) if atom.is_singlet(): continue #bruce 060331 precaution, related to above but different (could conceivably have valence errors w/o it) if atom.filtered(): continue #bruce 060331 fix a bug (don't know if reported) by doing 'continue' rather than 'return'. # Note, the motivation for 'return' might have been (I speculate) to not traverse bonds through filtered atoms # (so as to only delete a connected set of atoms), but the old code's 'return' was not a correct # implementation of that, in general; it might even have deleted a nondeterministic set of atoms, # depending on python dict item order and/or their order of deposition or their order in the mmp file. natoms += 1 atom.kill() from platform_dependent.PlatformDependent import fix_plurals info = fix_plurals( "%d connected atom(s) deleted." % natoms) #bruce 060331 comment: this message is sometimes wrong, since caller has deleted some atoms on click 1 of # a double click, and then calls us on click 2 to delete the atoms connected to the neighbors of those. # To fix this, the caller ought to pass us the number of atoms it deleted, for us to add to our number, # or (better) we ought to return the number we delete so the caller can print the history message itself. env.history.message( cmd + info) ## self.o.gl_update() self.w.win_update() #bruce 060331 possible bugfix (bug is unconfirmed) -- update MT too, in case some chunk is gone now return
def fuse_atoms(self): """ Deletes overlapping atoms found with the selected chunk(s). Only the overlapping atoms from the unselected chunk(s) are deleted. If the "Merge Chunks" checkbox is checked, then find_bondable_pairs() and make_bonds() is called, resulting in the merging of chunks. """ total_atoms_fused = 0 # The total number of atoms fused. # fused_chunks stores the list of chunks that contain overlapping atoms # (but no selected chunks, though) fused_chunks = [] # Delete overlapping atoms. for a1, a2 in self.overlapping_atoms: if a2.molecule not in fused_chunks: fused_chunks.append(a2.molecule) a2.kill() # print "Fused chunks list:", fused_chunks # Merge the chunks if the "merge chunks" checkbox is checked if self.propMgr.mergeChunksCheckBox.isChecked( ) and self.overlapping_atoms: # This will bond and merge the selected chunks only with # chunks that had overlapping atoms. #& This has bugs when the bonds don't line up nicely between # overlapping atoms in the selected chunk #& and the bondpoints of the deleted atoms' neighbors. # Needs a bug report. mark 060406. self.find_bondable_pairs(fused_chunks) self.make_bonds() # Print history msgs to inform the user what happened. total_atoms_fused = len(self.overlapping_atoms) msg = fix_plurals("%d atom(s) fused with %d chunk(s)" % (total_atoms_fused, len(fused_chunks))) env.history.message(msg) #"%s => %s overlapping atoms" % (tol_str, natoms_str) # Update the slider tolerance label. self.reset_tolerance_label() self.overlapping_atoms = [] # This must be done before win_update(), or it will try to draw the # overlapping atoms again, which generates errors. self.w.win_update()
def cm_remove_empty_groups(self): #bruce 080207 self.deselect_partly_picked_whole_nodes() nodeset = self.topmost_selected_nodes() empties = [] def func(group): if not group.members and group.permits_ungrouping(): empties.append(group) for node in nodeset: node.apply_to_groups(func) for group in empties: group.kill() msg = fix_plurals("removed %d empty Group(s)" % len(empties)) env.history.message( msg) self.mt_update() return
def cm_ungroup(self): self.deselect_partly_picked_whole_nodes() nodeset = self.topmost_selected_nodes() assert len(nodeset) == 1 # caller guarantees this node = nodeset[0] assert node.permits_ungrouping() # ditto need_update_parts = [] pickme = None if node.is_top_of_selection_group(): # this case is harder, since dissolving this node causes its members to become # new selection groups. Whether there's one or more members, Part structure needs fixing; # if more than one, interpart bonds need breaking (or in future might keep some subsets of # members together; more likely we'd have a different command for that). # simplest fix -- just make sure to update the part structure when you're done. # [bruce 050316] need_update_parts.append(node.assy) #bruce 050419 comment: if exactly one child, might as well retain the same Part... does this matter? # Want to retain its name (if group name was automade)? think about this a bit before doing it... # maybe fixing bugs for >1 child case will also cover this case. ###e #bruce 050420 addendum: I did some things in Part.__init__ which might handle all this well enough. We'll see. ###@@@ #k #bruce 050528 addendum: it's not handled well enough, so try this: hmm, it's not enough! try adding pickme too... ###@@@ if len(node.members) == 1 and node.part.topnode is node: node.part.topnode = pickme = node.members[0] if node.is_top_of_selection_group() and len(node.members) > 1: msg = "splitting %r into %d new clipboard items" % ( node.name, len(node.members)) else: msg = fix_plurals("ungrouping %d item(s) from " % len(node.members)) + "%s" % node.name env.history.message(msg) node.ungroup() # this also unpicks the nodes... is that good? Not really, it'd be nice to see who they were, # and to be consistent with Group command, and to avoid a glpane redraw. # But it's some work to make it pick them now, so for now I'll leave it like that. # BTW, if this group is a clipboard item and has >1 member, we couldn't pick all the members anyway! #bruce 050528 addendum: we can do it in this case, temporarily, just to get selgroup changed: if pickme is not None: pickme.pick( ) # just to change selgroup (too lazy to look up the official way to only do that) pickme.unpick( ) # then make it look the same as for all other "ungroup" ops #e history.message? for assy in need_update_parts: assy.update_parts() # this should break new inter-part bonds self.win.glpane.gl_update( ) #k needed? (e.g. for selection change? not sure. Needed if inter-part bonds break!) self.mt_update() return
def select_atoms_with_errors_command(glpane): """ current part only... """ count = 0 assy = glpane.win.assy for mol in assy.molecules: # current part only for atom in mol.atoms.itervalues(): if atom._dna_updater__error: count += 1 # whether or not already selected atom.pick() # should be safe inside itervalues ### REVIEW: selection filter effect not considered msg = "found %d pseudoatom(s) with dna updater errors in %r" % (count, assy.part) msg = fix_plurals(msg) env.history.message(quote_html(msg)) return
def fuse_atoms(self): """ Deletes overlapping atoms found with the selected chunk(s). Only the overlapping atoms from the unselected chunk(s) are deleted. If the "Merge Chunks" checkbox is checked, then find_bondable_pairs() and make_bonds() is called, resulting in the merging of chunks. """ total_atoms_fused = 0 # The total number of atoms fused. # fused_chunks stores the list of chunks that contain overlapping atoms # (but no selected chunks, though) fused_chunks = [] # Delete overlapping atoms. for a1, a2 in self.overlapping_atoms: if a2.molecule not in fused_chunks: fused_chunks.append(a2.molecule) a2.kill() # print "Fused chunks list:", fused_chunks # Merge the chunks if the "merge chunks" checkbox is checked if self.propMgr.mergeChunksCheckBox.isChecked() and self.overlapping_atoms: # This will bond and merge the selected chunks only with # chunks that had overlapping atoms. #& This has bugs when the bonds don't line up nicely between # overlapping atoms in the selected chunk #& and the bondpoints of the deleted atoms' neighbors. # Needs a bug report. mark 060406. self.find_bondable_pairs(fused_chunks) self.make_bonds() # Print history msgs to inform the user what happened. total_atoms_fused = len(self.overlapping_atoms) msg = fix_plurals( "%d atom(s) fused with %d chunk(s)" % (total_atoms_fused, len(fused_chunks))) env.history.message(msg) #"%s => %s overlapping atoms" % (tol_str, natoms_str) # Update the slider tolerance label. self.reset_tolerance_label() self.overlapping_atoms = [] # This must be done before win_update(), or it will try to draw the # overlapping atoms again, which generates errors. self.w.win_update()
def cm_remove_empty_groups(self): #bruce 080207 self.deselect_partly_picked_whole_nodes() nodeset = self.topmost_selected_nodes() empties = [] def func(group): if not group.members and group.permits_ungrouping(): empties.append(group) for node in nodeset: node.apply_to_groups(func) for group in empties: group.kill() msg = fix_plurals("removed %d empty Group(s)" % len(empties)) env.history.message(msg) self.mt_update() return
def getAngleHighlightedAtomAndSelAtoms(self, ppa2, ppa3, selectedAtomList, bendAngPrecision): """ Returns the angle between the last two selected atoms and the current highlighted atom. If the highlighed atom is also one of the selected atoms and there are only 2 selected atoms other than the highlighted one then it returns None.(then the function calling this routine needs to handle that case.) """ glpane = self.glpane lastSelAtom = None secondLastSelAtom = None ppa3Exists = self.lastTwoPickedInSelAtomList(ppa2, ppa3, selectedAtomList) #checks if *both* ppa2 and ppa3 exist if len(selectedAtomList) ==3 and glpane.selobj in selectedAtomList: if ppa3Exists and not (glpane.selobj is ppa2 or glpane.selobj is ppa3): lastSelAtom = ppa2 secondLastSelAtom = ppa3 else: #ninad060825 revised the following. Earlier code in v1.8 was correct but this one is simpler. Suggested by Bruce. I have tested it and is safe. tempAtomList =list(selectedAtomList) tempAtomList.remove(glpane.selobj) lastSelAtom = tempAtomList[0] secondLastSelAtom = tempAtomList[1] if len(selectedAtomList) == 2: #here I (ninad) don't care about whether itselected atom is also highlighted. It is handled below. if ppa3Exists: lastSelAtom = ppa2 secondLastSelAtom = ppa3 else: lastSelAtom = selectedAtomList[0] secondLastSelAtom = selectedAtomList[1] #ninad060821 No need to display angle info if highlighed object and lastpicked or secondlast picked # object are identical if glpane.selobj in selectedAtomList: return False if lastSelAtom and secondLastSelAtom: angle = atom_angle_radians( glpane.selobj, lastSelAtom,secondLastSelAtom ) * 180 / math.pi roundedAngle = str(round(angle, bendAngPrecision)) angleStr = fix_plurals("<font color=\"#0000FF\">Angle %s-%s-%s:</font> %s degree(s)" %(glpane.selobj, lastSelAtom, secondLastSelAtom, roundedAngle)) return angleStr else: return False
def cm_ungroup(self): self.deselect_partly_picked_whole_nodes() nodeset = self.topmost_selected_nodes() assert len(nodeset) == 1 # caller guarantees this node = nodeset[0] assert node.permits_ungrouping() # ditto need_update_parts = [] pickme = None if node.is_top_of_selection_group(): # this case is harder, since dissolving this node causes its members to become # new selection groups. Whether there's one or more members, Part structure needs fixing; # if more than one, interpart bonds need breaking (or in future might keep some subsets of # members together; more likely we'd have a different command for that). # simplest fix -- just make sure to update the part structure when you're done. # [bruce 050316] need_update_parts.append( node.assy) #bruce 050419 comment: if exactly one child, might as well retain the same Part... does this matter? # Want to retain its name (if group name was automade)? think about this a bit before doing it... # maybe fixing bugs for >1 child case will also cover this case. ###e #bruce 050420 addendum: I did some things in Part.__init__ which might handle all this well enough. We'll see. ###@@@ #k #bruce 050528 addendum: it's not handled well enough, so try this: hmm, it's not enough! try adding pickme too... ###@@@ if len(node.members) == 1 and node.part.topnode is node: node.part.topnode = pickme = node.members[0] if node.is_top_of_selection_group() and len(node.members) > 1: msg = "splitting %r into %d new clipboard items" % (node.name, len(node.members)) else: msg = fix_plurals("ungrouping %d item(s) from " % len(node.members)) + "%s" % node.name env.history.message( msg) node.ungroup() # this also unpicks the nodes... is that good? Not really, it'd be nice to see who they were, # and to be consistent with Group command, and to avoid a glpane redraw. # But it's some work to make it pick them now, so for now I'll leave it like that. # BTW, if this group is a clipboard item and has >1 member, we couldn't pick all the members anyway! #bruce 050528 addendum: we can do it in this case, temporarily, just to get selgroup changed: if pickme is not None: pickme.pick() # just to change selgroup (too lazy to look up the official way to only do that) pickme.unpick() # then make it look the same as for all other "ungroup" ops #e history.message? for assy in need_update_parts: assy.update_parts() # this should break new inter-part bonds self.win.glpane.gl_update() #k needed? (e.g. for selection change? not sure. Needed if inter-part bonds break!) self.mt_update() return
def Invert(self): """ Invert the atoms of the selected chunk(s) around the chunk centers """ mc = env.begin_op("Invert") cmd = greenmsg("Invert: ") if not self.selmols: msg = redmsg("No selected chunks to invert") env.history.message(cmd + msg) return self.changed() for m in self.selmols: m.stretch(-1.0) self.o.gl_update() info = fix_plurals( "Inverted %d chunk(s)" % len(self.selmols)) env.history.message( cmd + info) env.end_op(mc) #e try/finally?
def align_NEW(self): """ Align the axes of the selected movables to the axis of the movable that is placed at the highest order in the Model Tree """ #@@This is not called yet. #method *always* uses the MT order to align chunks or jigs #This supports jigs (including reference planes) but it has following #bug -- It always uses the selected movable that is placed highest #in the Model Tree, as the reference axis for alignment. (Instead #it should align to the 'first selected movable' #(this doesn't happen (or very rarely happens) in old align method where #'selmols' is used.) cmd = greenmsg("Align to Common Axis: ") movables = self.assy.getSelectedMovables() for m in movables: print "movable =", m.name numMovables = len(movables) if len(movables) < 2: msg = redmsg("Need two or more selected chunks to align") env.history.message(cmd + msg) return self.changed() try: firstAxis = movables[0].getaxis() for m in movables[1:]: m.rot(Q(m.getaxis(), firstAxis)) self.o.gl_update() except: print_compact_traceback( "bug: selected movable object doesn't have an \ axis") msg = redmsg("bug: selected movable object doesn't have an axis") env.history.message(cmd + msg) return self.o.gl_update() info = fix_plurals( "Aligned %d item(s)" % (len(movables) - 1) ) \ + " to axis of %s" % movables[0].name env.history.message(cmd + info) return
def align_NEW(self): """ Align the axes of the selected movables to the axis of the movable that is placed at the highest order in the Model Tree """ #@@This is not called yet. #method *always* uses the MT order to align chunks or jigs #This supports jigs (including reference planes) but it has following #bug -- It always uses the selected movable that is placed highest #in the Model Tree, as the reference axis for alignment. (Instead #it should align to the 'first selected movable' #(this doesn't happen (or very rarely happens) in old align method where #'selmols' is used.) cmd = greenmsg("Align to Common Axis: ") movables = self.assy.getSelectedMovables() for m in movables: print "movable =", m.name numMovables = len(movables) if len(movables) < 2: msg = redmsg("Need two or more selected chunks to align") env.history.message(cmd + msg) return self.changed() try: firstAxis = movables[0].getaxis() for m in movables[1:]: m.rot(Q(m.getaxis(),firstAxis)) self.o.gl_update() except: print_compact_traceback ("bug: selected movable object doesn't have an \ axis") msg = redmsg("bug: selected movable object doesn't have an axis") env.history.message(cmd + msg) return self.o.gl_update() info = fix_plurals( "Aligned %d item(s)" % (len(movables) - 1) ) \ + " to axis of %s" % movables[0].name env.history.message( cmd + info) return
def modifyDeleteBonds(self): """ Delete all bonds between selected and unselected atoms or chunks """ cmd = greenmsg("Delete Bonds: ") if not self.selatoms and not self.selmols: # optimization, and different status msg msg = redmsg("Nothing selected") env.history.message(cmd + msg) return cutbonds = 0 # Delete bonds between selected atoms and their neighboring atoms that are not selected. for a in self.selatoms.values(): for b in a.bonds[:]: neighbor = b.other(a) if neighbor.element != Singlet: if not neighbor.picked: b.bust() a.pick() # Probably not needed, but just in case... cutbonds += 1 # Delete bonds between selected chunks and chunks that are not selected. for mol in self.selmols[:]: # "externs" contains a list of bonds between this chunk and a different chunk for b in mol.externs[:]: # atom1 and atom2 are the connect atoms in the bond if int(b.atom1.molecule.picked) + int( b.atom2.molecule.picked) == 1: b.bust() cutbonds += 1 msg = fix_plurals("%d bond(s) deleted" % cutbonds) env.history.message(cmd + msg) if self.selatoms and cutbonds: self.modifySeparate( ) # Separate the selected atoms into a new chunk else: self.w.win_update() #e do this in callers instead? return
def selectConnected(self, atomlist = None): """ Selects any atom that can be reached from any currently selected atom through a sequence of bonds. @param atomlist: If supplied, use this list of atoms to select connected atoms instead of the currently selected atoms. @type atomlist: List of atoms. @attention: Only correctly reports the number newly selected atoms. """ ###@@@ should make sure we don't traverse interspace bonds, until all bugs creating them are fixed cmd = greenmsg("Select Connected: ") if atomlist is None and not self.selatoms: msg = redmsg("No atoms selected") env.history.message(cmd + msg) return if atomlist is None: # test for None since atomlist can be an empty list. atomlist = self.selatoms.values() catoms = self.getConnectedAtoms(atomlist) if not len(catoms): return natoms = 0 for atom in catoms[:]: if not atom.picked: atom.pick() if atom.picked: # Just in case a selection filter was applied to this atom. natoms += 1 else: natoms += 1 # Counts atom that is already picked. from platform_dependent.PlatformDependent import fix_plurals info = fix_plurals( "%d new atom(s) selected." % natoms) env.history.message( cmd + info) self.o.gl_update()
def modifyDeleteBonds(self): """ Delete all bonds between selected and unselected atoms or chunks """ cmd = greenmsg("Delete Bonds: ") if not self.selatoms and not self.selmols: # optimization, and different status msg msg = redmsg("Nothing selected") env.history.message(cmd + msg) return cutbonds = 0 # Delete bonds between selected atoms and their neighboring atoms that are not selected. for a in self.selatoms.values(): for b in a.bonds[:]: neighbor = b.other(a) if neighbor.element != Singlet: if not neighbor.picked: b.bust() a.pick() # Probably not needed, but just in case... cutbonds += 1 # Delete bonds between selected chunks and chunks that are not selected. for mol in self.selmols[:]: # "externs" contains a list of bonds between this chunk and a different chunk for b in mol.externs[:]: # atom1 and atom2 are the connect atoms in the bond if int(b.atom1.molecule.picked) + int(b.atom2.molecule.picked) == 1: b.bust() cutbonds += 1 msg = fix_plurals("%d bond(s) deleted" % cutbonds) env.history.message(cmd + msg) if self.selatoms and cutbonds: self.modifySeparate() # Separate the selected atoms into a new chunk else: self.w.win_update() #e do this in callers instead? return
def Stretch(self): """ stretch a Chunk """ mc = env.begin_op("Stretch") try: cmd = greenmsg("Stretch: ") if not self.selmols: msg = redmsg("No selected chunks to stretch") env.history.message(cmd + msg) else: self.changed() for m in self.selmols: m.stretch(1.1) self.o.gl_update() # Added history message. Mark 050413. info = fix_plurals( "Stretched %d chunk(s)" % len(self.selmols)) env.history.message( cmd + info) finally: env.end_op(mc) return
def align(self): """ """ cmd = greenmsg("Align to Common Axis: ") if len(self.selmols) < 2: msg = redmsg("Need two or more selected chunks to align") env.history.message(cmd + msg) return self.changed() #bruce 050131 bugfix or precaution #ax = V(0,0,0) #for m in self.selmols: # ax += m.getaxis() #ax = norm(ax) ax = self.selmols[0].getaxis() # Axis of first selected chunk for m in self.selmols[1:]: m.rot(Q(m.getaxis(),ax)) self.o.gl_update() info = fix_plurals( "Aligned %d chunk(s)" % (len(self.selmols) - 1) ) \ + " to chunk %s" % self.selmols[0].name env.history.message( cmd + info)
def alignmove(self): cmd = greenmsg("Move to Axis: ") if len(self.selmols) < 2: msg = redmsg("Need two or more selected chunks to align") env.history.message(cmd + msg) return self.changed() #ax = V(0,0,0) #for m in self.selmols: # ax += m.getaxis() #ax = norm(ax) ax = self.selmols[0].getaxis() # Axis of first selected chunk ctr = self.selmols[0].center # Center of first selected chunk for m in self.selmols[1:]: m.rot(Q(m.getaxis(),ax)) m.move(ctr-m.center) # offset self.o.gl_update() info = fix_plurals( "Aligned %d chunk(s)" % (len(self.selmols) - 1) ) \ + " to chunk %s" % self.selmols[0].name env.history.message( cmd + info)
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?
def writepdb(part, filename, mode = 'w', excludeFlags = EXCLUDE_BONDPOINTS | EXCLUDE_HIDDEN_ATOMS ): """ Write a PDB file of the I{part}. @param part: The part. @type part: assembly @param filename: The fullpath of the PDB file to write. We don't care if it has the .pdb extension or not. @type filename: string @param mode: 'w' for writing (the default) 'a' for appending @type mode: string @param excludeFlags: used to exclude certain atoms from being written, where: WRITE_ALL_ATOMS = 0 (even writes hidden and invisble atoms) EXCLUDE_BONDPOINTS = 1 (excludes bondpoints) EXCLUDE_HIDDEN_ATOMS = 2 (excludes invisible atoms) EXCLUDE_DNA_ATOMS = 4 (excludes PAM3 and PAM5 pseudo atoms) EXCLUDE_DNA_AXIS_ATOMS = 8 (excludes PAM3 axis atoms) EXCLUDE_DNA_AXIS_BONDS = 16 (supresses PAM3 axis bonds) @type excludeFlags: integer @note: Atoms and bonds of hidden chunks are never written. @see: U{B{PDB File Format}<http://www.wwpdb.org/documentation/format23/v2.3.html>} """ if mode != 'a': # Precaution. Mark 2007-06-25 mode = 'w' f = open(filename, mode) # doesn't yet detect errors in opening file [bruce 050927 comment] # Atom object's key is the key, the atomSerialNumber is the value atomsTable = {} # Each element of connectLists is a list of atoms to be connected with the # 1st atom in the list, i.e. the atoms to write into a CONECT record connectLists = [] atomSerialNumber = 1 from protein.model.Protein import enableProteins def exclude(atm): #bruce 050318 """ Exclude this atom (and bonds to it) from the file under the following conditions (as selected by excludeFlags): - if it is a singlet - if it is not visible - if it is a member of a hidden chunk - some dna-related conditions (see code for details) """ # Added not visible and hidden member of chunk. This effectively deletes # these atoms, which might be considered a bug. # Suggested solutions: # - if the current file is a PDB and has hidden atoms/chunks, warn user # before quitting NE1 (suggest saving as MMP). # - do not support native PDB. Open PDBs as MMPs; only allow export of # PDB. # Fixes bug 2329. Mark 070423 if excludeFlags & EXCLUDE_BONDPOINTS: if atm.element == Singlet: return True # Exclude if excludeFlags & EXCLUDE_HIDDEN_ATOMS: if not atm.visible(): return True # Exclude if excludeFlags & EXCLUDE_DNA_AXIS_ATOMS: ## if atm.element.symbol in ('Ax3', 'Ae3'): #bruce 080320 bugfix: revise to cover new elements and PAM5. if atm.element.role == 'axis': return True # Exclude if excludeFlags & EXCLUDE_DNA_ATOMS: # PAM5 atoms begin at 200. # # REVIEW: better to check atom.element.pam? # What about "carbon nanotube pseudoatoms"? # [bruce 080320 question] if atm.element.eltnum >= 200: return True # Exclude # Always exclude singlets connected to DNA p-atoms. if atm.element == Singlet: for a in atm.neighbors(): if a.element.eltnum >= 200: # REVIEW: see above comment about atom.element.pam vs >= 200 return True return False # Don't exclude. excluded = 0 molnum = 1 chainIdChar = 65 # ASCII "A" if mode == 'w': writePDB_Header(f) for mol in part.molecules: if mol.hidden: # Atoms and bonds of hidden chunks are never written. continue for a in mol.atoms.itervalues(): if exclude(a): excluded += 1 continue atomConnectList = [] atomsTable[a.key] = atomSerialNumber if enableProteins: # piotr 080709 : Use more robust ATOM output code for Proteins. resId = 1 resName = "UNK" atomName = a.element.symbol if mol.protein: res = mol.protein.get_residuum(a) if res: resId = res.get_id() resName = res.get_three_letter_code() atomName = res.get_atom_name(a) writepdb_atom(a, f, atomSerialNumber, atomName, chr(chainIdChar), resId, resName) else: a.writepdb(f, atomSerialNumber, chr(chainIdChar)) atomConnectList.append(a) for b in a.bonds: a2 = b.other(a) # The following removes bonds b/w PAM3 axis atoms. if excludeFlags & EXCLUDE_DNA_AXIS_BONDS: ## if a.element.symbol in ('Ax3', 'Ae3'): ## if a2.element.symbol in ('Ax3', 'Ae3'): ## continue #bruce 080320 bugfix: revise to cover new elements and PAM5. if a.element.role == 'axis' and a2.element.role == 'axis': continue if a2.key in atomsTable: assert not exclude(a2) # see comment below atomConnectList.append(a2) #bruce 050318 comment: the old code wrote every bond twice # (once from each end). I doubt we want that, so now I only # write them from the 2nd-seen end. (This also serves to # not write bonds to excluded atoms, without needing to check # that directly. The assert verifies this claim.) atomSerialNumber += 1 if len(atomConnectList) > 1: connectLists.append(atomConnectList) # bruce 050318 comment: shouldn't we leave it out if # len(atomConnectList) == 1? # I think so, so I'm doing that (unlike the previous code). # Write the chain TER-minator record # # COLUMNS DATA TYPE FIELD DEFINITION # ------------------------------------------------------ # 1 - 6 Record name "TER " # 7 - 11 Integer serial Serial number. # 18 - 20 Residue name resName Residue name. # 22 Character chainID Chain identifier. # 23 - 26 Integer resSeq Residue sequence number. # 27 AChar iCode Insertion code. f.write("TER %5d %1s\n" % (molnum, chr(chainIdChar))) molnum += 1 chainIdChar += 1 if chainIdChar > 126: # ASCII "~", end of PDB-acceptable chain chars chainIdChar = 32 # Rollover to ASCII " " for atomConnectList in connectLists: # Begin CONECT record ---------------------------------- f.write("CONECT") for a in atomConnectList: index = atomsTable[a.key] f.write("%5d" % index) f.write("\n") # End CONECT record ---------------------------------- connectLists = [] f.write("END\n") f.close() if excluded: msg = "Warning: excluded %d open bond(s) from saved PDB file; " \ % excluded msg += "consider Hydrogenating and resaving." msg = fix_plurals(msg) env.history.message( orangemsg(msg)) return # from writepdb
def run(self): """ Minimize (or Adjust) the Selection or the current Part """ #bruce 050324 made this method from the body of MWsemantics.modifyMinimize # and cleaned it up a bit in terms of how it finds the movie to use. #bruce 050412 added 'Sel' vs 'All' now that we have two different Minimize buttons. # In future the following code might become subclass-specific (and cleaner): ## fyi: this old code was incorrect, I guess since 'in' works by 'is' rather than '==' [not verified]: ## assert self.args in [['All'], ['Sel']], "%r" % (self.args,) #bruce 051129 revising this to clarify it, though command-specific subclasses would be better assert len(self.args) >= 1 cmd_subclass_code = self.args[0] cmd_type = self.kws.get('type', 'Minimize') # one of 'Minimize' or 'Adjust' or 'Adjust Atoms'; determines conv criteria, name [bruce 060705] self.cmd_type = cmd_type # kluge, see comment where used engine = self.kws.get('engine', MINIMIZE_ENGINE_UNSPECIFIED) if (engine == MINIMIZE_ENGINE_UNSPECIFIED): engine = env.prefs[Adjust_minimizationEngine_prefs_key] if (engine == MINIMIZE_ENGINE_GROMACS_FOREGROUND): self.useGromacs = True self.background = False elif (engine == MINIMIZE_ENGINE_GROMACS_BACKGROUND): self.useGromacs = True self.background = True else: self.useGromacs = False self.background = False assert cmd_subclass_code in ['All', 'Sel', 'Atoms'] #e and len(args) matches that? # These words and phrases are used in history messages and other UI text; # they should be changed by specific commands as needed. # See also some computed words and phrases, e.g. self.word_Minimize, # below the per-command if stamements. [bruce 060705] # Also set flags for other behavior which differs between these commands. if cmd_type.startswith('Adjust'): self.word_minimize = "adjust" self.word_minimization = "adjustment" self.word_minimizing = "adjusting" anchor_all_nonmoving_atoms = False pass else: assert cmd_type.startswith('Minimize') self.word_minimize = "minimize" self.word_minimization = "minimization" self.word_minimizing = "minimizing" anchor_all_nonmoving_atoms = True #bruce 080513 revision to implement nfr bug 2848 item 2 # (note: we might decide to add a checkbox for this into the UI, # and just change its default value for Minimize vs Adjust) pass self.word_Minimize = _capitalize_first_word( self.word_minimize) self.word_Minimizing = _capitalize_first_word( self.word_minimizing) if cmd_subclass_code == 'All': cmdtype = _MIN_ALL cmdname = "%s All" % self.word_Minimize elif cmd_subclass_code == 'Sel': cmdtype = _MIN_SEL cmdname = "%s Selection" % self.word_Minimize elif cmd_subclass_code == 'Atoms': #bruce 051129 added this case for Local Minimize (extending a kluge -- needs rewrite to use command-specific subclass) cmdtype = _LOCAL_MIN cmdname = "%s Atoms" % self.word_Minimize #bruce 060705; some code may assume this is always Adjust Atoms, as it is # self.args is parsed later else: assert 0, "unknown cmd_subclass_code %r" % (cmd_subclass_code,) self.cmdname = cmdname #e in principle this should come from a subclass for the specific command [bruce 051129 comment] startmsg = cmdname + ": ..." del cmd_subclass_code # remove model objects inserted only for feedback from prior runs # (both because it's a good feature, and to avoid letting them # mess up this command) [bruce 080520] from simulation.runSim import part_contains_pam_atoms # kluge to use this function for this purpose # (it's called later for other reasons) hasPAM_junk = part_contains_pam_atoms( self.part, kill_leftover_sim_feedback_atoms = True ) self.part.assy.update_parts() ###k is this always safe or good? # Make sure some chunks are in the part. # (Valid for all cmdtypes -- Minimize only moves atoms, even if affected by jigs.) if not self.part.molecules: # Nothing in the part to minimize. env.history.message(greenmsg(cmdname + ": ") + redmsg("Nothing to %s." % self.word_minimize)) return if cmdtype == _MIN_SEL: selection = self.part.selection_from_glpane() # compact rep of the currently selected subset of the Part's stuff if not selection.nonempty(): msg = greenmsg(cmdname + ": ") + redmsg("Nothing selected.") + \ " (Use %s All to %s the entire Part.)" % (self.word_Minimize, self.word_minimize) #e might need further changes for Minimize Energy, if it's confusing that Sel/All is a dialog setting then env.history.message( msg) return elif cmdtype == _LOCAL_MIN: from operations.ops_select import selection_from_atomlist junk, atomlist, ntimes_expand = self.args selection = selection_from_atomlist( self.part, atomlist) #e in cleaned up code, selection object might come from outside selection.expand_atomset(ntimes = ntimes_expand) # ok if ntimes == 0 # Rationale for adding monovalent atoms to the selection before # instantiating the sim_aspect # # (Refer to comments for sim_aspect.__init__.) Why is it safe to add # monovalent atoms to a selection? Let's look at what happens during a # local minimization. # # While minimizing, we want to simulate as if the entire rest of the # part is grounded, and only our selection of atoms is free to move. The # most obvious approach would be to minimize all the atoms in the part # while applying anchors to the atoms that aren't in the selection. But # minimizing all the atoms, especially if the selection is small, is very # wasteful. Applying the simulator to atoms is expensive and we want to # minimize as few atoms as possible. # # [revision, bruce 080513: this discussion applies for Adjust, # but the policy for Minimize is being changed to always include # all atoms, even if most of them are anchored, # re nfr bug 2848 item 2.] # # A more economical approach is to anchor the atoms for two layers going # out from the selection. The reason for going out two layers, and not just # one layer, is that we need bond angle terms to simulate accurately. When # we get torsion angles we will probably want to bump this up to three # layers. [Now we're doing three layers -- bruce 080507] # # Imagine labeling all the atoms in the selection with zero. Then take the # set of unlabeled atoms that are bonded to a zero-labeled atom, and label # all the atoms in that set with one. Next, take the set of yet-unlabeled # atoms that are bonded to a one-labeled atom, and label the atoms in that # set with two. The atoms labeled one and two become our first and second # layers, and we anchor them during the minimization. # # In sim_aspect.__init__, the labels for zero, one and two correspond # respectively to membership in the dictionaries self._moving_atoms, # self._boundary1_atoms, and self._boundary2_atoms. # # If an atom in the selection is anchored, we don't need to go two layers # out from that atom, only one layer. So we can label it with one, even # though it's a member of the selection and would normally be labeled with # zero. The purpose in doing this is to give the simulator a few less atoms # to worry about. # # If a jig includes one of the selected atoms, but additionally includes # atoms outside the selection, then it may not be obvious how to simulate # that jig. For the present, the only jig that counts in a local # minimization is an anchor, because all the other jigs are too complicated # to simulate. # # The proposed fix here has the effect that monovalent atoms bonded to # zero-labeled atoms are also labeled zero, rather than being labeled one, # so they are allowed to move. Why is this OK to do? # # (1) Have we violated the assumption that the rest of the part is locked # down? Yes, as it applies to those monovalent atoms, but they are # presumably acceptable violations, since bug 1240 is regarded as a bug. # # (2) Have we unlocked any bond lengths or bond angles that should remain # locked? Again, only those which involve (and necessarily end at) the # monovalent atoms in question. The same will be true when we introduce # torsion terms. # # (3) Have we lost any ground on the jig front? If a jig includes one or # more of the monovalent atoms, possibly - but the only jigs we are # simulating in this case is anchors, and those will be handled correctly. # Remember that anchored atoms are only extended one layer, not two, but # with a monovalent atom bonded to a selected atom, no extension is # possible at all. # # One can debate about whether bug 1240 should be regarded as a bug. But # having accepted it as a bug, one cannot object to adding these monovalents # to the original selection. # # wware 060410 bug 1240 atoms = selection.selatoms for atom in atoms.values(): # enumerate the monovalents bonded to atom for atom2 in filter(lambda atom: not atom.is_singlet(), atom.baggageNeighbors()): atoms[atom2.key] = atom2 else: assert cmdtype == _MIN_ALL selection = self.part.selection_for_all() # like .selection_from_glpane() but for all atoms presently in the part [bruce 050419] # no need to check emptiness, this was done above self.selection = selection #e might become a feature of all CommandRuns, at some point # At this point, the conditions are met to try to do the command. env.history.message(greenmsg( startmsg)) #bruce 050412 doing this earlier # Disable some QActions (menu items/toolbar buttons) during minimize. self.win.disable_QActions_for_sim(True) try: simaspect = sim_aspect( self.part, selection.atomslist(), cmdname_for_messages = cmdname, anchor_all_nonmoving_atoms = anchor_all_nonmoving_atoms ) #bruce 051129 passing cmdname # note: atomslist gets atoms from selected chunks, not only selected atoms # (i.e. it gets atoms whether you're in Select Atoms or Select Chunks mode) # history message about singlets written as H (if any); #bruce 051115 updated comment: this is used for both Minimize All and Minimize Selection as of long before 051115; # for Run Sim this code is not used (so this history message doesn't go out for it, though it ought to) # but the bug254 X->H fix is done (though different code sets the mapping flag that makes it happen). nsinglets_H = simaspect.nsinglets_H() if nsinglets_H: #bruce 051209 this message code is approximately duplicated elsewhere in this file info = fix_plurals( "(Treating %d bondpoint(s) as Hydrogens, during %s)" % (nsinglets_H, self.word_minimization) ) env.history.message( info) nsinglets_leftout = simaspect.nsinglets_leftout() assert nsinglets_leftout == 0 # for now # history message about how much we're working on; these atomcounts include singlets since they're written as H nmoving = simaspect.natoms_moving() nfixed = simaspect.natoms_fixed() info = fix_plurals( "(%s %d atom(s)" % (self.word_Minimizing, nmoving)) if nfixed: them_or_it = (nmoving == 1) and "it" or "them" if anchor_all_nonmoving_atoms: msg2 = "holding remaining %d atom(s) fixed" % nfixed else: msg2 = "holding %d atom(s) fixed around %s" % (nfixed, them_or_it) info += ", " + fix_plurals(msg2 ) info += ")" env.history.message( info) self.doMinimize(mtype = 1, simaspect = simaspect) # mtype = 1 means single-frame XYZ file. # [this also sticks results back into the part] #self.doMinimize(mtype = 2) # 2 = multi-frame DPB file. finally: self.win.disable_QActions_for_sim(False) simrun = self._movie._simrun #bruce 050415 klugetower if not simrun.said_we_are_done: env.history.message("Done.") return
def make_cmenuspec_for_set(self, nodeset, nodeset_whole, optflag): """ #doc... see superclass docstring, and the term "menu_spec" """ # Note: we use nodeset_whole (a subset of nodeset, or equal to it) # for operations which might be unsafe on partly-selected "whole nodes" # (i.e. unopenable nodes such as DnaStrand). # (Doing this on the Group operation is part of fixing bug 2948.) # These menu commands need corresponding changes in their cm methods, # namely, calling deselect_partly_picked_whole_nodes at the start. # All cm_duplicate methods (on individual nodes) also may need to do # this if they care about the selection. # [bruce 081218] #e some advice [bruce]: put "display changes" (eg Hide) before "structural changes" (such as Group/Ungroup)... #e a context-menu command "duplicate" which produces ##a copy of them, with related names and a "sibling" position. ##Whereas the menu command called "copy" produces a copy of the selected ##things in the system-wide "clipboard" shared by all apps.) # I think we might as well remake this every time, for most kinds of menus, # so it's easy for it to depend on current state. # I really doubt this will be too slow. [bruce 050113] if not nodeset: #e later we'll add useful menu commands for no nodes, # i.e. for a "context menu of the background". # In fact, we'll probably remove this special case # and instead let each menu command decide whether it applies # in this case. res = [('Model Tree (nothing selected)', noop, 'disabled')] #bruce 050505 adding some commands here (cm_delete_clipboard is a just-reported NFR from Mark) res.append(('Create new empty clipboard item', self.cm_new_clipboard_item)) lenshelf = len( self.assy.shelf.MT_kids()) #bruce 081217 use MT_kids if lenshelf: if lenshelf > 2: text = 'Delete all %d clipboard items' % lenshelf else: text = 'Delete all clipboard items' res.append((text, self.cm_delete_clipboard)) return res res = [] if len(nodeset_whole) < len(nodeset): # alert user to presence of partly-selected items [bruce 081218] # (which are not marked as selected on nodes seeable in MT) # (review: should we mark them in some other way?) # # (note about older text, # "deselect %d partly-selected item(s)": # the count is wrong if one partly-selected leaflike group # contains more than one selected node, and nothing explains # the situation well to the user) # # (about this text: it might be ambiguous whether they're too deep # because of groups being closed, or being leaflike; mitigated # by saying "shown" rather than "visible") text = "Deselect %d node(s) too deep to be shown" % \ (len(nodeset) - len(nodeset_whole)) text = fix_plurals(text) res.append((text, self.cm_deselect_partly_selected_items)) res.append(None) pass # old comment, not recently reviewed/updated as of 081217: # first put in a Hide item, checked or unchecked. But what if the hidden-state is mixed? # then there is a need for two menu commands! Or, use the command twice, fully hide then fully unhide -- not so good. # Hmm... let's put in Hide (with checkmark meaning "all hidden"), then iff that's not enough, Unhide. # So how do we know if a node is hidden -- this is only defined for leaf nodes now! # I guess we figure it out... I guess we might as well classify nodeset and its kids. # [update, bruce 080108/080306: does "and its kids" refer to members, or MT_kids? # It might be some of each -- we would want to include members present but not shown # in the MT (like the members of DnaGroup or DnaStrand), which are in members but not in # MT_kids, but we might also want to cover "shared members", like DnaStrandChunks, # which *might* be included in both strands and segments for this purpose (in the future; # shared members are NIM now).] allstats = all_attrs_act_as_counters() for node in nodeset: node_stats = all_attrs_act_as_counters() node.apply2all( lambda node1: mt_accumulate_stats(node1, node_stats)) allstats += node_stats # totals to allstats # Hide command (and sometimes Unhide) # now can we figure out how much is/could be hidden, etc #e (later, modularize this, make assertfails only affect certain menu commands, etc) nleafs = allstats.n - allstats.ngroups assert nleafs >= 0 nhidden = allstats.nhidden nunhidden = nleafs - nhidden # since only leafs can be hidden assert nunhidden >= 0 # We'll always define a Hide item. Checked means all is hidden (and the command will be unhide); # unchecked means not all is hidden (and the command will be hide). # First handle degenerate case where there are no leafs selected. if nleafs == 0: res.append( ('Hide', noop, 'disabled')) # nothing that can be hidden elif nunhidden == 0: # all is hidden -- show that, and offer to unhide it all ## res.append(( 'Hidden', self.cm_unhide, 'checked')) res.append(('Unhide', self.cm_unhide)) # will this be better? ##e do we want special cases saying "Unhide All", here and below, # when all hidden items would be unhidden, or vice versa? # (on PartGroup, or in other cases, so detect by comparing counts for sel and tree_node.) elif nhidden > 0: # some is not hidden, some is hidden -- make this clear & offer both extremes ## res.append(( 'Hide (' + fix_plurals('%d item(s)' % nunhidden) + ')', self.cm_hide )) #e fix_plurals bug, worked around res.append( (fix_plurals('Unhide %d item(s)' % nhidden), self.cm_unhide)) res.append( (fix_plurals('Hide %d item(s)' % nunhidden), self.cm_hide)) else: # all is unhidden -- just offer to hide it res.append(('Hide', self.cm_hide)) try: njigs = allstats.njigs if njigs == 1 and allstats.n == 1: # exactly one jig selected. Show its disabled state, with option to change this if permitted. # warning: depends on details of Jig.is_disabled() implem. Ideally we should ask Jig to contribute # this part of the menu-spec itself #e. [bruce 050421] jig = nodeset[0] if not isinstance( jig, RectGadget ): # remove this menu item for RectGadget [Huaicai 10/11/05] disabled_must = jig.disabled_by_atoms( ) # (by its atoms being in the wrong part) disabled_choice = jig.disabled_by_user_choice disabled_menu_item = disabled_must # menu item is disabled iff jig disabled state can't be changed, ie is "stuck on" checked = disabled_must or disabled_choice # menu item is checked if it's disabled for whatever reason (also affects text) if checked: command = self.cm_enable if disabled_must: text = "Disabled (atoms in other Part)" else: text = "Disabled" else: command = self.cm_disable text = "Disable" res.append( (text, command, checked and 'checked' or None, disabled_menu_item and 'disabled' or None)) except: print "bug in MT njigs == 1, ignored" ## raise # just during devel pass if nodeset_whole: res.append(None) # separator # (from here on, only add these at start of optional items # or sets of items) # Group command -- only offered for 2 or more subtrees of any Part, # or for exactly one clipboard item topnode itself if it's not already a Group. # [rules loosened by bruce 050419-050421] if optflag or len(nodeset_whole) >= 2: # note that these nodes are always in the same Part and can't include its topnode ok = True else: # exactly one node - ok iff it's a clipboard item and not a group node = nodeset_whole[0] ok = (node.dad is self.shelf_node and not node.is_group()) if not ok: res.append(('Group', noop, 'disabled')) else: res.append(('Group', self.cm_group)) # Ungroup command -- only when exactly one picked Group is what we have, of a suitable kind. # (As for Group, later this can become more general, tho in this case it might be general # enough already -- it's more "self-contained" than the Group command can be.) offered_ungroup = False # modified below; used by other menu items farther below if len(nodeset_whole) == 1 and nodeset_whole[0].permits_ungrouping( ): # (this implies it's a group, or enough like one) node = nodeset_whole[0] if not node.members: #bruce 080207 #REVIEW: use MT_kids? [same issue in many places in this file, as of 080306] #reply, bruce 081217: not yet; really we need a new Node or Group API method # "offer to remove as empty Group"; meanwhile, be conservative by using .members text = "Remove empty Group" elif node.dad == self.shelf_node and len(node.members) > 1: # todo: "Ungroup into %d separate clipboard item(s)" text = "Ungroup into separate clipboard items" #bruce 050419 new feature (distinct text in this case) else: # todo: "Ungroup %d item(s)" text = "Ungroup" res.append((text, self.cm_ungroup)) offered_ungroup = True else: # review: is this clear enough for nodes that are internally Groups # but for which permits_ungrouping is false, or would some other # text be better, or would leaving this item out be better? # An old suggestion of "Ungroup (unsupported)" seems bad now, # since it might sound like "a desired feature that's nim". # [bruce 081212 comment] res.append(("Ungroup", noop, 'disabled')) # Remove all %d empty Groups (which permit ungrouping) [bruce 080207] count_holder = [0] def func(group, count_holder=count_holder): if not group.members and group.permits_ungrouping(): count_holder[ 0] += 1 # UnboundLocalError when this was count += 1 for node in nodeset_whole: node.apply_to_groups( func ) # note: this descends into groups that don't permit ungrouping, e.g. DnaStrand count = count_holder[0] if count == 1 and len( nodeset_whole) == 1 and not nodeset_whole[0].members: # this is about the single top selected node, # so it's redundant with the Ungroup command above # (and if that was not offered, this should not be either) pass elif count: res.append(('Remove all %d empty Groups' % count, self.cm_remove_empty_groups)) # lack of fix_plurals seems best here; review when seen else: pass pass # Edit Properties command -- only provide this when there's exactly one thing to apply it to, # and it says it can handle it. ###e Command name should depend on what the thing is, e.g. "Part Properties", "Chunk Properties". # Need to add methods to return that "user-visible class name". res.append(None) # separator if debug_flags.atom_debug: if len(nodeset) == 1: res.append(("debug._node =", self.cm_set_node)) else: res.append(("debug._nodeset =", self.cm_set_node)) if len(nodeset) == 1 and nodeset[0].editProperties_enabled(): res.append(('Edit Properties...', self.cm_properties)) else: res.append(('Edit Properties...', noop, 'disabled')) # nim for multiple items #ninad 070320 - context menu option to edit color of multiple chunks if allstats.nchunks: res.append(("Edit Chunk Color...", self.cmEditChunkColor)) if allstats.canShowOverlayText: res.append(("Show Overlay Text", self.cmShowOverlayText)) if allstats.canHideOverlayText: res.append(("Hide Overlay Text", self.cmHideOverlayText)) #bruce 070531 - rename node -- temporary workaround for inability to do this in MT, or, maybe we'll like it to stay if len(nodeset) == 1: node = nodeset[0] if node.rename_enabled(): res.append( ("Rename node...", self.cmRenameNode) ) ##k should it be called node or item in this menu text? # subsection of menu (not a submenu unless they specify one) # for node-class-specific menu items, when exactly one node # (old way, based on methodnames that start with __CM; # and new better way, using Node method ModelTree_context_menu_section) if len(nodeset) == 1: node = nodeset[0] submenu = [] attrs = filter(lambda attr: "__CM_" in attr, dir( node.__class__)) #e should do in order of superclasses attrs.sort() # ok if empty list #bruce 050708 -- provide a way for these custom menu items to specify a list of menu_spec options (e.g. 'disabled') -- # they should define a method with the same name + "__options" and have it return a list of options, e.g. ['disabled'], # or [] if it doesn't want to provide any options. It will be called again every time the context menu is shown. # If it wants to remove the menu item entirely, it can return the special value (not a list) 'remove'. opts = {} for attr in attrs: # pass 1 - record menu options for certain commands if attr.endswith("__options"): boundmethod = getattr(node, attr) try: lis = boundmethod() assert type(lis) == type([]) or lis == 'remove' opts[attr] = lis # for use in pass 2 except: print_compact_traceback( "exception ignored in %r.%s(): " % (node, attr)) pass for attr in attrs: # pass 2 if attr.endswith("__options"): continue classname, menutext = attr.split("__CM_", 1) boundmethod = getattr(node, attr) if callable(boundmethod): lis = opts.get(attr + "__options") or [] if lis != 'remove': mitem = tuple( [menutext.replace('_', ' '), boundmethod] + lis) submenu.append(mitem) elif boundmethod is None: # kluge: None means remove any existing menu items (before the submenu) with this menutext! res = filter( lambda text_cmd: text_cmd and text_cmd[0] != menutext, res) # text_cmd might be None while res and res[0] == None: res = res[1:] #e should also remove adjacent Nones inside res else: assert 0, "not a callable or None: %r" % boundmethod if submenu: ## res.append(( 'other', submenu )) #e improve submenu name, ordering, location res.extend( submenu ) # changed append to extend -- Mark and Bruce at Retreat 050621 # new system, used in addition to __CM above (preferred in new code): # [bruce 080225] try: submenu = node.ModelTree_context_menu_section() assert submenu is not None # catch a likely possible error specifically assert type(submenu) is type( []) # it should be a menu_spec list except: print_compact_traceback("exception ignored in %r.%s() " \ "or in checking its result: " % \ (node, 'ModelTree_context_menu_section')) submenu = [] if submenu: res.extend(submenu) pass if nodeset_whole: # copy, cut, delete, maybe duplicate... # bruce 050704 revisions: # - these are probably ok for clipboard items; I'll enable them there and let them be tested there. # - I'll remove Copy when the selection only contains jigs that won't copy themselves # unless some of their atoms are copied (which for now is true of all jigs). # More generally (in principle -- the implem is not general), Copy should be removed # when the selection contains nothing which makes sense to copy on its own, # only things which make sense to copy only in conjunction with other things. # I think this is equivalent to whether all the selected things would fail to get copied, # when the copy command was run. # - I'll add Duplicate for single selected jigs which provide an appropriate method, # and show it dimmed for those that don't. res.append(None) # separator # figure out whether Copy would actually copy anything. part = nodeset_whole[ 0].part # the same for all nodes in nodeset_whole sel = selection_from_part( part, use_selatoms=False ) #k should this be the first code to use selection_from_MT() instead? doit = False for node in nodeset_whole: if node.will_copy_if_selected(sel, False): #wware 060329 added realCopy arg, False here (this is not a real copy, so do not issue a warning). #bruce 060329 points out about realCopy being False vs True that at this point in the code we don't # yet know whether the real copy will be made, and when we do, will_copy_if_selected # might like to be re-called with True, but that's presently nim. ###@@@ # # if this test is too slow, could inline it by knowing about Jigs here; but better to speed it up instead! doit = True break if doit: res.append(('Copy', self.cm_copy)) # For single items, add a Duplicate command and enable it if they support the method. [bruce 050704 new feature] # For now, hardly anything offers this command, so I'm changing the plan, and removing it (not disabling it) # when not available. This should be reconsidered if more things offer it. if len(nodeset_whole) == 1: node = nodeset_whole[0] try: method = node.cm_duplicate # Warning 1: different API than self.cm_xxx methods (arg differs) # or __CM_ methods (disabled rather than missing, if not defined). # Warning 2: if a class provides it, no way for a subclass to stop # providing it. This aspect of the API is bad, should be revised. # Warning 3: consider whether each implem of this needs to call # self.deselect_partly_picked_whole_nodes(). assert callable(method) except: dupok = False else: dupok = True if dupok: res.append(('Duplicate', method)) else: pass ## res.append(( 'Duplicate', noop, 'disabled' )) # Cut (unlike Copy), and Delete, should always be ok. res.append(('Cut', self.cm_cut)) res.append(('Delete', self.cm_delete)) #ninad060816 added option to select all atoms of the selected chunks. #I don't know how to handle a case when a whole group is selected. #So putting a condition allstats.nchunks == allstats.n. #Perhaps, I should unpick the groups while picking atoms? if allstats.nchunks == allstats.n and allstats.nchunks: res.append( (fix_plurals("Select all atoms of %d chunk(s)" % allstats.nchunks), self.cmSelectAllAtomsInChunk)) # add basic info on what's selected at the end # (later might turn into commands related to subclasses of nodes) if allstats.nchunks + allstats.njigs: # otherwise, nothing we can yet print stats on... (e.g. clipboard) res.append(None) # separator res.append(("selection:", noop, 'disabled')) if allstats.nchunks: res.append((fix_plurals("%d chunk(s)" % allstats.nchunks), noop, 'disabled')) if allstats.njigs: res.append((fix_plurals("%d jig(s)" % allstats.njigs), noop, 'disabled')) if allstats.nhidden: res.append(("(%d of these are hidden)" % allstats.nhidden, noop, 'disabled')) if allstats.njigs == allstats.n and allstats.njigs: # only jigs are selected -- offer to select their atoms [bruce 050504] # (text searches for this code might like to find "Select this jig's" or "Select these jigs'") want_select_item = True #bruce 051208 if allstats.njigs == 1: jig = nodeset[0] if isinstance( jig, RectGadget ): # remove menu item for RectGadget [Huaicai 10/11/05] ## return res -- this 'return' was causing bug 1189 by skipping the rest of the menu, not just this item. # Try to do something less drastic. [bruce 051208] want_select_item = False else: natoms = len(nodeset[0].atoms) myatoms = fix_plurals("this jig's %d atom(s)" % natoms) else: myatoms = "these jigs' atoms" if want_select_item: res.append( ('Select ' + myatoms, self.cm_select_jigs_atoms)) ## ##e following msg is not true, since nodeset doesn't include selection under selected groups! ## # need to replace it with a better breakdown of what's selected, ## # incl how much under selected groups is selected. Maybe we'll add a list of major types ## # of selected things, as submenus, lower down (with commands like "select only these", "deselect these"). ## ## res.append(( fix_plurals("(%d selected item(s))" % len(nodeset)), noop, 'disabled' )) # for single items that have a featurename, add wiki-help command [bruce 051201] if len(nodeset) == 1: node = nodeset[0] ms = wiki_help_menuspec_for_object( node ) # will be [] if this node should have no wiki help menu items #review: will this func ever need to know which widget is asking? if ms: res.append(None) # separator res.extend(ms) return res # from make_cmenuspec_for_set
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
def cm_group( self ): # bruce 050126 adding comments and changing behavior; 050420 permitting exactly one subtree """ put the selected subtrees (one or more than one) into a new Group (and update) """ ##e I wonder if option/alt/middleButton should be like a "force" or "power" flag # for cmenus; in this case, it would let this work even for a single element, # making a 1-item group. That idea can wait. [bruce 050126] #bruce 050420 making this work inside clipboard items too # TEST if assy.part updated in time ####@@@@ -- no, change to selgroup! self.deselect_partly_picked_whole_nodes() sg = self.assy.current_selgroup() node = sg.hindmost() # smallest nodetree containing all picked nodes if not node: env.history.message( "nothing selected to Group") # should never happen return if node.picked: #bruce 050420: permit this case whenever possible (formation of 1-item group); # cmenu constructor should disable or leave out the menu command when desired. if node != sg: assert node.dad # in fact, it'll be part of the same sg subtree (perhaps equal to sg) node = node.dad assert not node.picked # fall through -- general case below can handle this. else: # the picked item is the topnode of a selection group. # If it's the main part, we could make a new group inside it # containing all its children (0 or more). This can't happen yet # so I'll be lazy and save it for later. assert node != self.assy.tree # Otherwise it's a clipboard item. Let the Part take care of it # since it needs to patch up its topnode, choose the right name, # preserve its view attributes, etc. assert node.part.topnode == node newtop = node.part.create_new_toplevel_group() env.history.message( "made new group %s" % newtop.name ) ###k see if this looks ok with autogenerated name self.mt_update() return # (above 'if' might change node and then fall through to here) # node is an unpicked Group inside (or equal to) sg; # more than one of its children (or exactly one if we fell through from the node.picked case above) # are either picked or contain something picked (but maybe none of them are directly picked). # We'll make a new Group inside node, just before the first child containing # anything picked, and move all picked subtrees into it (preserving their order; # but losing their structure in terms of unpicked groups that contain some of them). ###e what do we do with the picked state of things we move? worry about the invariant! ####@@@@ # make a new Group (inside node, same assy) ###e future: require all assys the same, or, do this once per topnode or assy-node. # for now: this will have bugs when done across topnodes! # so the caller doesn't let that happen, for now. [050126] new = Group(gensym("Group", node.assy), node.assy, node) # was self.assy assert not new.picked # put it where we want it -- before the first node member-tree with anything picked in it for m in node.members: if m.haspicked(): assert m != new ## node.delmember(new) #e (addsibling ought to do this for us...) [now it does] m.addsibling(new, before=True) break # (this always happens, since something was picked under node) node.apply2picked(lambda (x): x.moveto(new)) # this will have skipped new before moving anything picked into it! # even so, I'd feel better if it unpicked them before moving them... # but I guess it doesn't. for now, just see if it works this way... seems to work. # ... later [050316], it evidently does unpick them, or maybe delmember does. msg = fix_plurals( "grouped %d item(s) into " % len(new.members)) + "%s" % new.name env.history.message(msg) # now, should we pick the new group so that glpane picked state has not changed? # or not, and then make sure to redraw as well? hmm... # - possibility 1: try picking the group, then see if anyone complains. # Caveat: future changes might cause glpane redraw to occur anyway, defeating the speed-purpose of this... # and as a UI feature I'm not sure what's better. # - possibility 2: don't pick it, do update glpane. This is consistent with Ungroup (for now) # and most other commands, so I'll do it. # # BTW, the prior code didn't pick the group # and orginally didn't unpick the members but now does, so it had a bug (failure to update # glpane to show new picked state), whose bug number I forget, which this should fix. # [bruce 050316] ## new.pick() # this will emit an undesirable history message... fix that? self.win.glpane.gl_update( ) #k needed? (e.g. for selection change? not sure.) self.mt_update() return
def select_bad_atoms_cmd( widget ): #bruce 060615 demo of simple "spelling checker" with hardcoded rules """Out of the selected atoms or chunks, select the atoms which have "bad spelling".""" from utilities.Log import orangemsg, redmsg, greenmsg greencmd = greenmsg("%s: " % cmdname) orangecmd = orangemsg( "%s: " % cmdname ) # used when bad atoms are found, even though no error occurred in the command itself win = env.mainwindow() assy = win.assy # 1. compile the patterns to search for. This could be done only once at init time, but it's fast so it doesn't matter. bad_patterns_dict, root_eltnums, other_eltnums = compile_patterns() # 2. Find the atoms to search from (all selected atoms, or atoms in selected chunks, are potential root atoms) checked_in_what = "selected atoms or chunks" contained = "contained" atoms = {} for m in assy.selmols: atoms.update(m.atoms) atoms.update(assy.selatoms) if 0: # do this if you don't like the feature of checking the entire model when nothing is selected. if not atoms: env.history.message( redmsg("%s: nothing selected to check." % cmdname)) return else: # if nothing is selected, work on the entire model. if not atoms: checked_in_what = "model" contained = "contains" for m in assy.molecules: atoms.update(m.atoms) if not atoms: env.history.message( redmsg("%s: model contains no atoms." % cmdname)) return pass # 3. Do the search. bad_triples = [] # list of bad triples of atoms (perhaps with overlap) for a in atoms.itervalues(): ae = a.element.eltnum if ae not in root_eltnums: continue checkbonds = [] for b in a.bonds: o = b.other(a) oe = o.element.eltnum if oe in other_eltnums: checkbonds.append((o, oe)) nbonds = len(checkbonds) if nbonds > 1: #e we could easily optimize the following loop for fixed nbonds like 2,3,4... or code it in pyrex. for i in xrange(nbonds - 1): for j in xrange(i + 1, nbonds): if (checkbonds[i][1], ae, checkbonds[j][1]) in bad_patterns_dict: # gotcha! bad_triples.append( (checkbonds[i][0], a, checkbonds[j][0])) if not bad_triples: env.history.message(greencmd + "no bad patterns found in %s." % checked_in_what) return # done - deselect all, then select bad atoms if any. (Should we also deselect if we found no bad atoms, above??) win.glpane.gl_update() assy.unpickall_in_GLPane() #bruce 060721; was unpickatoms and unpickparts bad_atoms = {} for a1, a2, a3 in bad_triples: bad_atoms[a1.key] = a1 bad_atoms[a2.key] = a2 bad_atoms[a3.key] = a3 reallypicked = 0 for a in bad_atoms.itervalues(): a.pick() reallypicked += (not not a.picked) # check for selection filter effect env.history.message(orangecmd + fix_plurals( "%s %s %d bad atom(s), in %d bad pattern(s)." % \ (checked_in_what, contained, len(bad_atoms), len(bad_triples)) )) if reallypicked < len(bad_atoms): env.history.message( orangemsg("Warning: ") + fix_plurals( "%d bad atom(s) were/was not selected due to the selection filter." % \ (len(bad_atoms) - reallypicked) )) win.mt.update_select_mode() return
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?
if _DEBUG_PRINT_BOND_DIRECTION_ERRORS: print "bond direction error for %r: %s" % (atom, error_data) print _atom_set_dna_updater_error( atom, error_data) atom.molecule.changeapp(0) #k probably not needed new_error_atoms[atom.key] = atom _global_direct_error_atoms[atom.key] = atom else: _atom_clear_dna_updater_error( atom) _global_direct_error_atoms.pop(atom.key, None) continue if new_error_atoms: #e if we print more below, this might be only interesting for debugging, not sure # maybe: move later since we will be expanding this set; or report number of base pairs msg = "Warning: dna updater noticed %d pseudoatom(s) with bond direction errors" % len(new_error_atoms) msg = fix_plurals(msg) env.history.orangemsg(msg) global _all_error_atoms_after_propogation old_all_error_atoms_after_propogation = _all_error_atoms_after_propogation new_all_error_atoms_after_propogation = {} for atom in _global_direct_error_atoms.itervalues(): for atom2 in _same_base_pair_atoms(atom): new_all_error_atoms_after_propogation[atom2.key] = atom2 _all_error_atoms_after_propogation = new_all_error_atoms_after_propogation for atom in old_all_error_atoms_after_propogation.itervalues(): if atom.key not in new_all_error_atoms_after_propogation:
def attach_to( self, singlet, autobond = True, autobond_msg = True): # in AtomTypeDepositionTool # [bruce 050831 added autobond option; 050901 added autobond_msg] """ [public method] Deposit a new atom of self.atomtype onto the given singlet, and (if autobond is true) make other bonds (to other near-enough atoms with singlets) as appropriate. (But never more than one bond per other real atom.) @return: a 2-tuple consisting of either the new atom and a description of it, or None and the reason we made nothing. ###@@@ should worry about bond direction! at least as a filter! If autobond_msg is true, mention the autobonding done or not done (depending on autobond option), in the returned message, if any atoms were near enough for autobonding to be done. This option is independent from the autobond option. [As of 050901 not all combos of these options have been tested. ###@@@] """ atype = self.atomtype if not atype.numbonds: whynot = "%s makes no bonds; can't attach one to an open bond" % atype.fullname_for_msg() return None, whynot if not atype.can_bond_to(singlet.singlet_neighbor(), singlet): #bruce 080502 new feature whynot = "%s bond to %r is not allowed" % (atype.fullname_for_msg(), singlet.singlet_neighbor()) # todo: return whynot from same routine return None, whynot spot = self.findSpot(singlet) pl = [(singlet, spot)] # will grow to a list of pairs (s, its spot) # bruce change 041215: always include this one in the list # (probably no effect, but gives later code less to worry about; # before this there was no guarantee singlet was in the list # (tho it probably always was), or even that the list was nonempty, # without analyzing the subrs in more detail than I'd like!) if autobond or autobond_msg: #bruce 050831 added this condition; 050901 added autobond_msg # extend pl to make additional bonds, by adding more (singlet, spot) pairs rl = [singlet.singlet_neighbor()] # list of real neighbors of singlets in pl [for bug 232 fix] ## mol = singlet.molecule cr = atype.rcovalent # bruce 041215: might as well fix the bug about searching for open bonds # in other mols too, since it's easy; search in this one first, and stop # when you find enough atoms to bond to. searchmols = list(singlet.molecule.part.molecules) #bruce 050510 revised this searchmols.remove(singlet.molecule) searchmols.insert(0, singlet.molecule) # max number of real bonds we can make (now this can be more than 4) maxpl = atype.numbonds for mol in searchmols: for s in mol.nearSinglets(spot, cr * 1.9): #bruce 041216 changed 1.5 to 1.9 above (it's a heuristic); # see email discussion (ninad, bruce, josh) #bruce 041203 quick fix for bug 232: # don't include two singlets on the same real atom! # (It doesn't matter which one we pick, in terms of which atom we'll # bond to, but it might affect the computation in the bonding # method of where to put the new atom, so ideally we'd do something # more principled than just using the findSpot output from the first # singlet in the list for a given real atom -- e.g. maybe we should # average the spots computed for all singlets of the same real atom. # But this is good enough for now.) #bruce 050510 adds: worse, the singlets are in an arb position... really we should just ask if # it makes sense to bond to each nearby *atom*, for the ones too near to comfortably *not* be bonded to. ###@@@ ###@@@ bruce 050221: bug 372: sometimes s is not a singlet. how can this be?? # guess: mol.singlets is not always invalidated when it should be. But even that theory # doesn't seem to fully explain the bug report... so let's find out a bit more, at least: try: real = s.singlet_neighbor() except: print_compact_traceback("bug 372 caught red-handed: ") print "bug 372-related data: mol = %r, mol.singlets = %r" % (mol, mol.singlets) continue if real not in rl and atype.can_bond_to(real, s, auto = True): # checking can_bond_to is bruce 080502 new feature pl += [(s, self.findSpot(s))] rl += [real] # after we're done with each mol (but not in the middle of any mol), # stop if we have as many open bonds as we can use if len(pl) >= maxpl: break del mol, s, real n = min(atype.numbonds, len(pl)) # number of real bonds to make (if this was computed above); always >= 1 pl = pl[0:n] # discard the extra pairs (old code did this too, implicitly) if autobond_msg and not autobond: pl = pl[0:1] # don't actually make the bonds we only wanted to tell the user we *might* have made # now pl tells which bonds to actually make, and (if autobond_msg) n tells how many we might have made. # bruce 041215 change: for n > 4, old code gave up now; # new code makes all n bonds for any n, tho it won't add singlets # for n > 4. (Both old and new code don't know how to add enough # singlets for n >= 3 and numbonds > 4. They might add some, tho.) # Note: _new_bonded_n uses len(pl) as its n. As of 050901 this might differ from the variable n. atm = self._new_bonded_n( pl) atm.make_enough_bondpoints() # (tries its best, but doesn't always make enough) desc = "%r (in %r)" % (atm, atm.molecule.name) #e what if caller renames atm.molecule?? if n > 1: #e really: if n > (number of singlets clicked on at once) if autobond: msg = " (%d bond(s) made)" % n else: msg = " (%d bond(s) NOT made, since autobond is off)" % (n-1) #bruce 050901 new feature from platform_dependent.PlatformDependent import fix_plurals msg = fix_plurals(msg) desc += msg return atm, desc