Repository: pyg-team/pytorch_geometric
Pull request #8196 (2023-10-16 06:36:17)

Added an industrial-scale heterogeneous dataset `RCDD`
A challenging dataset with 13,806,619 nodes, 157,814,864 edges, consisting of 7 node types and 7 edge types.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pyg-team/pytorch_geometric/pull/8196?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team) Report
> Merging [#8196](https://app.codecov.io/gh/pyg-team/pytorch_geometric/pull/8196?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team) (c49b802) into [master](https://app.codecov.io/gh/pyg-team/pytorch_geometric/commit/62599d19811b0e41af9f81476ca4ef3e7e2d92e7?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team) (62599d1) will **decrease** coverage by `0.67%`.
> Report is 1 commits behind head on master.
> The diff coverage is `n/a`.

> :exclamation: Current head c49b802 differs from pull request most recent head 63dd341. Consider uploading reports for the commit 63dd341 to get more accurate results

```diff
@@            Coverage Diff             @@
##           master    #8196      +/-   ##
==========================================
- Coverage   88.06%   87.40%   -0.67%     
==========================================
  Files         473      473              
  Lines       28590    28586       -4     
==========================================
- Hits        25179    24985     -194     
- Misses       3411     3601     +190     
```


[see 34 files with indirect coverage changes](https://app.codecov.io/gh/pyg-team/pytorch_geometric/pull/8196/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/CHANGELOG.md b/CHANGELOG.md
index 1c09dc09d..b6fa753cc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 
 ### Added
 
+- Added the `RCDD` dataset ([#8196](https://github.com/pyg-team/pytorch_geometric/pull/8196))
 - Added distributed `GAT + ogbn-products` example targeting XPU device ([#8032](https://github.com/pyg-team/pytorch_geometric/pull/8032))
 
 ### Changed
@@ -15,6 +16,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 
 ### Fixed
 
+- Fixed `to_networkx(to_undirected=True)` in case the input graph is not undirected ([#8204](https://github.com/pyg-team/pytorch_geometric/pull/8204))
 - Fixed sparse-sparse matrix multiplication support on Windows in `TwoHop` and `AddRandomWalkPE` transformations ([#8197](https://github.com/pyg-team/pytorch_geometric/pull/8197))
 
 ### Removed
diff --git a/pyproject.toml b/pyproject.toml
index 8827ce8bd..7de1fdb48 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -116,7 +116,11 @@ include_trailing_comma = true
 skip = [".gitignore", "__init__.py"]
 
 [tool.pytest.ini_options]
-addopts = "--capture=no"
+addopts = [
+    "--capture=no",
+    "--color=yes",
+    "-vv",
+]
 filterwarnings = [
     "ignore:distutils:DeprecationWarning",
     "ignore:'torch_geometric.contrib' contains experimental code:UserWarning",
diff --git a/torch_geometric/datasets/__init__.py b/torch_geometric/datasets/__init__.py
index 0387aab31..b95de8254 100644
--- a/torch_geometric/datasets/__init__.py
+++ b/torch_geometric/datasets/__init__.py
@@ -92,6 +92,7 @@ from .igmc_dataset import IGMCDataset
 from .amazon_book import AmazonBook
 from .hm import HM
 from .ose_gvcs import OSE_GVCS
+from .rcdd import RCDD
 
 from .fake import FakeDataset, FakeHeteroDataset
 from .sbm_dataset import StochasticBlockModelDataset
@@ -202,6 +203,7 @@ hetero_datasets = [
     'AmazonBook',
     'HM',
     'OSE_GVCS',
+    'RCDD',
 ]
 synthetic_datasets = [
     'FakeDataset',
diff --git a/torch_geometric/datasets/rcdd.py b/torch_geometric/datasets/rcdd.py
new file mode 100644
index 000000000..377cd71b7
--- /dev/null
+++ b/torch_geometric/datasets/rcdd.py
@@ -0,0 +1,139 @@
+import os
+from typing import Callable, List, Optional
+
+import numpy as np
+import torch
+
+from torch_geometric.data import (
+    HeteroData,
+    InMemoryDataset,
+    download_url,
+    extract_zip,
+)
+from torch_geometric.utils import index_to_mask
+
+
+class RCDD(InMemoryDataset):
+    r"""The risk commodity detection dataset (RCDD) from the
+    `"Datasets and Interfaces for Benchmarking Heterogeneous Graph
+    Neural Networks" <http://shichuan.org/doc/156.pdf>`_ paper.
+    RCDD is an industrial-scale heterogeneous graph dataset based on a
+    real risk detection scenario from Alibaba's e-commerce platform.
+    It consists of 13,806,619 nodes and 157,814,864 edges across 7 node types
+    and 7 edge types, respectively.
+
+    Args:
+        root (str): Root directory where the dataset should be saved.
+        transform (callable, optional): A function/transform that takes in an
+            :obj:`torch_geometric.data.HeteroData` object and returns a
+            transformed version. The data object will be transformed before
+            every access. (default: :obj:`None`)
+        pre_transform (callable, optional): A function/transform that takes in
+            an :obj:`torch_geometric.data.HeteroData` object and returns a
+            transformed version. The data object will be transformed before
+            being saved to disk. (default: :obj:`None`)
+    """
+    url = ('https://s3.cn-north-1.amazonaws.com.cn/dgl-data/dataset/'
+           'openhgnn/AliRCD_ICDM.zip')
+
+    def __init__(
+        self,
+        root: str,
+        transform: Optional[Callable] = None,
+        pre_transform: Optional[Callable] = None,
+    ):
+        super().__init__(root, transform, pre_transform)
+        self.load(self.processed_paths[0], data_cls=HeteroData)
+
+    @property
+    def raw_file_names(self) -> List[str]:
+        return [
+            'AliRCD_ICDM_nodes.csv',
+            'AliRCD_ICDM_edges.csv',
+            'AliRCD_ICDM_train_labels.csv',
+            'AliRCD_ICDM_test_labels.csv',
+        ]
+
+    @property
+    def processed_file_names(self) -> str:
+        return 'data.pt'
+
+    def download(self):
+        path = download_url(self.url, self.raw_dir)
+        extract_zip(path, self.raw_dir)
+        os.unlink(path)
+
+    @property
+    def num_classes(self) -> int:
+        return 2
+
+    def process(self):
+        import pandas as pd
+
+        data = HeteroData()
+
+        node_df = pd.read_csv(  # AliRCD_ICDM_nodes.csv:
+            self.raw_paths[0],
+            header=None,
+            names=['node_id', 'node_type', 'node_feat'],
+        )
+        # Map global node IDs to local ones for each node type:
+        mapping = torch.empty(len(node_df), dtype=torch.long)
+        for node_type in node_df['node_type'].unique():
+            mask = node_df['node_type'] == node_type
+            mask = torch.from_numpy(mask.values)
+            num_nodes = int(mask.sum())
+            mapping[mask] = torch.arange(num_nodes)
+            data[node_type].num_nodes = num_nodes
+            x = np.vstack([
+                np.asarray(f.split(':'), dtype=np.float32)
+                for f in node_df['node_feat'][mask.numpy()]
+            ])
+            data[node_type].x = torch.from_numpy(x)
+
+        edge_df = pd.read_csv(  # AliRCD_ICDM_edges.csv:
+            self.raw_paths[1],
+            header=None,
+            names=['src_id', 'dst_id', 'src_type', 'dst_type', 'edge_type'],
+        )
+        for edge_type in edge_df['edge_type'].unique():
+            edge_type_df = edge_df[edge_df['edge_type'] == edge_type]
+            src_type = edge_type_df['src_type'].iloc[0]
+            dst_type = edge_type_df['dst_type'].iloc[0]
+            src = mapping[torch.from_numpy(edge_type_df['src_id'].values)]
+            dst = mapping[torch.from_numpy(edge_type_df['dst_id'].values)]
+            edge_index = torch.stack([src, dst], dim=0)
+            data[src_type, edge_type, dst_type].edge_index = edge_index
+
+        train_df = pd.read_csv(  # AliRCD_ICDM_train_labels.csv:
+            self.raw_paths[2],
+            header=None,
+            names=['node_id', 'label'],
+            dtype=int,
+        )
+        test_df = pd.read_csv(  # AliRCD_ICDM_test_labels.csv:
+            self.raw_paths[3],
+            header=None,
+            sep='\t',
+            names=['node_id', 'label'],
+            dtype=int,
+        )
+
+        train_idx = mapping[torch.from_numpy(train_df['node_id'].values)]
+        test_idx = mapping[torch.from_numpy(test_df['node_id'].values)]
+
+        y = torch.full((data['item'].num_nodes, ), -1, dtype=torch.long)
+        y[train_idx] = torch.from_numpy(train_df['label'].values)
+        y[test_idx] = torch.from_numpy(test_df['label'].values)
+
+        train_mask = index_to_mask(train_idx, data['item'].num_nodes)
+        test_mask = index_to_mask(test_idx, data['item'].num_nodes)
+
+        data['item'].y = y
+        data['item'].train_mask = train_mask
+        data['item'].test_mask = test_mask
+
+        if self.pre_transform is not None:
+            data = self.pre_transform(data)
+
+        self.save([data], self.processed_paths[0])
diff --git a/torch_geometric/nn/conv/hgt_conv.py b/torch_geometric/nn/conv/hgt_conv.py
index 5fcdf2443..8404220ae 100644
--- a/torch_geometric/nn/conv/hgt_conv.py
+++ b/torch_geometric/nn/conv/hgt_conv.py
@@ -37,10 +37,6 @@ class HGTConv(MessagePassing):
             information.
         heads (int, optional): Number of multi-head-attentions.
             (default: :obj:`1`)
-        group (str, optional): The aggregation scheme to use for grouping node
-            embeddings generated by different relations
-            (:obj:`"sum"`, :obj:`"mean"`, :obj:`"min"`, :obj:`"max"`).
-            (default: :obj:`"sum"`)
         **kwargs (optional): Additional arguments of
             :class:`torch_geometric.nn.conv.MessagePassing`.
     """
diff --git a/torch_geometric/utils/convert.py b/torch_geometric/utils/convert.py
index dce666aa4..6035a4f63 100644
--- a/torch_geometric/utils/convert.py
+++ b/torch_geometric/utils/convert.py
@@ -101,12 +101,14 @@ def to_networkx(
             copied. (default: :obj:`None`)
         graph_attrs (iterable of str, optional): The graph attributes to be
             copied. (default: :obj:`None`)
-        to_undirected (bool or str, optional): If set to :obj:`True` or
-            :obj:`"upper"`, will return a :obj:`networkx.Graph` instead of a
-            :obj:`networkx.DiGraph`. The undirected graph will correspond to
-            the upper triangle of the corresponding adjacency matrix.
-            Similarly, if set to :obj:`"lower"`, the undirected graph will
-            correspond to the lower triangle of the adjacency matrix.
+        to_undirected (bool or str, optional): If set to :obj:`True`, will
+            return a :class:`networkx.Graph` instead of a
+            :class:`networkx.DiGraph`.
+            By default, will include all edges and make them undirected.
+            If set to :obj:`"upper"`, the undirected graph will only correspond
+            to the upper triangle of the input adjacency matrix.
+            If set to :obj:`"lower"`, the undirected graph will only correspond
+            to the lower triangle of the input adjacency matrix.
             Only applicable in case the :obj:`data` object holds a homogeneous
             graph. (default: :obj:`False`)
         remove_self_loops (bool, optional): If set to :obj:`True`, will not
@@ -127,9 +129,10 @@ def to_networkx(
 
     from torch_geometric.data import HeteroData
 
-    to_undirected_upper: bool = to_undirected in {'upper', True}
+    to_undirected_upper: bool = to_undirected == 'upper'
     to_undirected_lower: bool = to_undirected == 'lower'
-    to_undirected: bool = to_undirected_upper or to_undirected_lower
+    to_undirected: bool = (to_undirected_upper or to_undirected_lower
+                           or to_undirected is True)
 
     if isinstance(data, HeteroData) and to_undirected:
         raise ValueError("'to_undirected' is not supported in "


Test patch:
diff --git a/.github/workflows/prev_testing.yml b/.github/workflows/prev_testing.yml
index d4c247243..5769ffd5a 100644
--- a/.github/workflows/prev_testing.yml
+++ b/.github/workflows/prev_testing.yml
@@ -14,10 +14,8 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        torch-version: [1.11.0, 1.12.0, 1.13.0, 2.0.0]
+        torch-version: [1.12.0, 1.13.0, 2.0.0]
         include:
-          - torch-version: 1.11.0
-            torchvision-version: 0.12.0
           - torch-version: 1.12.0
             torchvision-version: 0.13.0
           - torch-version: 1.13.0
diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml
index 0710eb177..b334b3376 100644
--- a/.github/workflows/testing.yml
+++ b/.github/workflows/testing.yml
@@ -44,7 +44,7 @@ jobs:
       - name: Run tests
         if: steps.changed-files-specific.outputs.only_changed != 'true'
         run: |
-          pytest --cov --cov-report=xml
+          pytest --cov --cov-report=xml --durations 10
 
       - name: Upload coverage
         if: steps.changed-files-specific.outputs.only_changed != 'true'
diff --git a/test/nn/test_to_hetero_transformer.py b/test/nn/test_to_hetero_transformer.py
index 6ee4ed316..6bf1c269b 100644
--- a/test/nn/test_to_hetero_transformer.py
+++ b/test/nn/test_to_hetero_transformer.py
@@ -512,3 +512,24 @@ def test_to_hetero_validate():
 
     with pytest.warns(UserWarning, match="letters, numbers and underscores"):
         model = to_hetero(model, metadata, debug=False)
+
+
+def test_to_hetero_on_static_graphs():
+    x_dict = {
+        'paper': torch.randn(4, 100, 16),
+        'author': torch.randn(4, 100, 16),
+    }
+    edge_index_dict = {
+        ('paper', 'written_by', 'author'):
+        torch.randint(100, (2, 200), dtype=torch.long),
+        ('author', 'writes', 'paper'):
+        torch.randint(100, (2, 200), dtype=torch.long),
+    }
+
+    metadata = list(x_dict.keys()), list(edge_index_dict.keys())
+    model = to_hetero(Net4(), metadata, debug=False)
+    out_dict = model(x_dict, edge_index_dict)
+
+    assert len(out_dict) == 2
+    assert out_dict['paper'].size() == (4, 100, 32)
+    assert out_dict['author'].size() == (4, 100, 32)
diff --git a/test/utils/test_convert.py b/test/utils/test_convert.py
index 5a259ee26..b24269bdd 100644
--- a/test/utils/test_convert.py
+++ b/test/utils/test_convert.py
@@ -107,9 +107,13 @@ def test_to_networkx_undirected():
     data = Data(x=x, pos=pos, edge_index=edge_index, weight=edge_attr)
 
     for remove_self_loops in [True, False]:
-        G = to_networkx(data, node_attrs=['x', 'pos'], edge_attrs=['weight'],
-                        remove_self_loops=remove_self_loops,
-                        to_undirected=True)
+        G = to_networkx(
+            data,
+            node_attrs=['x', 'pos'],
+            edge_attrs=['weight'],
+            remove_self_loops=remove_self_loops,
+            to_undirected=True,
+        )
 
         assert G.nodes[0]['x'] == [1, 2]
         assert G.nodes[1]['x'] == [3, 4]
@@ -117,20 +121,35 @@ def test_to_networkx_undirected():
         assert G.nodes[1]['pos'] == [1, 1]
 
         if remove_self_loops:
-            assert nx.to_numpy_array(G).tolist() == [[0, 1], [1, 0]]
+            assert nx.to_numpy_array(G).tolist() == [[0, 2], [2, 0]]
         else:
-            assert nx.to_numpy_array(G).tolist() == [[3, 1], [1, 0]]
+            assert nx.to_numpy_array(G).tolist() == [[3, 2], [2, 0]]
 
     G = to_networkx(data, edge_attrs=['weight'], to_undirected=False)
     assert nx.to_numpy_array(G).tolist() == [[3, 1], [2, 0]]
 
-    G = to_networkx(data, edge_attrs=['weight'], to_undirected="upper")
+    G = to_networkx(data, edge_attrs=['weight'], to_undirected='upper')
     assert nx.to_numpy_array(G).tolist() == [[3, 1], [1, 0]]
 
-    G = to_networkx(data, edge_attrs=['weight'], to_undirected="lower")
+    G = to_networkx(data, edge_attrs=['weight'], to_undirected='lower')
     assert nx.to_numpy_array(G).tolist() == [[3, 2], [2, 0]]
 
 
+def test_to_networkx_undirected_options():
+    import networkx as nx
+    edge_index = torch.tensor([[0, 1, 1, 2], [1, 0, 2, 0]])
+    data = Data(edge_index=edge_index, num_nodes=3)
+
+    G = to_networkx(data, to_undirected=True)
+    assert nx.to_numpy_array(G).tolist() == [[0, 1, 1], [1, 0, 1], [1, 1, 0]]
+
+    G = to_networkx(data, to_undirected='upper')
+    assert nx.to_numpy_array(G).tolist() == [[0, 1, 0], [1, 0, 1], [0, 1, 0]]
+
+    G = to_networkx(data, to_undirected='lower')
+    assert nx.to_numpy_array(G).tolist() == [[0, 1, 1], [1, 0, 0], [1, 0, 0]]
+
+
 @withPackage('networkx')
 def test_to_networkx_hetero():
     edge_index = get_random_edge_index(5, 10, 20, coalesce=True)