# Layers

Four `nn.Module` layers cover the usual patterns, and they can all be used inside `nn.Sequential`. Most of these layers expose `in_features` and `out_features`. `MinMaxNorm` uses `feature_dim`.

The three layers that build a circuit, `Basic`, `Reupload` and `Ensemble`, take the same `provider=` argument as `Circuit`, and they pass it to the circuit unchanged. `MinMaxNorm` is classical arithmetic and has no circuit to run, so it does not take the argument, and passing it raises a `TypeError`.

The same three layers return the provider on `.provider`, as the resolved backend, whichever form was passed in. `.provider.name` is its registered name:

```python
from eigenkit import Basic, PennyLaneConfig

layer = Basic(n_qubits=4, depth=2, provider=PennyLaneConfig())
layer.provider.name  # "pennylane"
```

| Layer        | Pattern                                                                     |
| ------------ | --------------------------------------------------------------------------- |
| `Basic`      | One encoding, one variational block, and a measurement of each qubit.       |
| `Reupload`   | Standard data reuploading, optionally with a trainable scale.               |
| `Ensemble`   | `N` independent circuits, one for each chunk of the input.                  |
| `MinMaxNorm` | Min/max normalisation of each feature, in the style of batch normalisation. |

## `Basic`

`Basic` is the thinnest wrapper around `Circuit`: one encoding, one variational block, and a measurement of each qubit. Use it for textbook variational classifiers and regressors, where `n_features == n_qubits`.

```python
import torch
from eigenkit import Basic

# Defaults: RX encoding, RY variational, Z basis, which are three different axes.
layer = Basic(n_qubits=4, depth=2)
out = layer(torch.randn(8, 4))  # shape (8, 4)
```

The circuit that this call builds. `RX(x_i)` encodes one feature per qubit. Then the `basic` variational block, which is one `RY(θ)` per qubit followed by the ladder of controlled-NOT (CNOT) gates, repeats `depth` times, and every qubit is measured in `Z`. These three axes are the defaults. With `variational_type="strong"`, each `RY(θ)` box becomes the three rotations `RZ`, `RY` and `RZ`.

Some combinations of axes make the circuit inert

Some choices of `enc_rotation`, `var_rotation` and `basis` build a circuit whose weights, or whose input, provably cannot reach the output. All three layers check for this and issue a `UserWarning` that names the reason. The layer is still built, because you may want to build a degenerate circuit on purpose, as a baseline.

There are three ways to get one:

- **The weights commute with the observable.** With `variational_type="basic"` and `var_rotation` on the measurement axis (`RZ` with `basis="Z"`, or `RX` with `basis="X"`), the rotations and the CNOT ladder both preserve that group of Pauli operators. The weights then cancel out of the expectation value, and every gradient is exactly `0`. Rotate away from the measurement axis, or use `variational_type="strong"`, whose three rotations `RZ`, `RY` and `RZ` always include one away from the axis.
- **`RZ` encoding.** The circuit starts in `|0...0⟩`, which every `RZ` leaves unchanged up to a phase, so the input never enters. `Reupload` escapes this once it encodes again, because the second encoding acts on a state that has already moved. `Basic` and `Ensemble` encode only once, so they do not escape it.
- **A real circuit measured in `Y`.** `RY` and CNOT are real matrices, so an `RY` encoding with `RY` rotations keeps the statevector real, and `⟨Y⟩` is always `0` on a real state.

Sharing an axis is not a problem in itself. `RY` rotations with `basis="Y"` train well on two qubits or more, because the CNOT ladder does not preserve strings of `Y` operators.

## `Reupload`

`Reupload` is standard data reuploading in a single circuit. The input is encoded again `depth` times, with new variational weights between the encodings. Set `trainable_scale=True` to add a trainable scale for each pair of layer and feature (Schuld et al. 2021).

Here, `in_features == n_qubits`.

```python
from eigenkit import Reupload

# Encoding on RX, variational on RY: same rule as Basic.
layer = Reupload(n_qubits=4, depth=3, enc_rotation="RX", trainable_scale=True)
out = layer(torch.randn(8, 4))  # shape (8, 4)
```

