Repository: deepchem/deepchem

Issue:
GraphData class not working for single atom mols
## 🐛 Bug

`GraphData` class checks if the `edge_index` contains the invalid node number, using the condition `np.max(edge_index) >= len(node_features)`. In case of single atom mols, `edge_index` in an empty array of `shape = (2, 0)` and np.max() method raises error for empty array : `Error: zero-size array to reduction operation maximum which has no identity`.

## To Reproduce

Steps to reproduce the behaviour:

code to check the error:
```
    num_nodes, num_node_features = 1, 32
    num_edges = 0
    node_features = np.random.random_sample((num_nodes, num_node_features))
    edge_index = np.empty((2, 0), dtype=int)
    graph = GraphData(node_features=node_features, edge_index=edge_index)
    assert graph.num_nodes == num_nodes
    assert graph.num_edges == num_edges
    assert str(graph) == 'GraphData(node_features=[1, 32], edge_index=[2, 0], edge_features=None)'
```
error  message: 
`py::TestGraph::test_graph_data_single_atom_mol Failed with Error: zero-size array to reduction operation maximum which has no identity`

## Expected behaviour

No error should have raised and  `str(graph)` should return `'GraphData(node_features=[1, 32], edge_index=[2, 0], edge_features=None)'`

## Environment

* OS: Ubuntu 20.04.3 LTS
* Python version: Python 3.8.8
* DeepChem version: 2.6.1
* RDKit version (optional):
* TensorFlow version (optional):
* PyTorch version (optional):
* Any other relevant information: NA

## Additional context

This is how Deepchem generally computes edge index:
```
# construct edge (bond) index
    src, dest = [], []
    for bond in datapoint.GetBonds():
      # add edge list considering a directed graph
      start, end = bond.GetBeginAtomIdx(), bond.GetEndAtomIdx()
      src += [start, end]
      dest += [end, start]
```

Discussion:
ARY2260: @arunppsg Could you please confirm that none of these CI errors are related to my changed code and suggest if the PR could be merged in.

Patch:
diff --git a/deepchem/feat/graph_data.py b/deepchem/feat/graph_data.py
index b9d037e95..000b4d750 100644
--- a/deepchem/feat/graph_data.py
+++ b/deepchem/feat/graph_data.py
@@ -69,7 +69,9 @@ class GraphData:
       raise ValueError('edge_index.dtype must contains integers.')
     elif edge_index.shape[0] != 2:
       raise ValueError('The shape of edge_index is [2, num_edges].')
-    elif np.max(edge_index) >= len(node_features):
+
+    # np.max() method works only for a non-empty array, so size of the array should be non-zero
+    elif (edge_index.size != 0) and (np.max(edge_index) >= len(node_features)):
       raise ValueError('edge_index contains the invalid node number.')
 
     if edge_features is not None:


Test patch:
diff --git a/deepchem/feat/tests/test_graph_data.py b/deepchem/feat/tests/test_graph_data.py
index 4f28809c0..1d13cc76b 100644
--- a/deepchem/feat/tests/test_graph_data.py
+++ b/deepchem/feat/tests/test_graph_data.py
@@ -20,12 +20,11 @@ class TestGraph(unittest.TestCase):
     # z is kwargs
     z = np.random.random(5)
 
-    graph = GraphData(
-        node_features=node_features,
-        edge_index=edge_index,
-        edge_features=edge_features,
-        node_pos_features=node_pos_features,
-        z=z)
+    graph = GraphData(node_features=node_features,
+                      edge_index=edge_index,
+                      edge_features=edge_features,
+                      node_pos_features=node_pos_features,
+                      z=z)
 
     assert graph.num_nodes == num_nodes
     assert graph.num_node_features == num_node_features
@@ -97,13 +96,12 @@ class TestGraph(unittest.TestCase):
     ]
 
     graph_list = [
-        GraphData(
-            node_features=np.random.random_sample((num_nodes_list[i],
-                                                   num_node_features)),
-            edge_index=edge_index_list[i],
-            edge_features=np.random.random_sample((num_edge_list[i],
-                                                   num_edge_features)),
-            node_pos_features=None) for i in range(len(num_edge_list))
+        GraphData(node_features=np.random.random_sample(
+            (num_nodes_list[i], num_node_features)),
+                  edge_index=edge_index_list[i],
+                  edge_features=np.random.random_sample(
+                      (num_edge_list[i], num_edge_features)),
+                  node_pos_features=None) for i in range(len(num_edge_list))
     ]
     batch = BatchGraphData(graph_list)
 
@@ -112,3 +110,22 @@ class TestGraph(unittest.TestCase):
     assert batch.num_edges == sum(num_edge_list)
     assert batch.num_edge_features == num_edge_features
     assert batch.graph_index.shape == (sum(num_nodes_list),)
+
+  @pytest.mark.torch
+  def test_graph_data_single_atom_mol(self):
+    """
+    Test for graph data when no edges in the graph (example: single atom mol)
+    """
+    num_nodes, num_node_features = 1, 32
+    num_edges = 0
+    node_features = np.random.random_sample((num_nodes, num_node_features))
+    edge_index = np.empty((2, 0), dtype=int)
+
+    graph = GraphData(node_features=node_features, edge_index=edge_index)
+
+    assert graph.num_nodes == num_nodes
+    assert graph.num_node_features == num_node_features
+    assert graph.num_edges == num_edges
+    assert str(
+        graph
+    ) == 'GraphData(node_features=[1, 32], edge_index=[2, 0], edge_features=None)'