Repository: pyg-team/pytorch_geometric
Pull request #8047 (2023-09-17 12:10:31)

[Code Coverage] `data/data.py` & `data/hetero_data.py` & `nn/conv/eg_conv.py`
- Part of https://github.com/pyg-team/pytorch_geometric/issues/6528
- Fix `AttributeError: 'function' object has no attribute 'pop'` when calling `remove_edge_index`

Not sure if I am misunderstanding, please take a look:)

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pyg-team/pytorch_geometric/pull/8047?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team) Report
> Merging [#8047](https://app.codecov.io/gh/pyg-team/pytorch_geometric/pull/8047?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team) (7139db6) into [master](https://app.codecov.io/gh/pyg-team/pytorch_geometric/commit/d2f9d18eb1320b50964efe20b80ea107e35cfffd?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team) (d2f9d18) will **increase** coverage by `0.21%`.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##           master    #8047      +/-   ##
==========================================
+ Coverage   88.59%   88.81%   +0.21%     
==========================================
  Files         466      466              
  Lines       27679    27679              
==========================================
+ Hits        24523    24582      +59     
+ Misses       3156     3097      -59     
```


| [Files Changed](https://app.codecov.io/gh/pyg-team/pytorch_geometric/pull/8047?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team) | Coverage Δ | |
|---|---|---|
| [torch\_geometric/data/data.py](https://app.codecov.io/gh/pyg-team/pytorch_geometric/pull/8047?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team#diff-dG9yY2hfZ2VvbWV0cmljL2RhdGEvZGF0YS5weQ==) | `92.80% <100.00%> (+4.47%)` | :arrow_up: |
| [torch\_geometric/data/hetero\_data.py](https://app.codecov.io/gh/pyg-team/pytorch_geometric/pull/8047?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team#diff-dG9yY2hfZ2VvbWV0cmljL2RhdGEvaGV0ZXJvX2RhdGEucHk=) | `91.07% <100.00%> (+3.03%)` | :arrow_up: |

... and [2 files with indirect coverage changes](https://app.codecov.io/gh/pyg-team/pytorch_geometric/pull/8047/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team)

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team)


Patch:
diff --git a/torch_geometric/data/data.py b/torch_geometric/data/data.py
index d618fb4de..935c3b43a 100644
--- a/torch_geometric/data/data.py
+++ b/torch_geometric/data/data.py
@@ -954,17 +954,17 @@ class Data(BaseData, FeatureStore, GraphStore):
         if edge_attr.layout == EdgeLayout.COO and 'edge_index' in self:
             del self.edge_index
             if hasattr(self, '_edge_attrs'):
-                self._edges_to_layout.pop(EdgeLayout.COO, None)
+                self._edge_attrs.pop(EdgeLayout.COO, None)
             return True
         elif edge_attr.layout == EdgeLayout.CSR and 'adj' in self:
             del self.adj
             if hasattr(self, '_edge_attrs'):
-                self._edges_to_layout.pop(EdgeLayout.CSR, None)
+                self._edge_attrs.pop(EdgeLayout.CSR, None)
             return True
         elif edge_attr.layout == EdgeLayout.CSC and 'adj_t' in self:
             del self.adj_t
             if hasattr(self, '_edge_attrs'):
-                self._edges_to_layout.pop(EdgeLayout.CSC, None)
+                self._edge_attrs.pop(EdgeLayout.CSC, None)
             return True
         return False
 
diff --git a/torch_geometric/data/hetero_data.py b/torch_geometric/data/hetero_data.py
index 4df5ce69a..b83157bb4 100644
--- a/torch_geometric/data/hetero_data.py
+++ b/torch_geometric/data/hetero_data.py
@@ -1050,17 +1050,17 @@ class HeteroData(BaseData, FeatureStore, GraphStore):
         if edge_attr.layout == EdgeLayout.COO and 'edge_index' in store:
             del store.edge_index
             if hasattr(self, '_edge_attrs'):
-                self._edges_to_layout.pop((edge_type, EdgeLayout.COO), None)
+                self._edge_attrs.pop((edge_type, EdgeLayout.COO), None)
             return True
         elif edge_attr.layout == EdgeLayout.CSR and 'adj' in store:
             del store.adj
             if hasattr(self, '_edge_attrs'):
-                self._edges_to_layout.pop((edge_type, EdgeLayout.CSR), None)
+                self._edge_attrs.pop((edge_type, EdgeLayout.CSR), None)
             return True
         elif edge_attr.layout == EdgeLayout.CSC and 'adj_t' in store:
             del store.adj_t
             if hasattr(self, '_edge_attrs'):
-                self._edges_to_layout.pop((edge_type, EdgeLayout.CSC), None)
+                self._edge_attrs.pop((edge_type, EdgeLayout.CSC), None)
             return True
         return False
 
diff --git a/torch_geometric/nn/conv/eg_conv.py b/torch_geometric/nn/conv/eg_conv.py
index 964a74361..b1af61f40 100644
--- a/torch_geometric/nn/conv/eg_conv.py
+++ b/torch_geometric/nn/conv/eg_conv.py
@@ -77,15 +77,24 @@ class EGConv(MessagePassing):
     _cached_edge_index: Optional[Tuple[Tensor, OptTensor]]
     _cached_adj_t: Optional[SparseTensor]
 
-    def __init__(self, in_channels: int, out_channels: int,
-                 aggregators: List[str] = ["symnorm"], num_heads: int = 8,
-                 num_bases: int = 4, cached: bool = False,
-                 add_self_loops: bool = True, bias: bool = True, **kwargs):
+    def __init__(
+        self,
+        in_channels: int,
+        out_channels: int,
+        aggregators: List[str] = ['symnorm'],
+        num_heads: int = 8,
+        num_bases: int = 4,
+        cached: bool = False,
+        add_self_loops: bool = True,
+        bias: bool = True,
+        **kwargs,
+    ):
         super().__init__(node_dim=0, **kwargs)
 
         if out_channels % num_heads != 0:
-            raise ValueError(
-                'out_channels must be divisible by the number of heads')
+            raise ValueError(f"'out_channels' (got {out_channels}) must be "
+                             f"divisible by the number of heads "
+                             f"(got {num_heads})")
 
         for a in aggregators:
             if a not in ['sum', 'mean', 'symnorm', 'min', 'max', 'var', 'std']:
@@ -223,7 +232,11 @@ class EGConv(MessagePassing):
     def message_and_aggregate(self, adj_t: SparseTensor, x: Tensor) -> Tensor:
         adj_t_2 = adj_t
         if len(self.aggregators) > 1 and 'symnorm' in self.aggregators:
-            adj_t_2 = adj_t.set_value(None)
+            if isinstance(adj_t, SparseTensor):
+                adj_t_2 = adj_t.set_value(None)
+            else:
+                adj_t_2 = adj_t.clone()
+                adj_t_2.values().fill_(1.0)
 
         outs = []
         for aggr in self.aggregators:


Test patch:
diff --git a/test/data/test_data.py b/test/data/test_data.py
index 2271fb59d..e52f5f8c6 100644
--- a/test/data/test_data.py
+++ b/test/data/test_data.py
@@ -29,11 +29,20 @@ def test_data():
     assert data.get('x').tolist() == x.tolist()
     assert data.get('y', 2) == 2
     assert data.get('y', None) is None
+    assert data.num_edge_types == 1
+    assert data.num_node_types == 1
+    assert next(data('x')) == ('x', x)
 
     assert sorted(data.keys()) == ['edge_index', 'x']
     assert len(data) == 2
     assert 'x' in data and 'edge_index' in data and 'pos' not in data
 
+    data.apply_(lambda x: x.mul_(2), 'x')
+    assert torch.allclose(data.x, x)
+
+    data.requires_grad_('x')
+    assert data.x.requires_grad is True
+
     D = data.to_dict()
     assert len(D) == 2
     assert 'x' in D and 'edge_index' in D
@@ -453,6 +462,13 @@ def test_basic_graph_store():
     edge_attrs = data.get_all_edge_attrs()
     assert len(edge_attrs) == 3
 
+    # Remove:
+    coo, csr, csc = edge_attrs
+    data.remove_edge_index(coo)
+    data.remove_edge_index(csr)
+    data.remove_edge_index(csc)
+    assert len(data.get_all_edge_attrs()) == 0
+
 
 def test_data_generate_ids():
     x = torch.randn(3, 8)
diff --git a/test/data/test_hetero_data.py b/test/data/test_hetero_data.py
index b820d0815..7d5b78fb9 100644
--- a/test/data/test_hetero_data.py
+++ b/test/data/test_hetero_data.py
@@ -639,6 +639,13 @@ def test_basic_graph_store():
     edge_attrs = data.get_all_edge_attrs()
     assert len(edge_attrs) == 3
 
+    # Remove:
+    coo, csr, csc = edge_attrs
+    data.remove_edge_index(coo)
+    data.remove_edge_index(csr)
+    data.remove_edge_index(csc)
+    assert len(data.get_all_edge_attrs()) == 0
+
 
 def test_data_generate_ids():
     data = HeteroData()
diff --git a/test/nn/conv/test_eg_conv.py b/test/nn/conv/test_eg_conv.py
index 665e1bcf0..cdc86edc8 100644
--- a/test/nn/conv/test_eg_conv.py
+++ b/test/nn/conv/test_eg_conv.py
@@ -1,3 +1,4 @@
+import pytest
 import torch
 
 import torch_geometric.typing
@@ -7,55 +8,59 @@ from torch_geometric.typing import SparseTensor
 from torch_geometric.utils import to_torch_csc_tensor
 
 
-def test_eg_conv():
+def test_eg_conv_with_error():
+    with pytest.raises(ValueError, match="must be divisible by the number of"):
+        EGConv(16, 30, num_heads=8)
+
+    with pytest.raises(ValueError, match="Unsupported aggregator"):
+        EGConv(16, 32, aggregators=['xxx'])
+
+
+@pytest.mark.parametrize('aggregators', [
+    ['symnorm'],
+    ['sum', 'symnorm', 'max', 'std'],
+])
+@pytest.mark.parametrize('add_self_loops', [True, False])
+def test_eg_conv(aggregators, add_self_loops):
     x = torch.randn(4, 16)
     edge_index = torch.tensor([[0, 0, 0, 1, 2, 3], [1, 2, 3, 0, 0, 0]])
     adj1 = to_torch_csc_tensor(edge_index, size=(4, 4))
 
-    conv = EGConv(16, 32)
-    assert str(conv) == "EGConv(16, 32, aggregators=['symnorm'])"
+    conv = EGConv(
+        in_channels=16,
+        out_channels=32,
+        aggregators=aggregators,
+        add_self_loops=add_self_loops,
+    )
+    assert str(conv) == f"EGConv(16, 32, aggregators={aggregators})"
     out = conv(x, edge_index)
     assert out.size() == (4, 32)
-    assert torch.allclose(conv(x, adj1.t()), out, atol=1e-6)
+    assert torch.allclose(conv(x, adj1.t()), out, atol=1e-2)
 
     if torch_geometric.typing.WITH_TORCH_SPARSE:
         adj2 = SparseTensor.from_edge_index(edge_index, sparse_sizes=(4, 4))
-        assert torch.allclose(conv(x, adj2.t()), out, atol=1e-6)
+        assert torch.allclose(conv(x, adj2.t()), out, atol=1e-2)
 
     conv.cached = True
-    assert torch.allclose(conv(x, edge_index), out, atol=1e-6)
+    assert torch.allclose(conv(x, edge_index), out, atol=1e-2)
     assert conv._cached_edge_index is not None
-    assert torch.allclose(conv(x, edge_index), out, atol=1e-6)
-    assert torch.allclose(conv(x, adj1.t()), out, atol=1e-6)
+    assert torch.allclose(conv(x, edge_index), out, atol=1e-2)
+    assert torch.allclose(conv(x, adj1.t()), out, atol=1e-2)
 
     if torch_geometric.typing.WITH_TORCH_SPARSE:
-        assert torch.allclose(conv(x, adj2.t()), out, atol=1e-6)
+        assert torch.allclose(conv(x, adj2.t()), out, atol=1e-2)
         assert conv._cached_adj_t is not None
-        assert torch.allclose(conv(x, adj2.t()), out, atol=1e-6)
+        assert torch.allclose(conv(x, adj2.t()), out, atol=1e-2)
 
     if is_full_test():
         t = '(Tensor, Tensor) -> Tensor'
         jit = torch.jit.script(conv.jittable(t))
-        assert torch.allclose(jit(x, edge_index), out, atol=1e-6)
+        assert torch.allclose(jit(x, edge_index), out, atol=1e-2)
 
     if is_full_test() and torch_geometric.typing.WITH_TORCH_SPARSE:
         t = '(Tensor, SparseTensor) -> Tensor'
         jit = torch.jit.script(conv.jittable(t))
-        assert torch.allclose(jit(x, adj2.t()), out, atol=1e-6)
-
-
-def test_eg_conv_multiple_aggregators():
-    x = torch.randn(4, 16)
-    edge_index = torch.tensor([[0, 0, 0, 1, 2, 3], [1, 2, 3, 0, 0, 0]])
-
-    conv = EGConv(16, 32, aggregators=["max", "min"])
-    assert str(conv) == "EGConv(16, 32, aggregators=['max', 'min'])"
-    out = conv(x, edge_index)
-    assert out.size() == (4, 32)
-
-    if torch_geometric.typing.WITH_TORCH_SPARSE:
-        adj = SparseTensor.from_edge_index(edge_index, sparse_sizes=(4, 4))
-        assert torch.allclose(conv(x, adj.t()), out, atol=1e-6)
+        assert torch.allclose(jit(x, adj2.t()), out, atol=1e-2)
 
 
 def test_eg_conv_with_sparse_input_feature():
diff --git a/test/nn/conv/test_hypergraph_conv.py b/test/nn/conv/test_hypergraph_conv.py
index a510e0cd9..483796721 100644
--- a/test/nn/conv/test_hypergraph_conv.py
+++ b/test/nn/conv/test_hypergraph_conv.py
@@ -6,10 +6,10 @@ from torch_geometric.nn import HypergraphConv
 def test_hypergraph_conv_with_more_nodes_than_edges():
     in_channels, out_channels = (16, 32)
     hyperedge_index = torch.tensor([[0, 0, 1, 1, 2, 3], [0, 1, 0, 1, 0, 1]])
-    hyperedge_weight = torch.tensor([1.0, 0.5])
     num_nodes = hyperedge_index[0].max().item() + 1
     num_edges = hyperedge_index[1].max().item() + 1
     x = torch.randn((num_nodes, in_channels))
+    hyperedge_weight = torch.tensor([1.0, 0.5])
     hyperedge_attr = torch.randn((num_edges, in_channels))
 
     conv = HypergraphConv(in_channels, out_channels)