What distinguishes this layer from `Basic` is where the outer bracket starts. It covers the encoding as well as the variational block, so the input is encoded again at each repetition, with new weights. The circuit is drawn with `enc_rotation="RX"`, as in the call above, and without the trainable scale. With `trainable_scale=True`, each `RX(x_i)` becomes `RX(λ·x_i)`, with one trainable `λ` for each pair of layer and feature.

Keep `enc_rotation` and `var_rotation` on different axes here too. When they share one, each `R(x)` and the `R(θ)` that follows it combine into a single `R(x + θ)`, and the variational weights of that layer become a plain offset of the data angle. The defaults of `Reupload` put both rotations on `RY`, so pass `enc_rotation` explicitly, as in the call above.

## `Ensemble`

`Ensemble` runs `N` independent circuits, one for each chunk of the input, each with its own weights. The outputs are concatenated along the feature dimension.

```python
from eigenkit import Ensemble

layer = Ensemble(in_features=20, n_qubits=4, depth=5)
out = layer(torch.randn(128, 20))  # shape (128, 20): 5 circuits * 4 qubits
```

The input is split into chunks of `n_qubits` features, and each chunk gets its own circuit, with its own weights and its own measurement. The outputs of the circuits are concatenated back into one feature vector. Nothing is shared between the branches: no statevector and no parameters. Four branches are drawn, while the call above has five. Note also that on this layer, the variational block defaults to `variational_type="strong"`, not `"basic"`.

## `MinMaxNorm`

`MinMaxNorm` normalises each feature with its minimum and maximum, and keeps running statistics. It is useful before encoding rotations, which expect bounded angles.

```python
import torch
from eigenkit import MinMaxNorm

norm = MinMaxNorm(feature_dim=16, min_val=0.0, max_val=torch.pi)
```

In training mode, it uses the minimum and maximum of each batch, and it updates an exponential moving average of them (`momentum=0.1` by default). In evaluation mode, it uses the running statistics. The output is clamped to `[min_val, max_val]`, to guard against outliers that were not seen in training.

## API reference

### `eigenkit.torch.layers`

Quantum layers for the standard patterns.

- `Basic`: one `Encoding`, one `Variational` block and a measurement.
- `Reupload`: standard data reuploading, optionally with a trainable scale.
- `Ensemble`: `N` independent circuits, one for each chunk of the input.
- `MinMaxNorm`: min-max normalisation of each feature, in the style of `BatchNorm`.

#### `Basic`

Bases: `Module`

One `Encoding`, one `Variational` block and a measurement on each qubit.

Parameters:

| Name               | Type                         | Description                                                                                                                                                                                                                      | Default    |
| ------------------ | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `n_qubits`         | `int`                        | The number of qubits, which is also the number of input features.                                                                                                                                                                | *required* |
| `depth`            | `int`                        | The number of variational layers.                                                                                                                                                                                                | *required* |
| `enc_rotation`     | `Literal['RX', 'RY', 'RZ']`  | The encoding rotation: "RX", "RY" or "RZ". Default: "RX".                                                                                                                                                                        | `'RX'`     |
| `var_rotation`     | `Literal['RX', 'RY', 'RZ']`  | The variational rotation. It is used only when variational_type="basic". The default, "RY", is off both the "RX" encoding axis and the "Z" measurement axis, and this is what lets the weights change the output. Default: "RY". | `'RY'`     |
| `variational_type` | `Literal['basic', 'strong']` | "basic", for one rotation and a controlled-NOT (CNOT) ladder in each layer, or "strong", for RZ, RY and RZ rotations and a CNOT ladder in each layer. Default: "basic".                                                          | `'basic'`  |
| `basis`            | `Literal['X', 'Y', 'Z']`     | The Pauli measurement basis: "X", "Y" or "Z". Default: "Z".                                                                                                                                                                      | `'Z'`      |
| `provider`         | `ProviderSpec`               | The backend that runs the circuit. Default: "numpy".                                                                                                                                                                             | `'numpy'`  |

Example

```python
import torch
from eigenkit import Basic

layer = Basic(n_qubits=4, depth=2)
out = layer(torch.randn(8, 4))  # shape (8, 4)
```

