Repository: pyg-team/pytorch_geometric
Pull request: Add code examples for `torch_geometric.utils` functions
This PR is to add some code examples to doc-strings for `torch_geometric.utils` functions, as mentioned in [this issue](https://github.com/pyg-team/pytorch_geometric/issues/5383). Currently, the following functions imported in `torch_geometric.utils.__init__` are listed (ordered by the filenames):

- [x] **coalesce.py**: `coalesce`
- [x] **convert.py**: `to_scipy_sparse_matrix`, `from_scipy_sparse_matrix`
- [x] **convert.py**: `to_networkx`, `from_networkx`
- [x] **convert.py**: `to_trimesh`, `from_trimesh`
- [ ] **convert.py**: `to_cugraph`, `from_cugraph`
- [x] **degree.py**: `degree`
- [x] **dropout.py**: `dropout_adj`
- [x] **geodesic.py**: `geodesic_distance`
- [x] **get_laplacian.py**: `get_laplacian`
- [ ] **get_mesh_laplacian.py**: `get_mesh_laplacian`
- [x] **grid.py**: `grid`
- [x] **homophily.py**: `homophily`
- [x] **isolated.py**: `contains_isolated_nodes`, `remove_isolated_nodes`
- [x] **loop.py**: `contains_self_loops`, `remove_self_loops`,`segregate_self_loops`, `add_self_loops`,  `add_remaining_self_loops`, `get_self_loop_attr`
- [x] **mask.py**: `index_to_mask`, `mask_to_index`
- [x] **negative_sampling.py**: `negative_sampling`, `batched_negative_sampling`, `structured_negative_sampling`, `structured_negative_sampling_feasible`
- [x] **normalized_cut.py**: `normalized_cut`
- [x] **random.py**: `erdos_renyi_graph`, `stochastic_blockmodel_graph`, `barabasi_albert_graph`
- [ ] **scatter.py** `scatter`
- [ ] **smiles.py**: `from_smiles`
- [x] **softmax.py**: `softmax`
- [x] **sort_edge_index.py**: `sort_edge_index`
- [x] **sparse.py**: `dense_to_sparse`
- [x] **subgraph.py**: `get_num_hops`, `subgraph`, `k_hop_subgraph`, `bipartite_subgraph`
- [x] **to_dense_adj.py**: `to_dense_adj`
- [x] **to_dense_batch.py**: `to_dense_batch`
- [ ] ~~**train_test_split_edges.py**: `train_test_split_edges`~~(deprecated)
- [ ] **tree_decomposition.py**: `tree_decomposition`
- [x] **unbatch.py**: `unbatch`, `unbatch_edge_index`
- [x] **undirected.py**: `is_undirected`, `to_undirected`

Just wanted to let you know how this PR is doing now. I'll finish the remaining functions in the next few days. Please let me know if there is something missing or wrong. 

Problem statement:
Examples needed
### 📚 Describe the documentation issue

Hi,

I have a suggestion for the documentation. Why not just providing a simple example for all the APIs just like Pytorch official documentation. I am pretty sure this would be more convenient for users to know how exactly those APIs work. Thank you
 

### Suggest a potential alternative/fix

Need a simple example so that users can know how to use those APIs

Hints from issue discussion:
Do you mean for `torch_geometric.utils` or for every module provided in PyG? A lot of these modules built upon similar principles so I assume this would result in a lot of duplicated code examples. Anyway, I am always happy to improve documentation, but would need to rely on help to integrate this.
Thank you for your reply.
Yep. I think torch_geometric.utils might be the first priority, cause it's a very common used module in PyG. For the rest modules, I guess a simple example can also be provided if the usage is not that straightforward? 
Sounds great. Shall we split the work and collect all functions for which we want to provide an example?
Yep. Looks like a good idea!
Would you mind if I take this task? I plan to add PyTorch-style code examples to `torch_geometric.utils` functions.
@EdisonLeeeee would be great to get help in. I will be collecting all functions we want to add an example for in this issue here, but this shouldn‘t stop you from going ahead :)

Commit message: Update torch_geometric/utils/coalesce.py

Co-authored-by: Matthias Fey <matthias.fey@tu-dortmund.de>

