def normalize_bond_length(self, bond_length=30): """make the average bond-length be bond_length by scaling the structure up""" if not self.edges or len(self.vertices) < 2: return False minx = None maxx = None miny = None maxy = None # atoms for v in self.vertices: if not maxx or v.x > maxx: maxx = v.x if not minx or v.x < minx: minx = v.x if not miny or v.y < miny: miny = v.y if not maxy or v.y > maxy: maxy = v.y scale = bond_length / self.get_mean_bond_length() movex = (maxx + minx) / 2 movey = (maxy + miny) / 2 trans = transform3d.transform3d() trans.set_move(-movex, -movey, 0) trans.set_scaling(scale) trans.set_move(movex, movey, 0) for v in self.atoms: v.x, v.y, v.z = trans.transform_xyz(v.x, v.y, v.z) return True
def normalize_bond_length( self, bond_length=30): """make the average bond-length be bond_length by scaling the structure up""" if not self.edges or len( self.vertices) < 2: return False minx = None maxx = None miny = None maxy = None # atoms for v in self.vertices: if not maxx or v.x > maxx: maxx = v.x if not minx or v.x < minx: minx = v.x if not miny or v.y < miny: miny = v.y if not maxy or v.y > maxy: maxy = v.y scale = bond_length / self.get_mean_bond_length() movex = (maxx+minx)/2 movey = (maxy+miny)/2 trans = transform3d.transform3d() trans.set_move( -movex, -movey, 0) trans.set_scaling( scale) trans.set_move( movex, movey, 0) for v in self.atoms: v.x, v.y, v.z = trans.transform_xyz( v.x, v.y, v.z) return True
def create_transformation_to_rotate_around_particular_axis( line_start, line_end, theta): """ taken from http://inside.mines.edu/~gmurray/ArbitraryAxisRotation/ArbitraryAxisRotation.html """ a, b, c = line_start u, v, w = line_end u -= a v -= b w -= c u2 = u * u v2 = v * v w2 = w * w cosT = cos(theta) sinT = sin(theta) l2 = u2 + v2 + w2 l = sqrt(l2) if (l2 < 0.000000001): raise ValueError("RotationMatrix: direction vector too short!") m11 = (u2 + (v2 + w2) * cosT) / l2 m12 = (u * v * (1 - cosT) - w * l * sinT) / l2 m13 = (u * w * (1 - cosT) + v * l * sinT) / l2 m14 = (a * (v2 + w2) - u * (b * v + c * w) + (u * (b * v + c * w) - a * (v2 + w2)) * cosT + (b * w - c * v) * l * sinT) / l2 m21 = (u * v * (1 - cosT) + w * l * sinT) / l2 m22 = (v2 + (u2 + w2) * cosT) / l2 m23 = (v * w * (1 - cosT) - u * l * sinT) / l2 m24 = (b * (u2 + w2) - v * (a * u + c * w) + (v * (a * u + c * w) - b * (u2 + w2)) * cosT + (c * u - a * w) * l * sinT) / l2 m31 = (u * w * (1 - cosT) - v * l * sinT) / l2 m32 = (v * w * (1 - cosT) + u * l * sinT) / l2 m33 = (w2 + (u2 + v2) * cosT) / l2 m34 = (c * (u2 + v2) - w * (a * u + b * v) + (w * (a * u + b * v) - c * (u2 + v2)) * cosT + (a * v - b * u) * l * sinT) / l2 from transform3d import transform3d t = transform3d([[m11, m12, m13, m14], [m21, m22, m23, m24], [m31, m32, m33, m34], [0, 0, 0, 1]]) return t
def create_transformation_to_coincide_point_with_z_axis(mov, point): """takes 3d coordinates 'point' (vector mov->point) and returns 3d transform object (transform3d.transform3d) that performs rotation to get 'point' onto z axis (x,y)=(0,0) with positive 'z'. NOTE: this is probably far from efficient, but it works """ from transform3d import transform3d t = transform3d() a, b, c = mov t.set_move(-a, -b, -c) x, y, z = t.transform_xyz(*point) t.set_rotation_y(atan2(x, z)) x, y, z = t.transform_xyz(*point) t.set_rotation_x(-atan2(y, sqrt(x**2 + z**2))) x, y, z = t.transform_xyz(*point) if z < 0: t.set_rotation_x(pi) #t.set_move( *mov) return t
def create_transformation_to_coincide_point_with_z_axis( mov, point): """takes 3d coordinates 'point' (vector mov->point) and returns 3d transform object (transform3d.transform3d) that performs rotation to get 'point' onto z axis (x,y)=(0,0) with positive 'z'. NOTE: this is probably far from efficient, but it works """ from transform3d import transform3d t = transform3d() a,b,c = mov t.set_move( -a, -b, -c) x,y,z = t.transform_xyz( *point) t.set_rotation_y( atan2( x, z)) x,y,z = t.transform_xyz( *point) t.set_rotation_x( -atan2( y, sqrt(x**2+z**2))) x,y,z = t.transform_xyz( *point) if z < 0: t.set_rotation_x( pi) #t.set_move( *mov) return t
def create_transformation_to_rotate_around_particular_axis( line_start, line_end, theta): """ taken from http://inside.mines.edu/~gmurray/ArbitraryAxisRotation/ArbitraryAxisRotation.html """ a,b,c = line_start u,v,w = line_end u -= a v -= b w -= c u2 = u*u; v2 = v*v; w2 = w*w; cosT = cos(theta); sinT = sin(theta); l2 = u2 + v2 + w2; l = sqrt(l2); if (l2 < 0.000000001): raise ValueError("RotationMatrix: direction vector too short!") m11 = (u2 + (v2 + w2) * cosT)/l2; m12 = (u*v * (1 - cosT) - w*l*sinT)/l2; m13 = (u*w * (1 - cosT) + v*l*sinT)/l2; m14 = (a*(v2 + w2) - u*(b*v + c*w) + (u*(b*v + c*w) - a*(v2 + w2))*cosT + (b*w - c*v)*l*sinT)/l2; m21 = (u*v * (1 - cosT) + w*l*sinT)/l2; m22 = (v2 + (u2 + w2) * cosT)/l2; m23 = (v*w * (1 - cosT) - u*l*sinT)/l2; m24 = (b*(u2 + w2) - v*(a*u + c*w) + (v*(a*u + c*w) - b*(u2 + w2))*cosT + (c*u - a*w)*l*sinT)/l2; m31 = (u*w * (1 - cosT) - v*l*sinT)/l2; m32 = (v*w * (1 - cosT) + u*l*sinT)/l2; m33 = (w2 + (u2 + v2) * cosT)/l2; m34 = (c*(u2 + v2) - w*(a*u + b*v) + (w*(a*u + b*v) - c*(u2 + v2))*cosT + (a*v - b*u)*l*sinT)/l2; from transform3d import transform3d t = transform3d( [[m11,m12,m13,m14],[m21,m22,m23,m24],[m31,m32,m33,m34],[0,0,0,1]]) return t
def _draw_edge( self, e): def draw_plain_or_colored_line( _start, _end, second=False): """second means if this is not the main line, drawing might be different""" if not has_shown_vertex or not self.color_bonds: if not second: self._draw_line( _start, _end, line_width=self.line_width, capstyle=cairo.LINE_CAP_ROUND) else: self._draw_line( _start, _end, line_width=self.line_width, capstyle=cairo.LINE_CAP_BUTT) else: self._draw_colored_line( _start, _end, line_width=self.line_width, start_color=color1, end_color=color2) def draw_plain_or_colored_wedge( _start, _end): x1, y1 = _start x2, y2 = _end x, y, x0, y0 = geometry.find_parallel( x1, y1, x2, y2, self.wedge_width/2.0) xa, ya, xb, yb = geometry.find_parallel( x1, y1, x2, y2, self.line_width/2.0) # no coloring now if not has_shown_vertex or not self.color_bonds: self._create_cairo_path( [(xa, ya), (x0, y0), (2*x2-x0, 2*y2-y0), (2*x1-xa, 2*y1-ya)], closed=True) self.context.set_source_rgb( 0,0,0) self.context.fill() else: # ratio 0.4 looks better than 0.5 because the area difference # is percieved more than length difference ratio = 0.4 xm1 = ratio*xa + (1-ratio)*x0 ym1 = ratio*ya + (1-ratio)*y0 xm2 = (1-ratio)*(2*x2-x0) + ratio*(2*x1-xa) ym2 = (1-ratio)*(2*y2-y0) + ratio*(2*y1-ya) self.context.set_source_rgb( *color1) self._create_cairo_path( [(xa,ya), (xm1,ym1), (xm2,ym2), (2*x1-xa, 2*y1-ya)], closed=True) self.context.fill() self.context.set_source_rgb( *color2) self._create_cairo_path( [(xm1,ym1), (x0, y0), (2*x2-x0, 2*y2-y0), (xm2,ym2)], closed=True) self.context.fill() def draw_plain_or_colored_hatch( _start, _end): x1, y1 = _start x2, y2 = _end # no coloring now x, y, x0, y0 = geometry.find_parallel( x1, y1, x2, y2, self.wedge_width/2.0) xa, ya, xb, yb = geometry.find_parallel( x1, y1, x2, y2, self.line_width/2.0) d = math.sqrt( (x1-x2)**2 + (y1-y2)**2) # length of the bond if d == 0: return # to prevent division by zero dx1 = (x0 - xa)/d dy1 = (y0 - ya)/d dx2 = (2*x2 -x0 -2*x1 +xa)/d dy2 = (2*y2 -y0 -2*y1 +ya)/d # we have to decide if the first line should be at the position of the first atom draw_start = 1 # is index not boolean if not v1 in self._vertex_to_bbox and v1.occupied_valency > 1: draw_start = 1 draw_end = 1 # is added to index not boolean if not v2 in self._vertex_to_bbox and v2.occupied_valency > 1: draw_end = 0 # adjust the step length step_size = 2*(self.line_width) ns = round( d / step_size) or 1 step_size = d / ns # now we finally draw self.context.set_line_cap( cairo.LINE_CAP_BUTT) self.context.set_source_rgb( *color1) middle = 0.5 * (draw_start + int( round( d/ step_size)) + draw_end - 2) for i in range( draw_start, int( round( d/ step_size)) +draw_end): coords = [xa+dx1*i*step_size, ya+dy1*i*step_size, 2*x1-xa+dx2*i*step_size, 2*y1-ya+dy2*i*step_size] if coords[0] == coords[2] and coords[1] == coords[3]: if (dx1+dx2) > (dy1+dy2): coords[0] += 1 else: coords[1] += 1 self._create_cairo_path( [coords[:2],coords[2:]]) if i >= middle: self.context.stroke() self.context.set_source_rgb( *color2) self.context.stroke() # code itself # at first detect the need to make 3D adjustments self._transform = transform3d.transform3d() self._invtransform = transform3d.transform3d() transform = None if e.order > 1: atom1,atom2 = e.vertices for n in atom1.neighbors + atom2.neighbors: # e.atom1 and e.atom2 are in this list as well if n.z != 0: # engage 3d transform prior to detection of where to draw transform = self._get_3dtransform_for_drawing( e) #transform = None break if transform: for n in atom1.neighbors + atom2.neighbors: n.coords = transform.transform_xyz( *n.coords) self._transform = transform self._invtransform = transform.get_inverse() # // end of 3D adjustments # now the code itself coords = self._where_to_draw_from_and_to( e) if coords: start = coords[:2] end = coords[2:] v1, v2 = e.vertices if self.color_bonds: color1 = self.atom_colors.get( v1.symbol, (0,0,0)) color2 = self.atom_colors.get( v2.symbol, (0,0,0)) else: color1 = color2 = (0,0,0) has_shown_vertex = bool( [1 for _v in e.vertices if _v in self._vertex_to_bbox]) if e.order == 1: if e.type == 'w': draw_plain_or_colored_wedge( start, end) elif e.type == 'h': draw_plain_or_colored_hatch( start, end) else: draw_plain_or_colored_line( start, end) if e.order == 2: side = 0 # find how to center the bonds # rings have higher priority in setting the positioning in_ring = False for ring in self.molecule.get_smallest_independent_cycles_dangerous_and_cached(): double_bonds = len( [b for b in self.molecule.vertex_subgraph_to_edge_subgraph(ring) if b.order == 2]) if v1 in ring and v2 in ring: in_ring = True side += double_bonds * reduce( operator.add, [geometry.on_which_side_is_point( start+end, (a.x,a.y)) for a in ring if a!=v1 and a!=v2]) # if rings did not decide, use the other neigbors if not side: for v in v1.neighbors + v2.neighbors: if v != v1 and v!= v2: side += geometry.on_which_side_is_point( start+end, (v.x, v.y)) # if neighbors did not decide either if not side and (in_ring or not has_shown_vertex): if in_ring: # we don't want centered bonds inside rings side = 1 # select arbitrary value else: # bond between two unshown atoms - we want to center them only in some cases if len( v1.neighbors) == 1 and len( v2.neighbors) == 1: # both atoms have only one neighbor side = 0 elif len( v1.neighbors) < 3 and len( v2.neighbors) < 3: # try to figure out which side is more towards the center of the molecule side = reduce( operator.add, [geometry.on_which_side_is_point( start+end, (a.x,a.y)) for a in self.molecule.vertices if a!=v1 and a!=v2], 0) if not side: side = 1 # we choose arbitrary value, we don't want centering if side: draw_plain_or_colored_line( start, end) x1, y1, x2, y2 = geometry.find_parallel( start[0], start[1], end[0], end[1], self.bond_width*misc.signum( side)) # shorten the second line length = geometry.point_distance( x1,y1,x2,y2) if v2 not in self._vertex_to_bbox: x2, y2 = geometry.elongate_line( x1, y1, x2, y2, -self.bond_second_line_shortening*length) if v1 not in self._vertex_to_bbox: x1, y1 = geometry.elongate_line( x2, y2, x1, y1, -self.bond_second_line_shortening*length) draw_plain_or_colored_line( (x1, y1), (x2, y2), second=True) else: for i in (1,-1): x1, y1, x2, y2 = geometry.find_parallel( start[0], start[1], end[0], end[1], i*self.bond_width*0.5) draw_plain_or_colored_line( (x1, y1), (x2, y2)) elif e.order == 3: self._draw_line( start, end, line_width=self.line_width) for i in (1,-1): x1, y1, x2, y2 = geometry.find_parallel( start[0], start[1], end[0], end[1], i*self.bond_width*0.7) draw_plain_or_colored_line( (x1, y1), (x2, y2), second=True) if transform: # if transform was used, we need to transform back for n in atom1.neighbors + atom2.neighbors: n.coords = self._invtransform.transform_xyz( *n.coords)
def _draw_edge(self, e): def draw_plain_or_colored_line(_start, _end, second=False): """second means if this is not the main line, drawing might be different""" if not has_shown_vertex or not self.color_bonds: if not second: self._draw_line(_start, _end, line_width=self.line_width, capstyle=cairo.LINE_CAP_ROUND) else: self._draw_line(_start, _end, line_width=self.line_width, capstyle=cairo.LINE_CAP_BUTT) else: self._draw_colored_line(_start, _end, line_width=self.line_width, start_color=color1, end_color=color2) def draw_plain_or_colored_wedge(_start, _end): x1, y1 = _start x2, y2 = _end x, y, x0, y0 = geometry.find_parallel(x1, y1, x2, y2, self.wedge_width / 2.0) xa, ya, xb, yb = geometry.find_parallel(x1, y1, x2, y2, self.line_width / 2.0) # no coloring now if not has_shown_vertex or not self.color_bonds: self._create_cairo_path([(xa, ya), (x0, y0), (2 * x2 - x0, 2 * y2 - y0), (2 * x1 - xa, 2 * y1 - ya)], closed=True) self.context.set_source_rgb(0, 0, 0) self.context.fill() else: # ratio 0.4 looks better than 0.5 because the area difference # is percieved more than length difference ratio = 0.4 xm1 = ratio * xa + (1 - ratio) * x0 ym1 = ratio * ya + (1 - ratio) * y0 xm2 = (1 - ratio) * (2 * x2 - x0) + ratio * (2 * x1 - xa) ym2 = (1 - ratio) * (2 * y2 - y0) + ratio * (2 * y1 - ya) self.context.set_source_rgb(*color1) self._create_cairo_path([(xa, ya), (xm1, ym1), (xm2, ym2), (2 * x1 - xa, 2 * y1 - ya)], closed=True) self.context.fill() self.context.set_source_rgb(*color2) self._create_cairo_path([(xm1, ym1), (x0, y0), (2 * x2 - x0, 2 * y2 - y0), (xm2, ym2)], closed=True) self.context.fill() def draw_plain_or_colored_hatch(_start, _end): x1, y1 = _start x2, y2 = _end # no coloring now x, y, x0, y0 = geometry.find_parallel(x1, y1, x2, y2, self.wedge_width / 2.0) xa, ya, xb, yb = geometry.find_parallel(x1, y1, x2, y2, self.line_width / 2.0) d = math.sqrt((x1 - x2)**2 + (y1 - y2)**2) # length of the bond if d == 0: return # to prevent division by zero dx1 = (x0 - xa) / d dy1 = (y0 - ya) / d dx2 = (2 * x2 - x0 - 2 * x1 + xa) / d dy2 = (2 * y2 - y0 - 2 * y1 + ya) / d # we have to decide if the first line should be at the position of the first atom draw_start = 1 # is index not boolean if not v1 in self._vertex_to_bbox and v1.occupied_valency > 1: draw_start = 1 draw_end = 1 # is added to index not boolean if not v2 in self._vertex_to_bbox and v2.occupied_valency > 1: draw_end = 0 # adjust the step length step_size = 2 * (self.line_width) ns = round(d / step_size) or 1 step_size = d / ns # now we finally draw self.context.set_line_cap(cairo.LINE_CAP_BUTT) self.context.set_source_rgb(*color1) middle = 0.5 * (draw_start + int(round(d / step_size)) + draw_end - 2) for i in range(draw_start, int(round(d / step_size)) + draw_end): coords = [ xa + dx1 * i * step_size, ya + dy1 * i * step_size, 2 * x1 - xa + dx2 * i * step_size, 2 * y1 - ya + dy2 * i * step_size ] if coords[0] == coords[2] and coords[1] == coords[3]: if (dx1 + dx2) > (dy1 + dy2): coords[0] += 1 else: coords[1] += 1 self._create_cairo_path([coords[:2], coords[2:]]) if i >= middle: self.context.stroke() self.context.set_source_rgb(*color2) self.context.stroke() # code itself # at first detect the need to make 3D adjustments self._transform = transform3d.transform3d() self._invtransform = transform3d.transform3d() transform = None if e.order > 1: atom1, atom2 = e.vertices for n in atom1.neighbors + atom2.neighbors: # e.atom1 and e.atom2 are in this list as well if n.z != 0: # engage 3d transform prior to detection of where to draw transform = self._get_3dtransform_for_drawing(e) #transform = None break if transform: for n in atom1.neighbors + atom2.neighbors: n.coords = transform.transform_xyz(*n.coords) self._transform = transform self._invtransform = transform.get_inverse() # // end of 3D adjustments # now the code itself coords = self._where_to_draw_from_and_to(e) if coords: start = coords[:2] end = coords[2:] v1, v2 = e.vertices if self.color_bonds: color1 = self.atom_colors.get(v1.symbol, (0, 0, 0)) color2 = self.atom_colors.get(v2.symbol, (0, 0, 0)) else: color1 = color2 = (0, 0, 0) has_shown_vertex = bool( [1 for _v in e.vertices if _v in self._vertex_to_bbox]) if e.order == 1: if e.type == 'w': draw_plain_or_colored_wedge(start, end) elif e.type == 'h': draw_plain_or_colored_hatch(start, end) else: draw_plain_or_colored_line(start, end) if e.order == 2: side = 0 # find how to center the bonds # rings have higher priority in setting the positioning in_ring = False for ring in self.molecule.get_smallest_independent_cycles_dangerous_and_cached( ): double_bonds = len([ b for b in self.molecule.vertex_subgraph_to_edge_subgraph(ring) if b.order == 2 ]) if v1 in ring and v2 in ring: in_ring = True side += double_bonds * reduce(operator.add, [ geometry.on_which_side_is_point( start + end, (a.x, a.y)) for a in ring if a != v1 and a != v2 ]) # if rings did not decide, use the other neigbors if not side: for v in v1.neighbors + v2.neighbors: if v != v1 and v != v2: side += geometry.on_which_side_is_point( start + end, (v.x, v.y)) # if neighbors did not decide either if not side and (in_ring or not has_shown_vertex): if in_ring: # we don't want centered bonds inside rings side = 1 # select arbitrary value else: # bond between two unshown atoms - we want to center them only in some cases if len(v1.neighbors) == 1 and len(v2.neighbors) == 1: # both atoms have only one neighbor side = 0 elif len(v1.neighbors) < 3 and len(v2.neighbors) < 3: # try to figure out which side is more towards the center of the molecule side = reduce(operator.add, [ geometry.on_which_side_is_point( start + end, (a.x, a.y)) for a in self.molecule.vertices if a != v1 and a != v2 ], 0) if not side: side = 1 # we choose arbitrary value, we don't want centering if side: draw_plain_or_colored_line(start, end) x1, y1, x2, y2 = geometry.find_parallel( start[0], start[1], end[0], end[1], self.bond_width * misc.signum(side)) # shorten the second line length = geometry.point_distance(x1, y1, x2, y2) if v2 not in self._vertex_to_bbox: x2, y2 = geometry.elongate_line( x1, y1, x2, y2, -self.bond_second_line_shortening * length) if v1 not in self._vertex_to_bbox: x1, y1 = geometry.elongate_line( x2, y2, x1, y1, -self.bond_second_line_shortening * length) draw_plain_or_colored_line((x1, y1), (x2, y2), second=True) else: for i in (1, -1): x1, y1, x2, y2 = geometry.find_parallel( start[0], start[1], end[0], end[1], i * self.bond_width * 0.5) draw_plain_or_colored_line((x1, y1), (x2, y2)) elif e.order == 3: self._draw_line(start, end, line_width=self.line_width) for i in (1, -1): x1, y1, x2, y2 = geometry.find_parallel( start[0], start[1], end[0], end[1], i * self.bond_width * 0.7) draw_plain_or_colored_line((x1, y1), (x2, y2), second=True) if transform: # if transform was used, we need to transform back for n in atom1.neighbors + atom2.neighbors: n.coords = self._invtransform.transform_xyz(*n.coords)