##### `provider`

The backend that the circuit of this layer was compiled for.

The constructor takes `provider=` in the same forms as `Circuit` (a registered name, a configuration or a backend object) and turns it into one `Provider`. This property returns it. The registered name is `provider.name`.

Returns:

| Name       | Type       | Description                           |
| ---------- | ---------- | ------------------------------------- |
| `provider` | `Provider` | The backend that the circuit runs on. |

##### `forward(x)`

Run the layer.

Parameters:

| Name | Type     | Description                                     | Default    |
| ---- | -------- | ----------------------------------------------- | ---------- |
| `x`  | `Tensor` | The input batch, of shape (batch, in_features). | *required* |

Returns:

| Name  | Type     | Description                             |
| ----- | -------- | --------------------------------------- |
| `out` | `Tensor` | The output of the layer for that batch. |

#### `Ensemble`

Bases: `Module`

An ensemble of independent quantum circuits, one for each chunk of the input.

Parameters:

| Name               | Type                         | Description                                                                             | Default    |
| ------------------ | ---------------------------- | --------------------------------------------------------------------------------------- | ---------- |
| `in_features`      | `int`                        | The total number of input features. It must be a multiple of n_qubits.                  | *required* |
| `n_qubits`         | `int`                        | The number of qubits of each circuit.                                                   | *required* |
| `depth`            | `int`                        | The number of variational layers of each circuit.                                       | *required* |
| `enc_rotation`     | `Literal['RX', 'RY', 'RZ']`  | The encoding rotation: "RX", "RY" or "RZ". Default: "RX".                               | `'RX'`     |
| `var_rotation`     | `Literal['RX', 'RY', 'RZ']`  | The variational rotation. It is used only when variational_type="basic". Default: "RY". | `'RY'`     |
| `variational_type` | `Literal['basic', 'strong']` | "basic" or "strong". Default: "strong".                                                 | `'strong'` |
| `basis`            | `Literal['X', 'Y', 'Z']`     | The Pauli measurement basis: "X", "Y" or "Z". Default: "Z".                             | `'Z'`      |
| `provider`         | `ProviderSpec`               | The backend that runs the circuits. Default: "numpy".                                   | `'numpy'`  |

Example

```python
import torch
from eigenkit import Ensemble

layer = Ensemble(in_features=20, n_qubits=4, depth=5)
out = layer(torch.randn(128, 20))  # shape (128, 20)
```

##### `provider`

The backend that the circuits of this layer were compiled for.

The constructor takes `provider=` in the same forms as `Circuit` (a registered name, a configuration or a backend object) and turns it into one `Provider`. This property returns it. The registered name is `provider.name`.

Returns:

| Name       | Type       | Description                           |
| ---------- | ---------- | ------------------------------------- |
| `provider` | `Provider` | The backend that the circuits run on. |

##### `forward(x)`

Run the layer.

Parameters:

| Name | Type     | Description                                     | Default    |
| ---- | -------- | ----------------------------------------------- | ---------- |
| `x`  | `Tensor` | The input batch, of shape (batch, in_features). | *required* |

Returns:

| Name  | Type     | Description                             |
| ----- | -------- | --------------------------------------- |
| `out` | `Tensor` | The output of the layer for that batch. |

#### `MinMaxNorm`

Bases: `Module`

Min-max normalisation of each feature, with running statistics.

It rescales each feature to `[min_val, max_val]`. In training mode, it uses the minimum and maximum of each batch, and it updates an exponential moving average of these statistics. In evaluation mode, it uses the running statistics. A training batch of a single row has no range, so it uses the running statistics too, and if no batch with a range has been seen yet, it returns the row unchanged with a warning. Training and evaluation modes work as in the `BatchNorm` and `LayerNorm` layers of PyTorch, but the output is rescaled to a fixed interval instead of standardised.

It is useful to prepare the inputs of quantum encoding rotations, which expect angles in a bounded range such as `[0, π]`.

Parameters:

