import math, string import chemfig_mappings as cfm from common import debug # some atoms should carry their hydrogens to the left, rather than # to the right. This is applied to solitary atoms, but not to bonded # functional groups that contain those elements. hydrogen_lefties = "O S Se Te F Cl Br I At".split() # I hope these are all ... class Atom(object): ''' wrapper around toolkit atom object, augmented with coordinates helper class for molecule.Molecule ''' explicit_characters = set(string.ascii_uppercase + string.digits) quadrant_turf = 80 # 80 degrees have to remain free on either side quadrants = [ # quadrants for hydrogen placement [0, 0, 'east'], [1, 180, 'west'], [2, 270, 'south'], [3, 90, 'north'] ] charge_positions = [ # angles for placement of detached charges [0, 15, 'top_right'], [1,165, 'top_left'], [2, 90, 'top_center'], [3,270, 'bottom_center'], [4,345, 'bottom_right'], [5,195, 'bottom_left'] ] charge_turf = 50 # reserved angle for charges - needs to be big enough for 2+ def __init__(self, options, idx, x, y, element, hydrogens, charge, radical, neighbors): self.options = options self.idx = idx self.x = x self.y = y self.element = element self.hydrogens = hydrogens self.charge = charge self.radical = radical self.neighbors = neighbors # the indexes only # angles of all attached bonds - to be populated later self.bond_angles = [] # self.explicit = False # flag for explicitly printed atoms - set later marker = self.options.get('markers', None) if marker is not None: self.marker = "%s%s" % (marker, self.idx + 1) else: self.marker = "" def _score_angle(self, a, b, turf): ''' helper. calculates absolute angle between a and b. 0 <= angle <= 180 then compares to turf angle and returns a score > 0 if angle falls within turf. ''' diff = (a-b) % 360 angle = min(diff,360-diff) return (max(0, turf - angle)) ** 2 def _score_angles(self, choices, turf): ''' backend for score_angles ''' aux = [] for priority, choice_angle, name in choices: score = 0 for bond_angle in self.bond_angles: score += self._score_angle(choice_angle, bond_angle, turf) aux.append((score, priority, name)) aux.sort() #if self.element == 'Cl': #debug(aux) named = [a[-1] for a in aux] return named def score_angles(self): ''' determine which positions We use one score for the placement of hydrogens w/ or w/o charge, and a separate one for the placement of charges only. Atoms: precedence east, west, south, north tolerated impingement 10 degrees Charges: precedence top right, top left, top straight, bottom straight, others ''' if len(self.bond_angles) > 0: # this atom is bonded quadrants = self._score_angles(self.quadrants, self.quadrant_turf) self.first_quadrant = quadrants[0] self.second_quadrant = quadrants[1] # 2nd choice may be used for radical electrons else: # this atom is solitary if self.element in hydrogen_lefties: self.first_quadrant = 'west' self.second_quadrant = 'east' else: self.first_quadrant = 'east' self.second_quadrant = 'west' self.charge_angle = self._score_angles(self.charge_positions, self.charge_turf)[0] def render_phantom(self): ''' render a bond that closes a ring or loop, or for late-rendered cross bonds. The target atom is represented by a phantom. This relies on .render() having been called earlier, which it will be - atoms always precede their phantoms during molecule tree traversal. ''' atom_code = self.phantom comment_code = cfm.format_closure_comment( self.options, self.idx + 1, self.element, self.hydrogens, self.charge ) return atom_code, comment_code def render(self): ''' render the atom and a comment ''' atom_code, self.string_pos, \ self.phantom, self.phantom_pos = cfm.format_atom( self.options, self.idx + 1, self.element, self.hydrogens, self.charge, self.radical, self.first_quadrant, self.second_quadrant, self.charge_angle ) comment_code = cfm.format_atom_comment( self.options, self.idx + 1, self.element, self.hydrogens, self.charge ) marker_code = cfm.format_marker(self.marker) if marker_code: comment_code = " " # force an empty comment, needed after markers self.explicit = bool(self.explicit_characters & set(atom_code)) # debug(self.idx, atom_code, self.explicit) return marker_code + atom_code, comment_code