Repository: pyg-team/pytorch_geometric
Pull request #8204 (2023-10-16 17:21:05)

Fix `to_networkx` when `to_undirected=True`
Fix https://github.com/pyg-team/pytorch_geometric/issues/4294

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pyg-team/pytorch_geometric/pull/8204?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team) Report
> Merging [#8204](https://app.codecov.io/gh/pyg-team/pytorch_geometric/pull/8204?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team) (d5a7aaf) into [master](https://app.codecov.io/gh/pyg-team/pytorch_geometric/commit/bbe5a9f6840f7967060ca55f36808db5241f230b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team) (bbe5a9f) will **not change** coverage.
> Report is 1 commits behind head on master.
> The diff coverage is `100.00%`.

> :exclamation: Current head d5a7aaf differs from pull request most recent head 62f0fdb. Consider uploading reports for the commit 62f0fdb to get more accurate results

```diff
@@           Coverage Diff           @@
##           master    #8204   +/-   ##
=======================================
  Coverage   87.39%   87.39%           
=======================================
  Files         473      473           
  Lines       28595    28595           
=======================================
  Hits        24992    24992           
  Misses       3603     3603           
```


| [Files](https://app.codecov.io/gh/pyg-team/pytorch_geometric/pull/8204?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team) | Coverage Δ | |
|---|---|---|
| [torch\_geometric/utils/convert.py](https://app.codecov.io/gh/pyg-team/pytorch_geometric/pull/8204?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team#diff-dG9yY2hfZ2VvbWV0cmljL3V0aWxzL2NvbnZlcnQucHk=) | `57.80% <100.00%> (ø)` | |

: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..5ce5e7da3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,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/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/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)