Commit d984056d authored by Bharath Ramsundar's avatar Bharath Ramsundar
Browse files

Cleaned up tests for ConvMolFeaturizer

parent 46cb93bf
Loading
Loading
Loading
Loading
+139 −76
Original line number Diff line number Diff line
@@ -13,36 +13,45 @@ import csv
import random
import numpy as np

max_deg = 6
min_deg = 0

def index_sum(l, offset=0):
  """Returns cumulative sums for set of counts.
def cumulative_sum_minus_last(l, offset=0):
  """Returns cumulative sums for set of counts, removing last entry.

  Returns the cumulative sums for a set of counts with the first returned value
  starting at 0. I.e [3,2,4] -> [0, 3, 5]. Useful for reindexing
  starting at 0. I.e [3,2,4] -> [0, 3, 5]. Note last sum element 9 is missing.
  Useful for reindexing

  Parameters
  ----------
  l: list
    List of integers. Typically small counts.
  """
  return np.delete(np.insert(np.cumsum(l), 0, 0), -1) + offset

def index_sum_with_final(l, offset=0):
def cumulative_sum(l, offset=0):
  """Returns cumulative sums for set of counts.

  TODO(rbharath): How is this different from index_sum?

  Returns the cumulative sums for a set of counts with the first returned value
  starting at 0. I.e [3,2,4] -> [0, 3, 5, 9]. Keeps final sum for searching. 
  Useful for reindexing.

  Parameters
  ----------
  l: list
    List of integers. Typically small counts.
  """
  return np.insert(np.cumsum(l), 0, 0) + offset

class ConvMol(object):
  """Holds information about a molecules.

  Resorts order of atoms internally to be in order of increasing degree. Note
  that only heavy atoms (hydrogens excluded) are considered here.
  """
  def __init__(self, nodes, canon_adj_list, max_deg=6, min_deg=0):
  def __init__(self, atom_features, adj_list, max_deg=6, min_deg=0):
    """
    Parameters
    ----------
    nodes: np.ndarray
    atom_features: np.ndarray
      Has shape (n_atoms, n_feat)
    canon_ad_list: list
      List of length n_atoms, with neighor indices of each atom.
@@ -52,61 +61,63 @@ class ConvMol(object):
      Minimum degree of any atom.
    """

    self.nodes = nodes
    self.n_atoms, self.n_feat = nodes.shape
    self.deg_list = np.array([len(nbr) for nbr in canon_adj_list], dtype=np.int32)
    self.canon_adj_list = canon_adj_list
    self.atom_features = atom_features 
    self.n_atoms, self.n_feat = atom_features.shape
    self.deg_list = np.array([len(nbrs) for nbrs in adj_list], dtype=np.int32)
    self.canon_adj_list = adj_list
    self.deg_adj_lists = []
    self.deg_slice = []
    self.max_deg = max_deg
    self.min_deg = min_deg
    
    self.membership = self.get_num_nodes() * [0]
    self.membership = self.get_num_atoms() * [0]
    
    self.deg_sort()            
    self._deg_sort()            

    # Get the degree id list (which corrects for min_deg)
    self.deg_id_list = np.array(self.deg_list)-min_deg

    # Get the size of each degree block
    deg_size = [self.get_deg_size(deg)
    deg_size = [self.get_num_atoms_with_deg(deg)
                for deg in range(self.min_deg, self.max_deg+1)]

    # Get the the start indices for items in each block
    self.deg_start = index_sum_with_final(deg_size)
    self.deg_start = cumulative_sum(deg_size)

    # Get the node indices when they are reset when the degree changes
    deg_block_indices = [i - self.deg_start[self.deg_list[i]] 
                         for i in range(self.nodes.shape[0])]
                         for i in range(self.n_atoms)]
    
    # Convert to numpy array
    self.deg_block_indices = np.array(deg_block_indices)

  def get_nodes_with_deg(self, deg):
    # Retrieves nodes with the specific degree
  def get_atoms_with_deg(self, deg):
    # Retrieves atom_features with the specific degree
    start_ind = self.deg_slice[deg-self.min_deg,0]
    sz = self.deg_slice[deg-self.min_deg,1]
    return self.nodes[start_ind:(start_ind+sz),:]
    size = self.deg_slice[deg-self.min_deg,1]
    return self.atom_features[start_ind:(start_ind+size),:]

  def get_deg_size(self, deg):
    """Returns the number of nodes with the given degree"""
  def get_num_atoms_with_deg(self, deg):
    """Returns the number of atoms with the given degree"""
    return self.deg_slice[deg-self.min_deg,1]

  def get_num_nodes(self):
    return self.nodes.shape[0]
  def get_num_atoms(self):
    return self.n_atoms

  def _deg_sort(self):
    """Sorts atoms by degree and reorders internal data structures.

  def deg_sort(self):
    """ Sort the order of the nodes by degree, maintaining original order
        whenever two nodes have the same degree. 
    Sort the order of the atom_features by degree, maintaining original order
    whenever two atom_features have the same degree. 
    """
    old_ind = range(self.get_num_nodes())
    old_ind = range(self.get_num_atoms())
    deg_list = self.deg_list
    new_ind = list(np.lexsort((old_ind, deg_list)))

    N_nodes = self.get_num_nodes()
    num_atoms = self.get_num_atoms()
    
    # Reorder old nodes
    self.nodes = self.nodes[new_ind,:]
    # Reorder old atom_features 
    self.atom_features = self.atom_features[new_ind,:]

    # Reorder old deg lists
    self.deg_list = [self.deg_list[i] for i in new_ind]
