Skip to content

Commit 6e6490a

Browse files
authored
Merge pull request #2838 from kirkbadger18/add_charged_bidentate_draw_test
Fix drawing errors for charged species by moving to `rdkit` geometry generation
2 parents 019362c + de4fa63 commit 6e6490a

File tree

2 files changed

+134
-40
lines changed

2 files changed

+134
-40
lines changed

rmgpy/molecule/draw.py

Lines changed: 35 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ def clear(self):
155155
self.surface = None
156156
self.cr = None
157157

158-
def draw(self, molecule, file_format, target=None):
158+
def draw(self, molecule, file_format, target=None, use_rdkit=True):
159159
"""
160160
Draw the given `molecule` using the given image `file_format` - pdf, svg, ps, or
161161
png. If `path` is given, the drawing is saved to that location on disk. The
@@ -165,6 +165,9 @@ def draw(self, molecule, file_format, target=None):
165165
This function returns the Cairo surface and context used to create the
166166
drawing, as well as a bounding box for the molecule being drawn as the
167167
tuple (`left`, `top`, `width`, `height`).
168+
169+
If `use_rdkit` is True, then the RDKit 2D coordinate generation is used to generate the coordinates.
170+
If `use_rdkit` is False, then the molecule is drawn using our (deprecated) original algorithm.
168171
"""
169172

170173
# The Cairo 2D graphics library (and its Python wrapper) is required for
@@ -219,13 +222,13 @@ def draw(self, molecule, file_format, target=None):
219222
if molecule.contains_surface_site():
220223
try:
221224
self._connect_surface_sites()
222-
self._generate_coordinates()
225+
self._generate_coordinates(use_rdkit=use_rdkit)
223226
self._disconnect_surface_sites()
224227
except AdsorbateDrawingError as e:
225228
self._disconnect_surface_sites()
226-
self._generate_coordinates(fix_surface_sites=False)
229+
self._generate_coordinates(fix_surface_sites=False, use_rdkit=use_rdkit)
227230
else:
228-
self._generate_coordinates()
231+
self._generate_coordinates(use_rdkit=use_rdkit)
229232
self._replace_bonds(old_bond_dictionary)
230233

231234
# Generate labels to use
@@ -341,7 +344,7 @@ def _find_ring_groups(self):
341344
if not found:
342345
self.ringSystems.append([cycle])
343346