Patch to review:
diff --git a/torch_geometric/utils/coalesce.py b/torch_geometric/utils/coalesce.py
index 6616bc03c1e8..9b6f2c878ad0 100644
--- a/torch_geometric/utils/coalesce.py
+++ b/torch_geometric/utils/coalesce.py
@@ -37,6 +37,31 @@ def coalesce(
 
     :rtype: :class:`LongTensor` if :attr:`edge_attr` is :obj:`None`, else
         (:class:`LongTensor`, :obj:`Tensor` or :obj:`List[Tensor]]`)
+
+    Example:
+
+        >>> edge_index = torch.tensor([[1, 1, 2, 3], [3, 3, 1, 2]])
+        >>> edge_attr = torch.tensor([1., 1., 1., 1.])
+
+        >>> coalesce(edge_index)
+        tensor([[1, 2, 3],
+                [3, 1, 2]])
+
+        >>> # Sort `edge_index` column-wise
+        >>> coalesce(edge_index, sort_by_row=False)
+        tensor([[2, 3, 1],
+                [1, 2, 3]])
+
+        >>> coalesce(edge_index, edge_attr)
+        (tensor([[1, 2, 3],
+                [3, 1, 2]]),
+        tensor([2., 1., 1.]))
+
+        >>> # Use 'mean' operation to merge edge features
+        >>> coalesce(edge_index, edge_attr, reduce='mean')
+        (tensor([[1, 2, 3],
+                [3, 1, 2]]),
+        tensor([1., 1., 1.]))
     """
     nnz = edge_index.size(1)
     num_nodes = maybe_num_nodes(edge_index, num_nodes)
diff --git a/torch_geometric/utils/convert.py b/torch_geometric/utils/convert.py
index 3316c13bc811..d4bef65f1ced 100644
--- a/torch_geometric/utils/convert.py
+++ b/torch_geometric/utils/convert.py
@@ -19,6 +19,16 @@ def to_scipy_sparse_matrix(edge_index, edge_attr=None, num_nodes=None):
             edge features. (default: :obj:`None`)
         num_nodes (int, optional): The number of nodes, *i.e.*
             :obj:`max_val + 1` of :attr:`index`. (default: :obj:`None`)
+
+    Examples:
+
+        >>> edge_index = torch.tensor([
+        ...     [0, 1, 1, 2, 2, 3],
+        ...     [1, 0, 2, 1, 3, 2],
+        ... ])
+        >>> to_scipy_sparse_matrix(edge_index)
+        <4x4 sparse matrix of type '<class 'numpy.float32'>'
+            with 6 stored elements in COOrdinate format>
     """
     row, col = edge_index.cpu()
 
@@ -39,6 +49,20 @@ def from_scipy_sparse_matrix(A):
 
     Args:
         A (scipy.sparse): A sparse matrix.
+
+    Examples:
+
+        >>> edge_index = torch.tensor([
+        ...     [0, 1, 1, 2, 2, 3],
+        ...     [1, 0, 2, 1, 3, 2],
+        ... ])
+        >>> adj = to_scipy_sparse_matrix(edge_index)
+
+        >>> `edge_index` and `edge_weight` are both returned
+        >>> from_scipy_sparse_matrix(adj)
+        (tensor([[0, 1, 1, 2, 2, 3],
+                [1, 0, 2, 1, 3, 2]]),
+        tensor([1., 1., 1., 1., 1., 1.]))
     """
     A = A.tocoo()
     row = torch.from_numpy(A.row).to(torch.long)
@@ -72,6 +96,17 @@ def to_networkx(data, node_attrs=None, edge_attrs=None, graph_attrs=None,
             :obj:`False`)
         remove_self_loops (bool, optional): If set to :obj:`True`, will not
             include self loops in the resulting graph. (default: :obj:`False`)
+
+    Examples:
+
+        >>> edge_index = torch.tensor([
+        ...     [0, 1, 1, 2, 2, 3],
+        ...     [1, 0, 2, 1, 3, 2],
+        ... ])
+        >>> data = Data(edge_index=edge_index, num_nodes=4)
+        >>> to_networkx(data)
+        <networkx.classes.digraph.DiGraph at 0x2713fdb40d0>
+
     """
     import networkx as nx
 
@@ -140,6 +175,19 @@ def from_networkx(G, group_node_attrs: Optional[Union[List[str], all]] = None,
 
         All :attr:`group_node_attrs` and :attr:`group_edge_attrs` values must
         be numeric.
+
+    Examples:
+
+        >>> edge_index = torch.tensor([
+        ...     [0, 1, 1, 2, 2, 3],
+        ...     [1, 0, 2, 1, 3, 2],
+        ... ])
+        >>> data = Data(edge_index=edge_index, num_nodes=4)
+        >>> g = to_networkx(data)
+
+        >>> A `Data` object is returned
+        >>> from_networkx(g)
+        Data(edge_index=[2, 6], num_nodes=4)
     """
     import networkx as nx
 
diff --git a/torch_geometric/utils/degree.py b/torch_geometric/utils/degree.py
index eeee39e67463..ebe34cfb2e59 100644
--- a/torch_geometric/utils/degree.py
+++ b/torch_geometric/utils/degree.py
@@ -19,6 +19,12 @@ def degree(index: Tensor, num_nodes: Optional[int] = None,
             returned tensor.
 
     :rtype: :class:`Tensor`
+
+    Examples:
+
+        >>> row = torch.tensor([0, 1, 0, 2, 0])
+        >>> degree(row, dtype=torch.long)
+        tensor([3, 1, 1])
     """
     N = maybe_num_nodes(index, num_nodes)
     out = torch.zeros((N, ), dtype=dtype, device=index.device)
diff --git a/torch_geometric/utils/dropout.py b/torch_geometric/utils/dropout.py
index 368ae3ca91f1..bbf3f953ae5f 100644
--- a/torch_geometric/utils/dropout.py
+++ b/torch_geometric/utils/dropout.py
@@ -35,6 +35,24 @@ def dropout_adj(
             :obj:`max_val + 1` of :attr:`edge_index`. (default: :obj:`None`)
         training (bool, optional): If set to :obj:`False`, this operation is a
             no-op. (default: :obj:`True`)
+
+    Examples:
+
+        >>> edge_index = torch.tensor([
+        ...     [0, 1, 1, 2, 2, 3],
+        ...     [1, 0, 2, 1, 3, 2],
+        ... ])
+        >>> edge_attr = torch.tensor([1, 2, 3, 4, 5, 6])
+        >>> dropout_adj(edge_index, edge_attr)
+        (tensor([[0, 1, 2, 3],
+                [1, 2, 3, 2]]),
+        tensor([1, 3, 5, 6]))
+
+        >>> # The returned graph is kept undirected
+        >>> dropout_adj(edge_index, edge_attr, force_undirected=True)
+        (tensor([[0, 1, 2, 1, 2, 3],
+                [1, 2, 3, 0, 1, 2]]),
+        tensor([1, 3, 5, 1, 3, 5]))
     """
 
     if p < 0. or p > 1.:
diff --git a/torch_geometric/utils/get_laplacian.py b/torch_geometric/utils/get_laplacian.py
index ebc23e06e3ce..9d8fe368004a 100644
--- a/torch_geometric/utils/get_laplacian.py
+++ b/torch_geometric/utils/get_laplacian.py
@@ -35,6 +35,22 @@ def get_laplacian(edge_index, edge_weight: Optional[torch.Tensor] = None,
             in case :obj:`edge_weight=None`. (default: :obj:`None`)
         num_nodes (int, optional): The number of nodes, *i.e.*
             :obj:`max_val + 1` of :attr:`edge_index`. (default: :obj:`None`)
+
+    Examples:
+
+        >>> edge_index = torch.tensor([[0, 1, 1, 2],
+        ...                            [1, 0, 2, 1]])
+        >>> edge_weight = torch.tensor([1., 2., 2., 4.])
+
+        >>> # No normalization
+        >>> lap = get_laplacian(edge_index, edge_weight)
+
+        >>> # Symmetric normalization
+        >>> lap_sym = get_laplacian(edge_index, edge_weight,
+                                    normalization='sym')
+
+        >>> # Random-walk normalization
+        >>> lap_rw = get_laplacian(edge_index, edge_weight, normalization='rw')
     """
 
     if normalization is not None:
diff --git a/torch_geometric/utils/homophily.py b/torch_geometric/utils/homophily.py
index 7d955deb2050..0ff608421c63 100644
--- a/torch_geometric/utils/homophily.py
+++ b/torch_geometric/utils/homophily.py
@@ -63,6 +63,22 @@ def homophily(edge_index: Adj, y: Tensor, batch: OptTensor = None,
             either :obj:`"edge"` (first formula), :obj:`"node"` (second
             formula) or :obj:`"edge_insensitive"` (third formula).
             (default: :obj:`"edge"`)
+
+    Examples:
+
+        >>> edge_index = torch.tensor([[0, 1, 2, 3],
+        ...                            [1, 2, 0, 4]])
+        >>> y = torch.tensor([0, 0, 0, 0, 1])
+        >>> batch = torch.tensor([0, 0, 0, 1, 1])
+
+        >>> # Edge homophily ratio
+        >>> homophily(edge_index, y, method='edge')
+
+        >>> # Node homophily ratio
+        >>> homophily(edge_index, y, method='node')
+
+        >>> # Class insensitive edge homophily ratio
+        >>> homophily(edge_index, y, method='edge_insensitive')
     """
     assert method in {'edge', 'node', 'edge_insensitive'}
     y = y.squeeze(-1) if y.dim() > 1 else y
diff --git a/torch_geometric/utils/isolated.py b/torch_geometric/utils/isolated.py
index a7ace6d0f03c..3755e7e97560 100644
--- a/torch_geometric/utils/isolated.py
+++ b/torch_geometric/utils/isolated.py
@@ -15,6 +15,16 @@ def contains_isolated_nodes(edge_index, num_nodes=None):
             :obj:`max_val + 1` of :attr:`edge_index`. (default: :obj:`None`)
 
     :rtype: bool
+
+    Examples:
+
+        >>> edge_index = torch.tensor([[0, 1, 0],
+        ...                            [1, 0, 0]])
+        >>> contains_isolated_nodes(edge_index)
+        False
+
+        >>> contains_isolated_nodes(edge_index, num_nodes=3)
+        True
     """
     num_nodes = maybe_num_nodes(edge_index, num_nodes)
     edge_index, _ = remove_self_loops(edge_index)
@@ -36,6 +46,19 @@ def remove_isolated_nodes(edge_index, edge_attr=None, num_nodes=None):
             :obj:`max_val + 1` of :attr:`edge_index`. (default: :obj:`None`)
 
     :rtype: (LongTensor, Tensor, BoolTensor)
+
+    Examples:
+
+        >>> edge_index = torch.tensor([[0, 1, 0],
+        ...                            [1, 0, 0]])
+        >>> edge_index, edge_attr, mask = remove_isolated_nodes(edge_index)
+        >>> mask # node mask (2 nodes)
+        tensor([True, True])
+
+        >>> edge_index, edge_attr, mask = remove_isolated_nodes(edge_index,
+        ...                                                     num_nodes=3)
+        >>> mask # node mask (3 nodes)
+        tensor([ True,  True, False])
     """
     num_nodes = maybe_num_nodes(edge_index, num_nodes)
 


Code review comments:
diff_hunk: @@ -140,6 +175,19 @@ def from_networkx(G, group_node_attrs: Optional[Union[List[str], all]] = None,
 
         All :attr:`group_node_attrs` and :attr:`group_edge_attrs` values must
         be numeric.
+
+    Examples:
+
+        >>> edge_index = torch.tensor([
+        ...     [0, 1, 1, 2, 2, 3],
+        ...     [1, 0, 2, 1, 3, 2],
+        ... ])
+        >>> data = Data(edge_index=edge_index, num_nodes=4)
+        >>> g = to_networkx(data)
+
+        >>> A `Data` object is returned
path: torch_geometric/utils/convert.py
text: @user1:
```suggestion
        >>> # A `Data` object is returned
```

diff_hunk: @@ -63,6 +63,22 @@ def homophily(edge_index: Adj, y: Tensor, batch: OptTensor = None,
             either :obj:`"edge"` (first formula), :obj:`"node"` (second
             formula) or :obj:`"edge_insensitive"` (third formula).
             (default: :obj:`"edge"`)
+
+    Examples:
+
+        >>> edge_index = torch.tensor([[0, 1, 2, 3],
+        ...                            [1, 2, 0, 4]])
+        >>> y = torch.tensor([0, 0, 0, 0, 1])
+        >>> batch = torch.tensor([0, 0, 0, 1, 1])
path: torch_geometric/utils/homophily.py
text: @user1:
not used?

@author:
My bad. Will soon be removed.