Repository: dmlc/dgl

Issue:
dgl.to_homogenous doesn't work correctly on graphs with 0 nodes of a given type
## 🐛 Bug

<!-- A clear and concise description of what the bug is. -->
Assume that we have a heterogenous graph of 2 ntypes - A and B. There are some type A nodes in the graph but 0 type B nodes(This can happen if the graph is sampled via `EdgeLoader` such that the `Block` has no dstnodes of type B).

As a result, I have features for type A nodes but not type B.
```py
g.ndata['h'] = block_outputs
hg = dgl.to_homogeneous(g, ndata=['h'])
print(hg.ndata['h'])
```
Line 1 and 2 give no errors but line 3 results in `KeyError: 'h'` since `hg` doesn't contain any `ndata['h']`.

I can fix this by using
```py
for ntype in ['A', 'B']:
    if ntype in block_outputs:
        emb_size = block_outputs[ntype].shape[1]
for ntype in ['A', 'B']:
    if ntype not in block_outputs:
        block_outputs[ntype] = th.zeros(0, emb_size)
```

## To Reproduce

Steps to reproduce the behavior:

I think the error is clear from above but I can attach a script if necessary, though the script won't be super straightforward.

<!-- If you have a code sample, error messages, stack traces, please provide it here as well -->

## Expected behavior

<!-- A clear and concise description of what you expected to happen. -->
1. If there are 0 nodes of a given type, then the resulting homogeneous graph has `ndata['h']` for the rest of the nodes.
2. An error message on `dgl.to_homogeneous(g, ndata=['h'])` call if the output graph won't contain `ndata` of the specified key.

## Environment

 - DGL Version (e.g., 1.0): 0.6.0post1
 - Backend Library & Version (e.g., PyTorch 0.4.1, MXNet/Gluon 1.3): 1.7.1
 - OS (e.g., Linux): Mac 10.15.7
 - How you installed DGL (`conda`, `pip`, source):
 - Python version: 3.6.10
 - CUDA/cuDNN version (if applicable): N/A
 - Any other relevant information:

dgl.to_homogenous doesn't work correctly on graphs with 0 nodes of a given type
## 🐛 Bug

<!-- A clear and concise description of what the bug is. -->
Assume that we have a heterogenous graph of 2 ntypes - A and B. There are some type A nodes in the graph but 0 type B nodes(This can happen if the graph is sampled via `EdgeLoader` such that the `Block` has no dstnodes of type B).

As a result, I have features for type A nodes but not type B.
```py
g.ndata['h'] = block_outputs
hg = dgl.to_homogeneous(g, ndata=['h'])
print(hg.ndata['h'])
```
Line 1 and 2 give no errors but line 3 results in `KeyError: 'h'` since `hg` doesn't contain any `ndata['h']`.

I can fix this by using
```py
for ntype in ['A', 'B']:
    if ntype in block_outputs:
        emb_size = block_outputs[ntype].shape[1]
for ntype in ['A', 'B']:
    if ntype not in block_outputs:
        block_outputs[ntype] = th.zeros(0, emb_size)
```

## To Reproduce

Steps to reproduce the behavior:

I think the error is clear from above but I can attach a script if necessary, though the script won't be super straightforward.

<!-- If you have a code sample, error messages, stack traces, please provide it here as well -->

## Expected behavior

<!-- A clear and concise description of what you expected to happen. -->
1. If there are 0 nodes of a given type, then the resulting homogeneous graph has `ndata['h']` for the rest of the nodes.
2. An error message on `dgl.to_homogeneous(g, ndata=['h'])` call if the output graph won't contain `ndata` of the specified key.

## Environment

 - DGL Version (e.g., 1.0): 0.6.0post1
 - Backend Library & Version (e.g., PyTorch 0.4.1, MXNet/Gluon 1.3): 1.7.1
 - OS (e.g., Linux): Mac 10.15.7
 - How you installed DGL (`conda`, `pip`, source):
 - Python version: 3.6.10
 - CUDA/cuDNN version (if applicable): N/A
 - Any other relevant information:



Discussion:

Resolving patch:
diff --git a/python/dgl/heterograph.py b/python/dgl/heterograph.py
--- a/python/dgl/heterograph.py
+++ b/python/dgl/heterograph.py
@@ -5985,13 +5985,14 @@ def combine_frames(frames, ids, col_names=None):
         schemes = {key: frames[ids[0]].schemes[key] for key in col_names}
     for frame_id in ids:
         frame = frames[frame_id]
-        for key, scheme in list(schemes.items()):
-            if key in frame.schemes:
-                if frame.schemes[key] != scheme:
-                    raise DGLError('Cannot concatenate column %s with shape %s and shape %s' %
-                                   (key, frame.schemes[key], scheme))
-            else:
-                del schemes[key]
+        if frame.num_rows != 0:
+            for key, scheme in list(schemes.items()):
+                if key in frame.schemes:
+                    if frame.schemes[key] != scheme:
+                        raise DGLError('Cannot concatenate column %s with shape %s and shape %s' %
+                                       (key, frame.schemes[key], scheme))
+                else:
+                    del schemes[key]
 
     if len(schemes) == 0:
         return None


Test patch:
diff --git a/tests/compute/test_heterograph.py b/tests/compute/test_heterograph.py
--- a/tests/compute/test_heterograph.py
+++ b/tests/compute/test_heterograph.py
@@ -1082,6 +1082,19 @@ def test_convert(idtype):
     assert hg.device == g.device
     assert g.number_of_nodes() == 5
 
+@unittest.skipIf(F._default_context_str == 'gpu', reason="Test on cpu is enough")
+@parametrize_dtype
+def test_to_homo_zero_nodes(idtype):
+    # Fix gihub issue #2870
+    g = dgl.heterograph({
+        ('A', 'AB', 'B'): (np.random.randint(0, 200, (1000,)), np.random.randint(0, 200, (1000,))),
+        ('B', 'BA', 'A'): (np.random.randint(0, 200, (1000,)), np.random.randint(0, 200, (1000,))),
+    }, num_nodes_dict={'A': 200, 'B': 200, 'C': 0}, idtype=idtype)
+    g.nodes['A'].data['x'] = F.randn((200, 3))
+    g.nodes['B'].data['x'] = F.randn((200, 3))
+    gg = dgl.to_homogeneous(g, ['x'])
+    assert 'x' in gg.ndata
+
 @parametrize_dtype
 def test_to_homo2(idtype):
     # test the result homogeneous graph has nodes and edges sorted by their types