Skip to content

Commit

Permalink
Mols matrix to grid image (#6080)
Browse files Browse the repository at this point in the history
* Start creating test for _MolsMatrixToLinear

* Progress on _MolsMatrixToLinear

* Passing _MolsMatrixToLinear tests:
items in 2D list are in correct position in 1D list,
1D list items (for highlight parameters, the subitems) are not lists

* Raise exception if legends_matrix not same dimensions as mols_matrix

* Raise exception if highlight matrices not same dimensions as mols_matrix

* Fix comment

* Rename to _MolsNestedToLinear

* Check that each item in nested lists is a list

* Moved _MolsNestedToLinear to __init__.py

* Attempt to revert to master to undo black autoformatting

* Revert to master; tests pass

* Restored _MolsNestedToLinear and testMolsMatrixToLinear
while undoing black autoformatting of existing code

* Removed obvious black formatting from my code

* Removed black formatting from my __init__ code

* Add some exception testing

* Add exception testing: other matrices aren't same length, or same row length, as mols_matrix

* Parametrize _nestedOrder test

* Create one test for MolsMatrixToGridImage

* Test that all combinations of MolsMatrixToGridImage do not give an error

* Replace list[list] type hint with just list to avoid error

* Add comment: Only test three matrices if supplied

* Remove commented-out line

* MolsMatrixToGridImage produces correct drawings

* Complete the docstrings

* Improved MolsMatrixToGridImage docstring

* Make column spacings consistent in text version of molgrid

* Undo Black autoformatting

* Remove some spacing. Clarify and move a comment.

* Change variable names from first_second to firstSecond

* Remove extended docstring for _MolsNestedToLinear. Remove longestRow().

* Correct drawing output types. Generalize check: instead of nested list, nested iterable.

* Fix import formatting. Remove remaining underscores in middle of variable names.

* Removed CheckElementsAreLists and calls to

* Move helper fns to global namespace and prefix with _. Change expected error type if molsMatrix not actually nested.

* Fix docstring example having returnPNG=True to note it returns a binary string
  • Loading branch information
bertiewooster committed Jul 29, 2023
1 parent 8d95e1b commit de602c8
Show file tree
Hide file tree
Showing 2 changed files with 361 additions and 7 deletions.
171 changes: 168 additions & 3 deletions rdkit/Chem/Draw/UnitTestDraw.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
import unittest

from rdkit import Chem
from rdkit.Chem import AllChem, Draw, rdDepictor, rdMolDescriptors
from rdkit.Chem import AllChem
from rdkit.Chem import Draw
from rdkit.Chem import rdDepictor
from rdkit.Chem import rdMolDescriptors

try:
from rdkit.Chem.Draw import IPythonConsole
Expand Down Expand Up @@ -47,6 +50,51 @@ def setUp(self):
IPythonConsole.UninstallIPythonRenderer()
self.mol = Chem.MolFromSmiles('c1c(C[15NH3+])ccnc1[C@](Cl)(Br)[C@](Cl)(Br)F')

# For testMolsMatrixToLinear and testMolsMatrixToLinear (which test MolsMatrixToGridImage and its helper _MolsNestedToLinear)
s = "NC(C)C(=O)"
mol = Chem.MolFromSmiles(s)
natoms = mol.GetNumAtoms()
nbonds = mol.GetNumBonds()

# Set up matrix with oligomer count for the molecules
# Should produce this grid:
# NC(C)C(=O)
# NC(C)C(=O)NC(C)C(=O)
# NC(C)C(=O)NC(C)C(=O)NC(C)C(=O) NC(C)C(=O)NC(C)C(=O)NC(C)C(=O)NC(C)C(=O)
repeats = [[1], [0, 2], [3, 0, 4]]

# Create molecule if there are 1 or more oligomers;
# otherwise, use None for molecule because drawing is distorted if use Chem.MolFromSmiles("")
self.molsMatrix = [[Chem.MolFromSmiles(s * count) if count else None for count in row] for row in repeats]

self.legendsMatrix = [[str(count) + " unit(s)" for count in row] for row in repeats]

def ithItemList(nunits, itemsPerUnit, i=0):
return [((n * itemsPerUnit) + i) for n in range(nunits)]

self.highlightAtomListsMatrix = [[ithItemList(count, natoms, 0) for count in row] for row in repeats]

# Another bond is created when molecule is oligomerized, so to keep the bond type consistent,
# make items per unit one more than the number of bonds
self.highlightBondListsMatrix = [[ithItemList(count, nbonds + 1, 1) for count in row] for row in repeats]

# Parametrize tests: In addition to supplying molsMatrix, supply 0-3 other matrices
# col labels: legendsMatrix, highlightAtomListsMatrix, highlightBondListsMatrix
# Zero other matrices: 1 parameter set
self.paramSets = [(None, None, None),
# One other matrix: 3 parameter sets
(self.legendsMatrix, None, None),
(None, self.highlightAtomListsMatrix, None),
(None, None, self.highlightBondListsMatrix),
# Two other matrices: 3 parameter sets
(self.legendsMatrix, self.highlightAtomListsMatrix, None),
(self.legendsMatrix, None, self.highlightBondListsMatrix),
(None, self.highlightAtomListsMatrix, self.highlightBondListsMatrix),
# All three other matrices: 1 parameter set
(self.legendsMatrix, self.highlightAtomListsMatrix, self.highlightBondListsMatrix),
]


def test_interactive(self):
# We avoid checking in the code with development flag set
self.assertFalse(self.showAllImages)
Expand Down Expand Up @@ -210,6 +258,123 @@ def testGridSVG(self):
useSVG=True, drawOptions=dopts)
self.assertIn("class='note'", svg)

