# Reuploading

The `Reuploading` block alternates encodings of the data with variational blocks, and repeats this pattern `depth` times within a single circuit. For non-trivial classification and regression tasks, reuploading is usually more expressive than encoding the data only once.

```python
from eigenkit import Reuploading

reuploading = Reuploading(
    in_features=4,
    depth=3,
    encoding_rotation="RY",
    variational_rotation="RY",
)
```

## Two patterns

`Reuploading` covers two known patterns:

1. **Standard reuploading** (the default; Pérez-Salinas et al. 2020). The same input is encoded `depth` times, with new variational weights between the encodings.
1. **Trainable feature scaling** (`trainable_scale=True`; Schuld et al. 2021). Each encoding rotation takes the angle `λ * x`, where `λ` is a trainable weight for each pair of layer and feature. At the same depth, this is strictly more expressive than pattern 1.

## Parameters

| Parameter              | Type      | Default    | Description                                                                               |
| ---------------------- | --------- | ---------- | ----------------------------------------------------------------------------------------- |
| `in_features`          | `int`     | Required   | The total number of input features.                                                       |
| `depth`                | `int`     | Required   | The number of full repetitions.                                                           |
| `encoding_rotation`    | `"RX"`    | `"RY"`     | `"RZ"`                                                                                    |
| `variational_rotation` | `"RX"`    | `"RY"`     | `"RZ"`                                                                                    |
| `variational_type`     | `"basic"` | `"strong"` | `"basic"`                                                                                 |
| `trainable_scale`      | `bool`    | `False`    | Whether each encoding rotation gets a trainable scale for each pair of layer and feature. |

## Number of weights

| Setup                       | Trainable weights                          |
| --------------------------- | ------------------------------------------ |
| `"basic"`, without a scale  | `depth * in_features`                      |
| `"strong"`, without a scale | `3 * depth * in_features`                  |
| With `trainable_scale=True` | `depth * in_features` more, for the scales |

## Trainable scale

`trainable_scale=True` adds the parameter expression `Multiply(weight, input)`. The default NumPy simulator differentiates it with its analytic adjoint method. Backends without their own automatic differentiation use the parameter-shift jacobian, which takes the chain rule into account. The gradients agree to machine precision on every backend that supports `Multiply`.

CUDA Quantum does not support `Multiply` at the moment. Use a different backend, or set `trainable_scale=False` if you need CUDA Quantum.

## API reference

### `eigenkit.configs.Reuploading`

Data reuploading: the input is encoded several times, between trainable layers.

The block repeats an encoding of the input followed by a variational layer, `depth` times. Each encoding can have a trainable scale for each feature and repetition.

The arguments give two patterns:

1. Standard reuploading (the default; Pérez-Salinas et al. 2020). The same input is encoded `depth` times, with new variational weights between the encodings.
1. Trainable feature scaling (`trainable_scale=True`; Schuld et al. 2021). Each encoding rotation takes the angle `λ_{d,i} * x_i`, where each pair of repetition `d` and feature `i` has its own trainable `λ_{d,i}`.

Parameters:

| Name                   | Type       | Description                                                                                                                                                                                                         | Default    |
| ---------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `in_features`          | `int`      | The total number of input features. It must be positive.                                                                                                                                                            | *required* |
| `depth`                | `int`      | The number of times the whole sequence is repeated. It must be positive.                                                                                                                                            | *required* |
| `encoding_rotation`    | `GateName` | The rotation gate of the encoding: "RX", "RY" or "RZ". Default: "RY".                                                                                                                                               | `'RY'`     |
| `variational_rotation` | `GateName` | The rotation gate of the variational layer: "RX", "RY" or "RZ". It is used only when variational_type="basic". Default: "RY".                                                                                       | `'RY'`     |
| `variational_type`     | `str`      | "basic", for one rotation per qubit followed by a ladder of controlled-NOT (CNOT) gates, or "strong", for RZ, RY and RZ rotations on every qubit followed by a ladder of CNOT gates. Default: "basic".              | `'basic'`  |
| `trainable_scale`      | `bool`     | If True, each encoding rotation gets a trainable scale for each repetition and each feature. The CUDA Quantum backend does not support it, because it allows only limited arithmetic on parameters. Default: False. | `False`    |

Weight count

- With `variational_type="basic"` and no trainable scale, the block has `depth * in_features` weights.
- With `variational_type="strong"` and no trainable scale, it has `3 * depth * in_features` weights.
- `trainable_scale=True` adds `depth * in_features` weights.

#### `build(n_qubits)`

Build the gates of this block for a circuit with `n_qubits` qubits.

Parameters:

| Name       | Type  | Description                                                                                                         | Default    |
| ---------- | ----- | ------------------------------------------------------------------------------------------------------------------- | ---------- |
| `n_qubits` | `int` | The number of qubits of the circuit being assembled, which is the number of qubits of the widest block in the list. | *required* |

Returns:

| Type         | Description                                                                                                                                          |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `IRFragment` | The gates of the block, as an IRFragment. Its weight and input indices start at 0, and they are shifted when the blocks are combined into a circuit. |

#### `from_dict(d)`

Rebuild a block from the output of `to_dict()`.

Parameters:

| Name | Type   | Description                         | Default    |
| ---- | ------ | ----------------------------------- | ---------- |
| `d`  | `dict` | A dictionary returned by to_dict(). | *required* |

Returns:

| Type            | Description              |
| --------------- | ------------------------ |
| `'Reuploading'` | The rebuilt Reuploading. |

#### `required_qubits()`

Return `in_features`: one qubit for each feature.

#### `to_dict()`

Return a description of this block that can be saved as JSON.

When a saved model is loaded, the `type` key selects the block class whose `from_dict()` rebuilds the block.

Returns:

| Type   | Description                            |
| ------ | -------------------------------------- |
| `dict` | The fields of the block, and its type. |
