4949import math
5050import os .path
5151import re
52+ import itertools
5253
5354try :
5455 import cairocffi as cairo
6061import numpy as np
6162from rdkit .Chem import AllChem
6263
63- from rmgpy .molecule .molecule import Atom , Molecule
64+ from rmgpy .molecule .molecule import Atom , Molecule , Bond
65+ from rmgpy .molecule .pathfinder import find_shortest_path
6466from rmgpy .qm .molecule import Geometry
6567
6668
@@ -96,6 +98,12 @@ def create_new_surface(file_format, target=None, width=1024, height=768):
9698
9799################################################################################
98100
101+ class AdsorbateDrawingError (Exception ):
102+ """
103+ When something goes wrong trying to draw an adsorbate.
104+ """
105+ pass
106+
99107class MoleculeDrawer (object ):
100108 """
101109 This class provides functionality for drawing the skeletal formula of
@@ -207,7 +215,16 @@ def draw(self, molecule, file_format, target=None):
207215 # replace the bonds after generating coordinates. This avoids
208216 # bugs with RDKit
209217 old_bond_dictionary = self ._make_single_bonds ()
210- self ._generate_coordinates ()
218+ if molecule .contains_surface_site ():
219+ try :
220+ self ._connect_surface_sites ()
221+ self ._generate_coordinates ()
222+ self ._disconnect_surface_sites ()
223+ except AdsorbateDrawingError as e :
224+ self ._disconnect_surface_sites ()
225+ self ._generate_coordinates (fix_surface_sites = False )
226+ else :
227+ self ._generate_coordinates ()
211228 self ._replace_bonds (old_bond_dictionary )
212229
213230 # Generate labels to use
@@ -323,11 +340,13 @@ def _find_ring_groups(self):
323340 if not found :
324341 self .ringSystems .append ([cycle ])
325342
326- def _generate_coordinates (self ):
343+ def _generate_coordinates (self , fix_surface_sites = True ):
327344 """
328345 Generate the 2D coordinates to be used when drawing the current
329346 molecule. The function uses rdKits 2D coordinate generation.
330347 Updates the self.coordinates Array in place.
348+ If `fix_surface_sites` is True, then the surface sites are placed
349+ at the bottom of the molecule.
331350 """
332351 atoms = self .molecule .atoms
333352 natoms = len (atoms )
@@ -390,7 +409,6 @@ def _generate_coordinates(self):
390409 # If two atoms lie on top of each other, push them apart a bit
391410 # This is ugly, but at least the mess you end up with isn't as misleading
392411 # as leaving everything piled on top of each other at the origin
393- import itertools
394412 for atom1 , atom2 in itertools .combinations (backbone , 2 ):
395413 i1 , i2 = atoms .index (atom1 ), atoms .index (atom2 )
396414 if np .linalg .norm (coordinates [i1 , :] - coordinates [i2 , :]) < 0.5 :
@@ -402,7 +420,6 @@ def _generate_coordinates(self):
402420 # If two atoms lie on top of each other, push them apart a bit
403421 # This is ugly, but at least the mess you end up with isn't as misleading
404422 # as leaving everything piled on top of each other at the origin
405- import itertools
406423 for atom1 , atom2 in itertools .combinations (backbone , 2 ):
407424 i1 , i2 = atoms .index (atom1 ), atoms .index (atom2 )
408425 if np .linalg .norm (coordinates [i1 , :] - coordinates [i2 , :]) < 0.5 :
@@ -457,26 +474,59 @@ def _generate_coordinates(self):
457474 coordinates [:, 0 ] = temp [:, 1 ]
458475 coordinates [:, 1 ] = temp [:, 0 ]
459476
460- # For surface species, rotate them so the site is at the bottom.
461- if self .molecule .contains_surface_site ():
477+ # For surface species
478+ if fix_surface_sites and self .molecule .contains_surface_site ():
462479 if len (self .molecule .atoms ) == 1 :
463480 return coordinates
464- for site in self .molecule .atoms :
465- if site .is_surface_site ():
466- break
467- else :
468- raise Exception ("Can't find surface site" )
469- if site .bonds :
470- adsorbate = next (iter (site .bonds ))
471- vector0 = coordinates [atoms .index (site ), :] - coordinates [atoms .index (adsorbate ), :]
472- angle = math .atan2 (vector0 [0 ], vector0 [1 ]) - math .pi
481+ sites = [atom for atom in self .molecule .atoms if atom .is_surface_site ()]
482+ if len (sites ) == 1 :
483+ # rotate them so the site is at the bottom.
484+ site = sites [0 ]
485+ if site .bonds :
486+ adatom = next (iter (site .bonds ))
487+ vector0 = coordinates [atoms .index (site ), :] - coordinates [atoms .index (adatom ), :]
488+ angle = math .atan2 (vector0 [0 ], vector0 [1 ]) - math .pi
489+ rot = np .array ([[math .cos (angle ), math .sin (angle )], [- math .sin (angle ), math .cos (angle )]], float )
490+ self .coordinates = coordinates = np .dot (coordinates , rot )
491+ else :
492+ # van der Waals
493+ index = atoms .index (site )
494+ coordinates [index , 1 ] = min (coordinates [:, 1 ]) - 0.8 # just move the site down a bit
495+ coordinates [index , 0 ] = coordinates [:, 0 ].mean () # and center it
496+ elif len (sites ) <= 4 :
497+ # Rotate so the line of best fit through the adatoms is horizontal.
498+ # find atoms bonded to sites
499+ adatoms = [next (iter (site .bonds )) for site in sites ]
500+ adatom_indices = [atoms .index (a ) for a in adatoms ]
501+ # find the best fit line through the bonded atoms
502+ x = coordinates [adatom_indices , 0 ]
503+ y = coordinates [adatom_indices , 1 ]
504+ A = np .vstack ([x , np .ones (len (x ))]).T
505+ m , c = np .linalg .lstsq (A , y , rcond = None )[0 ]
506+ # rotate so the line is horizontal
507+ angle = - math .atan (m )
473508 rot = np .array ([[math .cos (angle ), math .sin (angle )], [- math .sin (angle ), math .cos (angle )]], float )
474509 self .coordinates = coordinates = np .dot (coordinates , rot )
510+ # if the line is above the middle, flip it
511+ not_site_indices = [atoms .index (a ) for a in atoms if not a .is_surface_site ()]
512+ if coordinates [adatom_indices , 1 ].mean () > coordinates [not_site_indices , 1 ].mean ():
513+ coordinates [:, 1 ] *= - 1
514+ x = coordinates [adatom_indices , 0 ]
515+ y = coordinates [adatom_indices , 1 ]
516+ site_y_pos = min (min (y ) - 0.8 , min (coordinates [not_site_indices , 1 ]) - 0.5 )
517+ if max (y ) - site_y_pos > 1.5 :
518+ raise AdsorbateDrawingError ("Adsorbate bond too long" )
519+ for x1 , x2 in itertools .combinations (x , 2 ):
520+ if abs (x1 - x2 ) < 0.2 :
521+ raise AdsorbateDrawingError ("Sites overlapping" )
522+ for site , x_pos in zip (sites , x ):
523+ index = atoms .index (site )
524+ coordinates [index , 1 ] = site_y_pos
525+ coordinates [index , 0 ] = x_pos
526+
475527 else :
476- # van der waals
477- index = atoms .index (site )
478- coordinates [index , 1 ] = min (coordinates [:, 1 ]) - 0.8 # just move the site down a bit
479- coordinates [index , 0 ] = coordinates [:, 0 ].mean () # and center it
528+ # more than 4 surface sites? leave them alone
529+ pass
480530
481531 def _find_cyclic_backbone (self ):
482532 """
@@ -854,7 +904,7 @@ def _generate_functional_group_coordinates(self, atom0, atom1):
854904 # Check to see if atom1 is in any cycles in the molecule
855905 ring_system = None
856906 for ring_sys in self .ringSystems :
857- if any ([ atom1 in ring for ring in ring_sys ] ):
907+ if any (atom1 in ring for ring in ring_sys ):
858908 ring_system = ring_sys
859909
860910 if ring_system is not None :
@@ -1624,6 +1674,40 @@ def _replace_bonds(self, bond_order_dictionary):
16241674 for bond , order in bond_order_dictionary .items ():
16251675 bond .set_order_num (order )
16261676
1677+ def _connect_surface_sites (self ):
1678+ """
1679+ Creates single bonds between atoms that are surface sites.
1680+ This is to help make multidentate adsorbates look better.
1681+ """
1682+ sites = [a for a in self .molecule .atoms if a .is_surface_site ()]
1683+ if len (sites ) > 4 :
1684+ return
1685+ for site1 in sites :
1686+ other_sites = [a for a in sites if a != site1 ]
1687+ if not other_sites : break
1688+ # connect to the nearest site
1689+ site2 = min (other_sites , key = lambda a : len (find_shortest_path (site1 , a )))
1690+ if len (find_shortest_path (site1 , site2 )) > 2 and len (sites ) > 3 :
1691+ # if there are more than 3 sites, don't connect sites that aren't neighbors
1692+ continue
1693+
1694+ bond = site1 .bonds .get (site2 )
1695+ if bond is None :
1696+ bond = Bond (site1 , site2 , 1 )
1697+ site1 .bonds [site2 ] = bond
1698+ site2 .bonds [site1 ] = bond
1699+
1700+ def _disconnect_surface_sites (self ):
1701+ """
1702+ Removes all bonds between atoms that are surface sites.
1703+ """
1704+ for site1 in self .molecule .atoms :
1705+ if site1 .is_surface_site ():
1706+ for site2 in list (site1 .bonds .keys ()): # make a list copy so we can delete from the dict
1707+ if site2 .is_surface_site ():
1708+ del site1 .bonds [site2 ]
1709+ del site2 .bonds [site1 ]
1710+
16271711
16281712################################################################################
16291713
0 commit comments