@@ -131,7 +142,7 @@ class ConvMol(object):
    # Parse as deg separated
    for deg in range(self.min_deg, self.max_deg+1):
      # Get indices corresponding to the current degree
      rng = np.array(range(N_nodes))
      rng = np.array(range(num_atoms))
      indices = rng[deg_array==deg]

      # Extract and save adjacency list for the current degree
@@ -162,14 +173,46 @@ class ConvMol(object):
    deg_slice[:,0] *= (deg_slice[:,1]!=0)
    self.deg_slice = deg_slice                         

  def get_atom_features(self):
    """Returns canonicalized version of atom features.

    Features are sorted by atom degree, with original order maintained when
    degrees are same.
    """
    return self.atom_features

  def get_adjacency_list(self):
    """Returns a canonicalized adjacency list.

    Canonicalized means that the atoms are re-ordered by degree.

    Returns
    -------
    list
      Canonicalized form of adjacency list.
    """
    return self.canon_adj_list

  def get_deg_adjacency_lists(self):
    """Returns adjacency lists grouped by atom degree.

    Returns
    -------
    list
      Has length (max_deg+1-min_deg). The element at position deg is
      itself a list of the neighbor-lists for atoms with degree deg.
    """
    return self.deg_adj_lists

  def get_deg_slice(self):
    """Returns degree-slice tensor.
  
    The deg_slice tensor allows indexing into a flattened version of the
    molecule's atoms.  In general, deg_slice has shape (max_deg+1-min_deg, 2). For
    degree deg, deg_slice[deg][0] is the starting index in the flattened adjacency
    list of atoms with degree deg. Then deg_slice[deg][1] is the number of atoms
    with degree deg.
    molecule's atoms. Assume atoms are sorted in order of degree. Then
    deg_slice[deg][0] is the starting position for atoms of degree deg in
    flattened list, and deg_slice[deg][1] is the number of atoms with degree deg.

    Note deg_slice has shape (max_deg+1-min_deg, 2).

    Returns
    -------
@@ -178,81 +221,89 @@ class ConvMol(object):
    """
    return self.deg_slice

  # TODO(rbharath): Can this be removed?
  @staticmethod
  def get_null_mol(N_feat):
    """ Get one molecule with one node in each deg block, with all the nodes
    connected to each other, and containing N_feat features
  def get_null_mol(n_feat, max_deg=6, min_deg=0):
    """Constructs a null molecules

    args
    ----
    N_feat : (int) number of features for the nodes in the null molecule
    Get one molecule with one atom of each degree, with all the atoms 
    connected to themselves, and containing n_feat features.
    
    Parameters 
    ----------
    n_feat : int
        number of features for the nodes in the null molecule
    """
    # Use random insted of zeros to prevent weird issues with summing to zero
    nodes = np.random.uniform(0,1,[self.max_deg+1-self.min_deg, N_feat])
    canon_adj_list = [i*[i-self.min_deg] for i in range(self.min_deg, self.max_deg+1)]
    atom_features = np.random.uniform(
        0, 1, [max_deg+1-min_deg, n_feat])
    canon_adj_list = [deg*[deg-min_deg]
                      for deg in range(min_deg, max_deg+1)]

    return ConvMol(nodes, canon_adj_list)
    return ConvMol(atom_features, canon_adj_list)

  @staticmethod
  def agglomerate_mols(mol_list, max_deg=6, min_deg=0):
  def agglomerate_mols(mols, max_deg=6, min_deg=0):
    """Concatenates list of ConvMol's into one mol object that can be used to feed 
    into tensorflow placeholders. The indexing of the molecules are preseved during the
    combination, but the indexing of the atoms are greatly changed.
    
    args
    Parameters 
    ----
    mol_list : list of ConvMol objects to be combined into one molecule."""
    mols: list
      ConvMol objects to be combined into one molecule."""

    N_mols = len(mol_list)
    num_mols = len(mols)

    N_nodes_mol = [mol_list[k].get_num_nodes() for k in range(N_mols)]
    N_nodes_cum = index_sum(N_nodes_mol)
    atoms_per_mol = [mol.get_num_atoms() for mol in mols]

    # Get nodes by degree
    nodes_by_deg = [mol_list[k].get_nodes_with_deg(deg)
    # Get atoms by degree
    atoms_by_deg = [mol.get_atoms_with_deg(deg)
                    for deg in range(min_deg, max_deg+1)
                    for k in range(N_mols)]
                    for mol in mols]

    # stack the nodes
    nodes = np.vstack(nodes_by_deg)
    # stack the atoms 
    all_atoms = np.vstack(atoms_by_deg)

    # Sort all atoms by degree.
    # Get the size of each atom list separated by molecule id, then by degree
    mol_deg_sz = [[mol_list[k].get_deg_size(deg) for k in range(N_mols)]
    mol_deg_sz = [[mol.get_num_atoms_with_deg(deg) for mol in mols]
                 for deg in range(min_deg, max_deg+1)]

    deg_sz = map(np.sum, mol_deg_sz)  # Get the final size of each degree block
    # Get the final size of each degree block
    deg_sizes = map(np.sum, mol_deg_sz)  
    # Get the index at which each degree starts, not resetting after each degree
    # And not stopping at any speciic molecule

    deg_start = index_sum(deg_sz)
    deg_start = cumulative_sum_minus_last(deg_sizes)
    # Get the tensorflow object required for slicing (deg x 2) matrix, with the
    # first column telling the start indices of each degree block and the
    # second colum telling the size of each degree block

    # Input for tensorflow 
    deg_slice = np.array(zip(deg_start,deg_sz))
    deg_slice = np.array(zip(deg_start, deg_sizes))
    
    # Determines the membership (atom i belongs to membership[i] molecule)
    membership = [k
                  for deg in range(min_deg, max_deg+1)
                  for k in range(N_mols)
                  for k in range(num_mols)
                  for i in range(mol_deg_sz[deg][k])]

    # Get the index at which each deg starts, resetting after each degree
    # (deg x N_mols) matrix describing the start indices when you count up the atoms
    # (deg x num_mols) matrix describing the start indices when you count up the atoms
    # in the final representation, stopping at each molecule, 
    # resetting every time the degree changes
    start_by_deg = np.vstack([index_sum(l) for l in mol_deg_sz])  
    start_by_deg = np.vstack([cumulative_sum_minus_last(l) for l in mol_deg_sz])  
        
    # Gets the degree resetting block indices for the atoms in each molecule
    # Here, the indices reset when the molecules change, and reset when the
    # degree changes
    deg_block_indices = [mol.deg_block_indices for mol in mol_list]
    deg_block_indices = [mol.deg_block_indices for mol in mols]

    # Get the degree id lookup list. It allows us to search for the degree of a
    # molecule mol_id with corresponding atom mol_atom_id using
    # deg_id_lists[mol_id,mol_atom_id]
    deg_id_lists = [mol.deg_id_list for mol in mol_list]
    deg_id_lists = [mol.deg_id_list for mol in mols]

    # This is used for convience in the following function (explained below)
    start_per_mol = deg_start[:,np.newaxis] + start_by_deg
@@ -269,7 +320,7 @@ class ConvMol(object):
      return start_per_mol[deg_id,mol_id] + deg_block_indices[mol_id][mol_atom_id]
  
    # Initialize the new degree separated adjacency lists
    deg_adj_lists = [np.zeros([deg_sz[deg],deg], dtype=np.int32) 
    deg_adj_lists = [np.zeros([deg_sizes[deg], deg], dtype=np.int32) 
                     for deg in range(min_deg, max_deg+1)]

    # Update the old adjcency lists with the new atom indices and then combine
@@ -279,9 +330,9 @@ class ConvMol(object):
      deg_id = deg-min_deg  # Get corresponding degree id

      # Iterate through all the molecules
      for mol_id in range(N_mols):
      for mol_id in range(num_mols):
        # Get the adjacency lists for this molecule and current degree id
        nbr_list = mol_list[mol_id].deg_adj_lists[deg_id]
        nbr_list = mols[mol_id].deg_adj_lists[deg_id]

        # Correct all atom indices to the final indices, and then save the
        # results into the new adjacency lists 
@@ -294,18 +345,30 @@ class ConvMol(object):

    # Get the final aggregated molecule
    concat_mol = MultiConvMol(
      nodes, deg_adj_lists, deg_slice, membership, N_mols)
      all_atoms, deg_adj_lists, deg_slice, membership, num_mols)
    return concat_mol

class MultiConvMol(object):
  """Holds information about multiple molecules, for use in feeding information into
  tensorflow or keras. Generated using the agglomerate_mols function
  """
  def __init__(self, nodes, deg_adj_lists, deg_slice, membership, N_mols):
  def __init__(self, nodes, deg_adj_lists, deg_slice, membership, num_mols):

    self.nodes = nodes
    self.deg_adj_lists = deg_adj_lists
    self.deg_slice = deg_slice
    self.membership = membership
    self.N_mols = N_mols
    self.N_nodes = nodes.shape[0]
    self.num_mols = num_mols
    self.num_atoms = nodes.shape[0]

  def get_deg_adjacency_lists(self):
    return self.deg_adj_lists

  def get_atom_features(self):
    return self.nodes

  def get_num_atoms(self):
    return self.num_atoms

  def get_num_molecules(self):
    return self.num_mols
+113 −0
Original line number Diff line number Diff line
"""
Tests for ConvMolFeaturizer. 
"""
from __future__ import print_function
from __future__ import division
from __future__ import unicode_literals

__author__ = "Han Altae-Tran and Bharath Ramsundar"
__copyright__ = "Copyright 2016, Stanford University"
__license__ = "GPL"

import unittest
import os
import sys
import numpy as np
import rdkit
from deepchem.featurizers.mol_graphs import ConvMol
from deepchem.featurizers.mol_graphs import MultiConvMol
from deepchem.featurizers.graph_features import ConvMolFeaturizer

class TestConvMolFeaturizer(unittest.TestCase):
  """
  Test ConvMolFeaturizer featurizes properly.
  """
  def test_carbon_nitrogen(self):
    """Test on carbon nitrogen molecule"""
    # Note there is a central carbon of degree 4, with 3 carbons and
    # one nitrogen of degree 1 (connected only to central carbon).
    raw_smiles = ['C[N+](C)(C)C']
    mols = [rdkit.Chem.MolFromSmiles(s) for s in raw_smiles]
    featurizer = ConvMolFeaturizer()
    mols = featurizer.featurize(mols)
    mol = mols[0]

    # 5 atoms in compound
    assert mol.get_num_atoms() == 5

    # Get the adjacency lists grouped by degree
    deg_adj_lists = mol.get_deg_adjacency_lists()
    assert np.array_equal(deg_adj_lists[0],
                          np.zeros([0,0], dtype=np.int32))
    # The 4 outer atoms connected to central carbon
    assert np.array_equal(deg_adj_lists[1],
                          np.array([[4], [4], [4], [4]], dtype=np.int32))
    assert np.array_equal(deg_adj_lists[2],
                          np.zeros([0,2], dtype=np.int32))
    assert np.array_equal(deg_adj_lists[3],
                          np.zeros([0,3], dtype=np.int32))
    # Central carbon connected to everything else.
    assert np.array_equal(deg_adj_lists[4],
                          np.array([[0, 1, 2, 3]], dtype=np.int32))
    assert np.array_equal(deg_adj_lists[5],
                          np.zeros([0,5], dtype=np.int32))
    assert np.array_equal(deg_adj_lists[6],
                          np.zeros([0,6], dtype=np.int32))

  def test_single_carbon(self):
    """Test that single carbon atom is featurized properly."""
    raw_smiles = ['C']
    mols = [rdkit.Chem.MolFromSmiles(s) for s in raw_smiles]
    featurizer = ConvMolFeaturizer()
    mol_list = featurizer.featurize(mols)
    mol = mol_list[0]

    # Only one carbon
    assert mol.get_num_atoms() == 1

    # No bonds, so degree adjacency lists are empty
    deg_adj_lists = mol.get_deg_adjacency_lists()
    assert np.array_equal(deg_adj_lists[0],
                          np.zeros([1,0], dtype=np.int32))
    assert np.array_equal(deg_adj_lists[1],
                          np.zeros([0,1], dtype=np.int32))
    assert np.array_equal(deg_adj_lists[2],
                          np.zeros([0,2], dtype=np.int32))
    assert np.array_equal(deg_adj_lists[3],
                          np.zeros([0,3], dtype=np.int32))
    assert np.array_equal(deg_adj_lists[4],
                          np.zeros([0,4], dtype=np.int32))
    assert np.array_equal(deg_adj_lists[5],
                          np.zeros([0,5], dtype=np.int32))
    assert np.array_equal(deg_adj_lists[6],
                          np.zeros([0,6], dtype=np.int32))

  def test_alkane(self):
    """Test on simple alkane"""
    raw_smiles = ['CCC']
    mols = [rdkit.Chem.MolFromSmiles(s) for s in raw_smiles]
    featurizer = ConvMolFeaturizer()
    mol_list = featurizer.featurize(mols)
    mol = mol_list[0]

    # 3 carbonds in alkane 
    assert mol.get_num_atoms() == 3

    deg_adj_lists = mol.get_deg_adjacency_lists()
    assert np.array_equal(deg_adj_lists[0],
                          np.zeros([0,0], dtype=np.int32))
    # Outer two carbonds are connected to central carbon
    assert np.array_equal(deg_adj_lists[1],
                          np.array([[2], [2]], dtype=np.int32))
    # Central carbon connected to outer two
    assert np.array_equal(deg_adj_lists[2],
                          np.array([[0,1]], dtype=np.int32))
    assert np.array_equal(deg_adj_lists[3],
                          np.zeros([0,3], dtype=np.int32))
    assert np.array_equal(deg_adj_lists[4],
                          np.zeros([0,4], dtype=np.int32))
    assert np.array_equal(deg_adj_lists[5],
                          np.zeros([0,5], dtype=np.int32))
    assert np.array_equal(deg_adj_lists[6],
                          np.zeros([0,6], dtype=np.int32))
    
+86 −101

File changed.

Preview size limit exceeded, changes collapsed.