def testMolsMatrixToLinear(self):
mols, molsPerRow, legends, highlightAtomLists, highlightBondLists = Draw._MolsNestedToLinear(
self.molsMatrix, self.legendsMatrix, self.highlightAtomListsMatrix, self.highlightBondListsMatrix)

nrows = len(self.molsMatrix)

def _nestedOrder(self, molsMatrix, legendsMatrix, highlightAtomListsMatrix, highlightBondListsMatrix):
for r, row in enumerate(molsMatrix):
for c, item in enumerate(row):
linearIndex = (r * molsPerRow) + c
# Test that items in 2D list are in correct position in 1D list
self.assertTrue(mols[linearIndex] == item)
# Test that 1D list items are not lists
self.assertFalse(isinstance(item, list))

# Other three matrices (legends; atom and bond highlighting) need not be supplied;
# only test each if it's supplied
if legendsMatrix is not None:
for r, row in enumerate(legendsMatrix):
for c, item in enumerate(row):
linearIndex = (r * molsPerRow) + c
# Test that items in 2D list are in correct position in 1D list
self.assertTrue(legends[linearIndex] == item)
# Test that 1D list items are not lists
self.assertFalse(isinstance(item, list))

if highlightAtomListsMatrix is not None:
for r, row in enumerate(highlightAtomListsMatrix):
for c, item in enumerate(row):
linearIndex = (r * molsPerRow) + c
# Test that items in 2D list are in correct position in 1D list
self.assertTrue(highlightAtomLists[linearIndex] == item)
# For highlight parameters, entries are lists, so check that sub-items are not lists
for subitem in item:
self.assertFalse(isinstance(subitem, list))

if highlightBondListsMatrix is not None:
for r, row in enumerate(highlightBondListsMatrix):
for c, item in enumerate(row):
linearIndex = (r * molsPerRow) + c
# Test that items in 2D list are in correct position in 1D list
self.assertTrue(highlightBondLists[linearIndex] == item)
# For highlight parameters, entries are lists, so check that sub-items are not lists
for subitem in item:
self.assertFalse(isinstance(subitem, list))

# Test that 1D list has the correct length
self.assertTrue(len(mols) == nrows * molsPerRow)

# Parametrize tests: In addition to supplying molsMatrix, supply 0-3 other matrices
for paramSet in self.paramSets:
_nestedOrder(self, self.molsMatrix, *paramSet)

## Test that exceptions are thrown appropriately

# Test that supplying a non-nested list raises a ValueError
# Set up non-nested lists = first sublist of nested lists
molsNotNested = self.molsMatrix[0]
legendsNotNested = self.legendsMatrix[0]
highlightAtomListsNotNested = self.highlightAtomListsMatrix[0]
highlightBondListsNotNested = self.highlightBondListsMatrix[0]

with self.assertRaises(TypeError):
Draw._MolsNestedToLinear(molsNotNested)

with self.assertRaises(ValueError):
Draw._MolsNestedToLinear(molsMatrix=self.molsMatrix, legendsMatrix=legendsNotNested)

with self.assertRaises(ValueError):
Draw._MolsNestedToLinear(molsMatrix=self.molsMatrix, highlightAtomListsMatrix=highlightAtomListsNotNested)

with self.assertRaises(ValueError):
Draw._MolsNestedToLinear(molsMatrix=self.molsMatrix, highlightBondListsMatrix=highlightBondListsNotNested)

