Repository: pykeen/pykeen
Issue: NodePiece: The use of `complex_normalize` as a `relation_constrainer` results in relation embeddings that only have the value of 1 or -1. 

### Describe the bug

When using the example NodePiece code suggested in the tutorial page, the use of `complex_normalize` results in relation embeddings that all either have the value 1 or -1. 

This seems to be because the `complex_normalize` function is actually expecting a complex tensor in order to work. I couldn't see a clear way to set the `dtype` arg for the relation representations within NodePiece.

Not sure if this is expected or if some changes need to be made to the NodePiece docs or with `complex_normalize` itself. 

### How to reproduce

Running the following:
```python 
from pykeen.models import NodePiece
from pykeen.pipeline import pipeline

result = pipeline(
    dataset="nations",
    dataset_kwargs=dict(
        create_inverse_triples=True,
    ),
    model=NodePiece,
    model_kwargs=dict(
        tokenizers=["RelationTokenizer"],
        num_tokens=[12],
        interaction="rotate",
        relation_constrainer="complex_normalize"
    ),
)
result.model.relation_representations[0](indices=None)
```
Will result in:
```
tensor([ 1.,  1.,  1.,  1., -1.,  1.,  1.,  1., -1.,  1., -1., -1., -1.,  1.,
         1., -1., -1., -1.,  1.,  1., -1.,  1., -1., -1., -1., -1., -1.,  1.,
        -1., -1., -1.,  1.,  1., -1., -1., -1., -1.,  1.,  1., -1.,  1.,  1.,
        -1., -1., -1.,  1.,  1., -1., -1.,  1.,  1.,  1.,  1., -1.,  1.,  1.,
        -1., -1., -1.,  1.,  1., -1.,  1., -1.], grad_fn=<SelectBackward0>)
```

Indeed we can see this would be the behaviour when `complex_normalize` is provided with anything but a complex tensor:

```python
from pykeen.datasets import Nations
from pykeen.models import ERModel
from pykeen.pipeline import pipeline

dataset = Nations()
model = ERModel(
    triples_factory=dataset.training,
    interaction='complex',
    entity_representations_kwargs=dict(embedding_dim=10),
    relation_representations_kwargs=dict(embedding_dim=10, constrainer="complex_normalize"),
)
result = pipeline(
    dataset=dataset,
    model=model,

)
result.model.relation_representations[0](indices=None)[0]
```
```
tensor([-1.,  1., -1., -1.,  1., -1.,  1.,  1.,  1.,  1.],
       grad_fn=<SelectBackward0>)
```

This behaviour can also been seen just using the function itself:
```python 
import torch
from pykeen.utils import complex_normalize

x = torch.randn(1, 5)
print(complex_normalize(x))

x = torch.complex(torch.rand(1, 5), torch.rand(1, 5))
print(complex_normalize(x))
```

```
tensor([[-1., -1., -1., -1.,  1.]])
tensor([[0.7224+0.6915j, 0.5615+0.8275j, 0.7709+0.6370j, 0.7440+0.6682j,
         0.9029+0.4299j]])
```

### Environment

| Key             | Value                    |
|-----------------|--------------------------|
| OS              | posix                    |
| Platform        | Darwin                   |
| Release         | 22.5.0                   |
| Time            | Wed Jun 21 11:58:43 2023 |
| Python          | 3.9.12                   |
| PyKEEN          | 1.10.2-dev               |
| PyKEEN Hash     | UNHASHED                 |
| PyKEEN Branch   |                          |
| PyTorch         | 2.0.1                    |
| CUDA Available? | false                    |
| CUDA Version    | N/A                      |
| cuDNN Version   | N/A                      |

### Additional information

_No response_

### Issue Template Checks

- [X] This is not a feature request (use a different issue template if it is)
- [X] This is not a question (use the [discussions](https://github.com/pykeen/pykeen/discussions/new) forum instead)
- [X] I've read the text explaining why including environment information is important and understand if I omit this information that my issue will be dismissed

Changed files:
src/pykeen/nn/functional.py, src/pykeen/nn/modules.py, src/pykeen/utils.py

diff --git a/src/pykeen/nn/functional.py b/src/pykeen/nn/functional.py
index a1228576..4f22e7a6 100644
--- a/src/pykeen/nn/functional.py
+++ b/src/pykeen/nn/functional.py
@@ -560,6 +560,7 @@ def rotate_interaction(
     :return: shape: batch_dims
         The scores.
     """
+    # todo: raise error on non-complex instead
     h, r, t = ensure_complex(h, r, t)
     if estimate_cost_of_sequence(h.shape, r.shape) < estimate_cost_of_sequence(r.shape, t.shape):
         # r expresses a rotation in complex plane.
diff --git a/src/pykeen/nn/modules.py b/src/pykeen/nn/modules.py
index 038d816b..d72307bb 100644
--- a/src/pykeen/nn/modules.py
+++ b/src/pykeen/nn/modules.py
@@ -167,6 +167,8 @@ class Interaction(nn.Module, Generic[HeadRepresentation, RelationRepresentation,
     # TODO: annotate modelling capabilities? cf., e.g., https://arxiv.org/abs/1902.10197, Table 2
     # TODO: annotate properties, e.g., symmetry, and use them for testing?
     # TODO: annotate complexity?
+    #: whether the interaction is defined on complex input
+    is_complex: ClassVar[bool] = False
 
     @property
     def tail_entity_shape(self) -> Sequence[str]:
@@ -969,6 +971,9 @@ class RotatEInteraction(FunctionalInteraction[FloatTensor, FloatTensor, FloatTen
     .. seealso:: :func:`pykeen.nn.functional.rotate_interaction`
     """
 
+    is_complex: ClassVar[bool] = True
+
+    # todo: inline function?
     func = pkf.rotate_interaction
 
 
diff --git a/src/pykeen/utils.py b/src/pykeen/utils.py
index c195f17f..c987ddcf 100644
--- a/src/pykeen/utils.py
+++ b/src/pykeen/utils.py
@@ -14,6 +14,7 @@ import pathlib
 import random
 import re
 import time
+import warnings
 from abc import ABC, abstractmethod
 from collections import defaultdict
 from io import BytesIO
@@ -1039,8 +1040,16 @@ def complex_normalize(x: torch.Tensor) -> torch.Tensor:
         A tensor formulating complex numbers
 
     :returns:
-        An elementwise noramlized vector.
+        An elementwise normalized vector.
     """
+    # todo: this is a horrible hack, and should be fixed up-stream by making NodePiece
+    #  use proper complex embeddings for rotate interaction
+    if not torch.is_complex(x):
+        warnings.warn("Applying complex_normalize on non-complex input.")
+        (x_complex,) = ensure_complex(x)
+        x_complex = complex_normalize(x_complex)
+        x_real = torch.view_as_real(x_complex)
+        return x_real.view(x.shape)
     return x / x.abs().clamp_min(torch.finfo(x.dtype).eps)
 
 
@@ -1419,6 +1428,7 @@ def ensure_complex(*xs: torch.Tensor) -> Iterable[torch.Tensor]:
         if x.is_complex():
             yield x
             continue
+        warnings.warn(f"{x=} is not complex, but will be viewed as such")
         if x.shape[-1] != 2:
             x = x.view(*x.shape[:-1], -1, 2)
         yield torch.view_as_complex(x)