Repository: pyg-team/pytorch_geometric
Pull request: Automatic eigenvalue approximation in ChebConv
I modified ChebConv according to the issue [Eigenvalues in ChebConv](https://github.com/pyg-team/pytorch_geometric/issues/3996) and updated the documentation as well.

Problem statement:
Eigenvalues in ChebConv
### 🚀 The feature, motivation and pitch

In the forward function of ChebConv, there's this error message:

```
if self.normalization != 'sym' and lambda_max is None:
            raise ValueError('You need to pass `lambda_max` to `forward() in`'
                             'case the normalization is non-symmetric.')
```

Based on the Gershgorin circle theorem, 2.0 is the upper bound of eigenvalues for **both** normalizations ('sym' and 'rw').
Furthermore, 2.0 * edge_weight.max() is the upper bound of eigenvalues for the non-normalized Laplacian.

I'd suggest to remove this error message for 'rw' normalization and consider to remove it for non-normalization.
It makes the usage of ChebConv way easier, especially for the regular user.


### Alternatives

_No response_

### Additional context

_No response_

Hints from issue discussion:
Sounds good. Let me know if you want to contribute this feature.
I'd be glad to contribute.

Commit message: Automatic eigenvalue approximation in ChebConv

Patch to review:
diff --git a/torch_geometric/nn/conv/cheb_conv.py b/torch_geometric/nn/conv/cheb_conv.py
index 964832b0ea18..409ee6ff494b 100644
--- a/torch_geometric/nn/conv/cheb_conv.py
+++ b/torch_geometric/nn/conv/cheb_conv.py
@@ -52,8 +52,6 @@ class ChebConv(MessagePassing):
             3. :obj:`"rw"`: Random-walk normalization
             :math:`\mathbf{L} = \mathbf{I} - \mathbf{D}^{-1} \mathbf{A}`
 
-            You need to pass :obj:`lambda_max` to the :meth:`forward` method of
-            this operator in case the normalization is non-symmetric.
             :obj:`\lambda_max` should be a :class:`torch.Tensor` of size
             :obj:`[num_graphs]` in a mini-batch scenario and a
             scalar/zero-dimensional tensor when operating on single graphs.
@@ -104,7 +102,7 @@ def reset_parameters(self):
 
     def __norm__(self, edge_index, num_nodes: Optional[int],
                  edge_weight: OptTensor, normalization: Optional[str],
-                 lambda_max, dtype: Optional[int] = None,
+                 lambda_max: OptTensor = None, dtype: Optional[int] = None,
                  batch: OptTensor = None):
 
         edge_index, edge_weight = remove_self_loops(edge_index, edge_weight)
@@ -113,6 +111,13 @@ def __norm__(self, edge_index, num_nodes: Optional[int],
                                                 normalization, dtype,
                                                 num_nodes)
 
+        if lambda_max is None:
+            lambda_max = 2.0 * edge_weight
+        if not isinstance(lambda_max, torch.Tensor):
+            lambda_max = torch.tensor(lambda_max, dtype=edge_weight.dtype,
+                                      device=edge_weight.device)
+        assert lambda_max is not None
+
         if batch is not None and lambda_max.numel() > 1:
             lambda_max = lambda_max[batch[edge_index[0]]]
 
@@ -130,17 +135,6 @@ def forward(self, x: Tensor, edge_index: Tensor,
                 edge_weight: OptTensor = None, batch: OptTensor = None,
                 lambda_max: OptTensor = None):
         """"""
-        if self.normalization != 'sym' and lambda_max is None:
-            raise ValueError('You need to pass `lambda_max` to `forward() in`'
-                             'case the normalization is non-symmetric.')
-
-        if lambda_max is None:
-            lambda_max = torch.tensor(2.0, dtype=x.dtype, device=x.device)
-        if not isinstance(lambda_max, torch.Tensor):
-            lambda_max = torch.tensor(lambda_max, dtype=x.dtype,
-                                      device=x.device)
-        assert lambda_max is not None
-
         edge_index, norm = self.__norm__(edge_index, x.size(self.node_dim),
                                          edge_weight, self.normalization,
                                          lambda_max, dtype=x.dtype,


Code review comments:
diff_hunk: @@ -113,6 +111,13 @@ def __norm__(self, edge_index, num_nodes: Optional[int],
                                                 normalization, dtype,
                                                 num_nodes)
 
+        if lambda_max is None:
+            lambda_max = 2.0 * edge_weight
+        if not isinstance(lambda_max, torch.Tensor):
+            lambda_max = torch.tensor(lambda_max, dtype=edge_weight.dtype,
+                                      device=edge_weight.device)
path: torch_geometric/nn/conv/cheb_conv.py
text: @user1:
```suggestion
                                      device=edge_index.device)
```

diff_hunk: @@ -113,6 +111,13 @@ def __norm__(self, edge_index, num_nodes: Optional[int],
                                                 normalization, dtype,
                                                 num_nodes)
 
+        if lambda_max is None:
+            lambda_max = 2.0 * edge_weight
path: torch_geometric/nn/conv/cheb_conv.py
text: @user1:
Shouldn't this be?
```python
lambda_max = torch.tensor(2.0, dtype=dtype, device=edge_index.device)
```

@author:
Sorry, I made a mistake. It should be:
`lambda_max = 2.0 * edge_weight.max()`
If normalization is 'sym' or 'rw' then `edge_weight.max()` equals 1.

diff_hunk: @@ -113,6 +111,13 @@ def __norm__(self, edge_index, num_nodes: Optional[int],
                                                 normalization, dtype,
                                                 num_nodes)
 
+        if lambda_max is None:
+            lambda_max = 2.0 * edge_weight
+        if not isinstance(lambda_max, torch.Tensor):
path: torch_geometric/nn/conv/cheb_conv.py
text: @user1:
```suggestion
        elif not isinstance(lambda_max, torch.Tensor):
```

@author:
I preserved this if statement because the original code included it.
But since `lambda_max` is an OptTensor, it is either `None` or an instance of `torch.Tensor`.
So I think it could be deleted.

diff_hunk: @@ -113,6 +111,13 @@ def __norm__(self, edge_index, num_nodes: Optional[int],
                                                 normalization, dtype,
                                                 num_nodes)
 
+        if lambda_max is None:
+            lambda_max = 2.0 * edge_weight
+        if not isinstance(lambda_max, torch.Tensor):
+            lambda_max = torch.tensor(lambda_max, dtype=edge_weight.dtype,
path: torch_geometric/nn/conv/cheb_conv.py
text: @user1:
```suggestion
            lambda_max = torch.tensor(lambda_max, dtype=dtype,
```