| Name          | Type    | Description                                                                                                 | Default    |
| ------------- | ------- | ----------------------------------------------------------------------------------------------------------- | ---------- |
| `feature_dim` | `int`   | The number of features in the last dimension of the input. The inputs have shape (batch_size, feature_dim). | *required* |
| `min_val`     | `float` | The lower bound of the target interval. Default: 0.0.                                                       | `0.0`      |
| `max_val`     | `float` | The upper bound of the target interval. Default: π.                                                         | `pi`       |
| `momentum`    | `float` | The momentum factor of the exponential moving average of the running statistics. Default: 0.1.              | `0.1`      |

Example

```python
import torch
from eigenkit import Ensemble, MinMaxNorm

norm = MinMaxNorm(feature_dim=16, min_val=0.0, max_val=torch.pi)
layer = Ensemble(in_features=16, n_qubits=4, depth=5)
out = layer(norm(torch.randn(32, 16)))
```

##### `forward(x)`

Run the layer.

Parameters:

| Name | Type     | Description                                     | Default    |
| ---- | -------- | ----------------------------------------------- | ---------- |
| `x`  | `Tensor` | The input batch, of shape (batch, feature_dim). | *required* |

Returns:

| Name  | Type     | Description                             |
| ----- | -------- | --------------------------------------- |
| `out` | `Tensor` | The output of the layer for that batch. |

#### `Reupload`

Bases: `Module`

The data-reuploading layer (Perez-Salinas 2020; Schuld 2021).

It supports two patterns:

- Standard reuploading (the default). The same input is encoded `depth` times, with new variational weights between the encodings.
- Trainable feature scaling (`trainable_scale=True`). Each encoding rotation becomes `R(lambda * x)`, with a trainable `lambda` for each pair of layer and feature.

Parameters:

| Name               | Type                         | Description                                                                                                                                                                                            | Default    |
| ------------------ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- |
| `n_qubits`         | `int`                        | The number of qubits, which is also the number of input features.                                                                                                                                      | *required* |
| `depth`            | `int`                        | The number of times the input is encoded.                                                                                                                                                              | *required* |
| `enc_rotation`     | `Literal['RX', 'RY', 'RZ']`  | The encoding rotation: "RX", "RY" or "RZ". Default: "RY".                                                                                                                                              | `'RY'`     |
| `var_rotation`     | `Literal['RX', 'RY', 'RZ']`  | The variational rotation. It is used only when variational_type="basic". Default: "RY".                                                                                                                | `'RY'`     |
| `variational_type` | `Literal['basic', 'strong']` | "basic" or "strong". Default: "basic".                                                                                                                                                                 | `'basic'`  |
| `basis`            | `Literal['X', 'Y', 'Z']`     | The Pauli measurement basis: "X", "Y" or "Z". Default: "Z".                                                                                                                                            | `'Z'`      |
| `trainable_scale`  | `bool`                       | If True, each encoding rotation gets a trainable scale for each pair of layer and feature. The CUDA Quantum backend does not support it, because its QuakeValue arithmetic is limited. Default: False. | `False`    |
| `provider`         | `ProviderSpec`               | The backend that runs the circuit. Default: "numpy".                                                                                                                                                   | `'numpy'`  |

Example

```python
import torch
from eigenkit import Reupload

layer = Reupload(n_qubits=4, depth=3, trainable_scale=True)
out = layer(torch.randn(8, 4))  # shape (8, 4)
```

##### `provider`

The backend that the circuit of this layer was compiled for.

The constructor takes `provider=` in the same forms as `Circuit` (a registered name, a configuration or a backend object) and turns it into one `Provider`. This property returns it. The registered name is `provider.name`.

Returns:

| Name       | Type       | Description                           |
| ---------- | ---------- | ------------------------------------- |
| `provider` | `Provider` | The backend that the circuit runs on. |

##### `forward(x)`

Run the layer.

Parameters:

| Name | Type     | Description                                     | Default    |
| ---- | -------- | ----------------------------------------------- | ---------- |
| `x`  | `Tensor` | The input batch, of shape (batch, in_features). | *required* |

Returns:

| Name  | Type     | Description                             |
| ----- | -------- | --------------------------------------- |
| `out` | `Tensor` | The output of the layer for that batch. |