344-
def _generate_coordinates(self, fix_surface_sites=True):
347+
def _generate_coordinates(self, fix_surface_sites=True, use_rdkit=True):
345348
"""
346349
Generate the 2D coordinates to be used when drawing the current
347350
molecule. The function uses rdKits 2D coordinate generation.
@@ -372,15 +375,34 @@ def _generate_coordinates(self, fix_surface_sites=True):
372375
self.coordinates[1, :] = [0.5, 0.0]
373376
return self.coordinates
374377

375-
# Decide whether we can use RDKit or have to generate coordinates ourselves
376-
for atom in self.molecule.atoms:
377-
if atom.charge != 0:
378-
use_rdkit = False
379-
break
380-
else: # didn't break
381-
use_rdkit = True
378+
if use_rdkit == True:
379+
# Use RDKit 2D coordinate generation:
380+
381+
# Generate the RDkit molecule from the RDkit molecule, use geometry
382+
# in order to match the atoms in the rdmol with the atoms in the
383+
# RMG molecule (which is required to extract coordinates).
384+
self.geometry = Geometry(None, None, self.molecule, None)
385+
386+
rdmol, rd_atom_idx = self.geometry.rd_build()
387+
AllChem.Compute2DCoords(rdmol)
388+
389+
# Extract the coordinates from each atom.
390+
for atom in atoms:
391+
index = rd_atom_idx[atom]
392+
point = rdmol.GetConformer(0).GetAtomPosition(index)
393+
coordinates[index, :] = [point.x * 0.6, point.y * 0.6]
394+
395+
# RDKit generates some molecules more vertically than horizontally,
396+
# Especially linear ones. This will reflect any molecule taller than
397+
# it is wide across the line y=x
398+
ranges = np.ptp(coordinates, axis=0)
399+
if ranges[1] > ranges[0]:
400+
temp = np.copy(coordinates)
401+
coordinates[:, 0] = temp[:, 1]
402+
coordinates[:, 1] = temp[:, 0]
382403

383-
if not use_rdkit:
404+
else:
405+
logging.warning("Using deprecated molecule drawing algorithm; undesired behavior may occur. Consider using use_rdkit=True.")
384406
if len(self.cycles) > 0:
385407
# Cyclic molecule
386408
backbone = self._find_cyclic_backbone()
@@ -438,32 +460,6 @@ def _generate_coordinates(self, fix_surface_sites=True):
438460
# minimize likelihood of overlap
439461
self._generate_neighbor_coordinates(backbone)
440462

441-
else:
442-
# Use RDKit 2D coordinate generation:
443-
444-
# Generate the RDkit molecule from the RDkit molecule, use geometry
445-
# in order to match the atoms in the rdmol with the atoms in the
446-
# RMG molecule (which is required to extract coordinates).
447-
self.geometry = Geometry(None, None, self.molecule, None)
448-
449-
rdmol, rd_atom_idx = self.geometry.rd_build()
450-
AllChem.Compute2DCoords(rdmol)
451-
452-
# Extract the coordinates from each atom.
453-
for atom in atoms:
454-
index = rd_atom_idx[atom]
455-
point = rdmol.GetConformer(0).GetAtomPosition(index)
456-
coordinates[index, :] = [point.x * 0.6, point.y * 0.6]
457-
458-
# RDKit generates some molecules more vertically than horizontally,
459-
# Especially linear ones. This will reflect any molecule taller than
460-
# it is wide across the line y=x
461-
ranges = np.ptp(coordinates, axis=0)
462-
if ranges[1] > ranges[0]:
463-
temp = np.copy(coordinates)
464-
coordinates[:, 0] = temp[:, 1]
465-
coordinates[:, 1] = temp[:, 0]
466-
467463
# For surface species
468464
if fix_surface_sites and self.molecule.contains_surface_site():
469465
if len(self.molecule.atoms) == 1:

test/rmgpy/molecule/drawTest.py

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,4 +297,102 @@ def test_draw_bidentate_adsorbate(self):
297297
surface, _cr, (_xoff, _yoff, width, height) = self.drawer.draw(molecule, file_format="png", target=path)
298298
assert os.path.exists(path), "File doesn't exist"
299299
os.unlink(path)
300-
assert isinstance(surface, ImageSurface)
300+
assert isinstance(surface, ImageSurface)
301+
302+
def test_draw_bidentate_with_charge_separation(self):
303+
molecule = Molecule().from_adjacency_list(
304+
"""
305+
1 X u0 p0 c0 {3,S}
306+
2 X u0 p0 c0 {4,D}
307+
3 O u0 p2 c0 {1,S} {4,S}
308+
4 N u0 p0 c+1 {3,S} {2,D} {5,S}
309+
5 O u0 p3 c-1 {4,S}
310+
"""
311+
)
312+
try:
313+
from cairocffi import PDFSurface
314+
except ImportError:
315+
from cairo import PDFSurface
316+
surface, _cr, (_xoff, _yoff, _width, _height) = self.drawer.draw(molecule, file_format="pdf")
317+
assert isinstance(surface, PDFSurface)
318+
319+
def test_draw_cation(self):
320+
try:
321+
from cairocffi import PDFSurface
322+
except ImportError:
323+
from cairo import PDFSurface
324+
path = "test_molecule.pdf"
325+
if os.path.exists(path):
326+
os.unlink(path)
327+
polycycle = Molecule(smiles="C1=NC2=C(N1)C(=O)[NH2+]C(=N2)N")
328+
surface, _cr, (_xoff, _yoff, width, height) = self.drawer.draw(polycycle, file_format="pdf", target=path)
329+
assert isinstance(surface, PDFSurface)
330+
assert width > height
331+
os.unlink(path)
332+
333+
def test_draw_anion(self):
334+
try:
335+
from cairocffi import PDFSurface
336+
except ImportError:
337+
from cairo import PDFSurface
338+
path = "test_molecule.pdf"
339+
if os.path.exists(path):
340+
os.unlink(path)
341+
polycycle = Molecule(smiles="c1ccc2c3ccccc3[CH-]c2c1")
342+
surface, _cr, (_xoff, _yoff, width, height) = self.drawer.draw(polycycle, file_format="pdf", target=path)
343+
assert isinstance(surface, PDFSurface)
344+
assert width > height
345+
os.unlink(path)
346+
347+
def test_draw_zwitterion(self):
348+
try:
349+
from cairocffi import PDFSurface
350+
except ImportError:
351+
from cairo import PDFSurface
352+
path = "test_molecule.pdf"
353+
if os.path.exists(path):
354+
os.unlink(path)
355+
polycycle = Molecule(smiles="[NH3+]CC(=O)[O-]")
356+
surface, _cr, (_xoff, _yoff, width, height) = self.drawer.draw(polycycle, file_format="pdf", target=path)
357+
assert isinstance(surface, PDFSurface)
358+
assert width > height
359+
os.unlink(path)
360+
361+
def test_draw_cation_on_surface(self):
362+
molecule = Molecule().from_adjacency_list(
363+
"""
364+
1 X u0 p0 c0 {3,S}
365+
2 X u0 p0 c0 {3,S}
366+
3 O u0 p1 c+1 {1,S} {2,S} {4,S}
367+
4 H u0 p0 c0 {3,S}
368+
"""
369+
)
370+
try:
371+
from cairocffi import PDFSurface
372+
except ImportError:
373+
from cairo import PDFSurface
374+
path = "test_molecule.pdf"
375+
if os.path.exists(path):
376+
os.unlink(path)
377+
surface, _cr, (_xoff, _yoff, _width, _height) = self.drawer.draw(molecule, file_format="pdf", target=path)
378+
assert isinstance(surface, PDFSurface)
379+
os.unlink(path)
380+
381+
382+
def test_draw_anion_on_surface(self):
383+
molecule = Molecule().from_adjacency_list(
384+
"""
385+
1 X u0 p0 c0 {2,S}
386+
2 O u0 p3 c-1 {1,S}
387+
"""
388+
)
389+
try:
390+
from cairocffi import PDFSurface
391+
except ImportError:
392+
from cairo import PDFSurface
393+
path = "test_molecule.pdf"
394+
if os.path.exists(path):
395+
os.unlink(path)
396+
surface, _cr, (_xoff, _yoff, _width, _height) = self.drawer.draw(molecule, file_format="pdf", target=path)
397+
assert isinstance(surface, PDFSurface)
398+
os.unlink(path)

0 commit comments

Comments
 (0)