# Test that raises ValueError if other matrices aren't same size (# rows) as molsMatrix
with self.assertRaises(ValueError):
Draw._MolsNestedToLinear(molsMatrix=self.molsMatrix, legendsMatrix=self.legendsMatrix[0:1])

with self.assertRaises(ValueError):
Draw._MolsNestedToLinear(molsMatrix=self.molsMatrix, highlightAtomListsMatrix=self.highlightAtomListsMatrix[0:1])

with self.assertRaises(ValueError):
Draw._MolsNestedToLinear(molsMatrix=self.molsMatrix, highlightBondListsMatrix=self.highlightBondListsMatrix[0:1])

# Test that raises ValueError if other matrices' rows aren't same length as molsMatrix's corresponding row
# Remove last element from first row of each other matrix
legendsMatrixShortRow0 = [self.legendsMatrix[0][0:-1]] + [self.legendsMatrix[1:]]
highlightAtomListsMatrixShortRow0 = [self.highlightAtomListsMatrix[0][0:-1]] + [self.highlightAtomListsMatrix[1:]]
highlightBondListsMatrixShortRow0 = [self.highlightBondListsMatrix[0][0:-1]] + [self.highlightBondListsMatrix[1:]]

with self.assertRaises(ValueError):
Draw._MolsNestedToLinear(molsMatrix=self.molsMatrix, legendsMatrix=legendsMatrixShortRow0)

with self.assertRaises(ValueError):
Draw._MolsNestedToLinear(molsMatrix=self.molsMatrix, highlightAtomListsMatrix=highlightAtomListsMatrixShortRow0)

with self.assertRaises(ValueError):
Draw._MolsNestedToLinear(molsMatrix=self.molsMatrix, highlightBondListsMatrix=highlightBondListsMatrixShortRow0)


def testMolsMatrixToGridImage(self):
subImgSize = (200, 200)

kwargsValue = Draw.rdMolDraw2D.MolDrawOptions()
kwargsValue.addAtomIndices = True

# Tests are that running Draw.MolsMatrixToGridImage doesn't give an error
# with any combination of parameters supplied (or not)
# by parametrizing tests: In addition to supplying molsMatrix, supply 0-3 other matrices
for (legendsMatrix, highlightAtomListsMatrix, highlightBondListsMatrix) in self.paramSets:
for useSVG in (True, False):
for returnPNG in (True, False):
dwgSubImgSizeNokwargs = Draw.MolsMatrixToGridImage(self.molsMatrix, subImgSize, legendsMatrix=legendsMatrix, highlightAtomListsMatrix=highlightAtomListsMatrix, highlightBondListsMatrix=highlightBondListsMatrix, useSVG=useSVG, returnPNG=returnPNG)
dwgSubImgSizeKwargs = Draw.MolsMatrixToGridImage(self.molsMatrix, subImgSize, legendsMatrix=legendsMatrix, highlightAtomListsMatrix=highlightAtomListsMatrix, highlightBondListsMatrix=highlightBondListsMatrix, useSVG=useSVG, returnPNG=returnPNG, drawOptions=kwargsValue)
dwgNosubImgSizeNokwargs = Draw.MolsMatrixToGridImage(self.molsMatrix, legendsMatrix=legendsMatrix, highlightAtomListsMatrix=highlightAtomListsMatrix, highlightBondListsMatrix=highlightBondListsMatrix, useSVG=useSVG, returnPNG=returnPNG)
dwgNosubImgSizeKwargs = Draw.MolsMatrixToGridImage(self.molsMatrix, legendsMatrix=legendsMatrix, highlightAtomListsMatrix=highlightAtomListsMatrix, highlightBondListsMatrix=highlightBondListsMatrix, useSVG=useSVG, returnPNG=returnPNG, drawOptions=kwargsValue)

def testDrawMorgan(self):
m = Chem.MolFromSmiles('c1ccccc1CC1CC1')
bi = {}
Expand Down Expand Up @@ -282,10 +447,10 @@ def testGithub3762(self):
patt = re.compile(re_str)
self.assertEqual(len(patt.findall(svg1)), 2)
self.assertEqual(len(patt.findall(svg2)), 0)

pathlib.Path('testGithub_3762_1.svg').unlink()
pathlib.Path('testGithub_3762_2.svg').unlink()

def testGithub5863(self):
smiles = "C[C@]12C[C@H](O)[C@H]3[C@@H](CCC4=CC(=O)C=C[C@@]43C)[C@@H]1CC[C@]2(O)C(=O)CO"
mol = Chem.MolFromSmiles(smiles)
Expand Down
Loading

0 comments on commit de602c8

Please sign in to comment.