# Variational

These blocks are trainable, parametrised layers. There are two kinds:

- `Variational` applies rotations about one axis and a ladder of controlled-NOT (CNOT) gates, repeated `depth` times.
- `StrongVariational` applies the rotations `RZ`, `RY` and `RZ` and a ladder of CNOT gates, repeated `depth` times.

```python
from eigenkit import Variational, StrongVariational

basic = Variational(depth=2, rotation="RY")
strong = StrongVariational(depth=2)
```

## `Variational`

| Parameter  | Type   | Default  | Description                |
| ---------- | ------ | -------- | -------------------------- |
| `depth`    | `int`  | Required | The number of repetitions. |
| `rotation` | `"RX"` | `"RY"`   | `"RZ"`                     |

Each repetition applies one rotation to each qubit, followed by a ladder of CNOT gates (`CNOT(0, 1), CNOT(1, 2), ...`). The total number of trainable weights is `depth * n_qubits`.

The block is the middle box: one `RY(θ)` on every qubit, then `CNOT(0, 1), CNOT(1, 2), ...`, and the bracket shows how many times this repeats. The encoding on the left and the measurement of each qubit on the right are not part of the block. A layer such as [`Basic`](https://docs.kit.eigenorbit.com/user-guide/layers/#basic) adds them around it.

## `StrongVariational`

| Parameter | Type  | Default  | Description                |
| --------- | ----- | -------- | -------------------------- |
| `depth`   | `int` | Required | The number of repetitions. |

Each repetition applies `RZ`, `RY` and `RZ` to every qubit, followed by a ladder of CNOT gates. The total number of trainable weights is `3 * depth * n_qubits`.

The same picture with `strong` in place of `basic`, on two qubits. Each `Rot` box is the three rotations `RZ`, `RY` and `RZ`, so it has three weights where `Variational` has one, and the CNOT ladder after it is unchanged. It is drawn after an `RY` encoding, with a single expectation value measured on the first qubit.

`StrongVariational` is more expressive than `Variational` at the same depth, but it has three times as many parameters. The block adds all the `RZ` gates, then all the `RY` gates, then all the `RZ` gates, in place of three rotations per qubit in turn. This lets the backends that work layer by layer apply each layer of rotations in one fast call.

## Choosing the depth

Each repetition uses a new set of trainable weights. A larger depth gives more expressivity, but it adds parameters and makes training slower.

```python
# Minimal expressivity, fast training.
Variational(depth=1, rotation="RY")

# More expressive; useful for harder classification tasks.
Variational(depth=3, rotation="RY")
```

`depth` must be positive

Both blocks check it when they are created: `Variational(depth=0)` and `StrongVariational(depth=0)` raise `ValueError: depth must be positive, got 0`, and so does any negative value. `Variational` also checks `rotation` at that point. `Reuploading` makes the same checks on `depth`, `in_features` and both of its rotation arguments.

## API reference

### `eigenkit.configs.Variational`

`depth` trainable layers of single-qubit rotations and entangling gates.

Each layer applies one trainable rotation to every qubit, then a controlled-NOT (CNOT) gate from each qubit to the next one. The block takes `depth * n_qubits` weights and no inputs.

Parameters:

| Name       | Type       | Description                                                                      | Default    |
| ---------- | ---------- | -------------------------------------------------------------------------------- | ---------- |
| `depth`    | `int`      | The number of layers. It must be positive, and a ValueError is raised otherwise. | *required* |
| `rotation` | `GateName` | The rotation gate of the trainable layer: "RX", "RY" or "RZ". Default: "RY".     | `'RY'`     |

#### `build(n_qubits)`

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

The layers act on every qubit of the circuit.

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                   |
| -------------------- | ----------------------------- |
| `'BasicVariational'` | The rebuilt BasicVariational. |

#### `required_qubits()`

Return `1`, because the block extends to all the qubits of the circuit.

#### `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. |

### `eigenkit.configs.StrongVariational`

`depth` trainable layers of `RZ`, `RY` and `RZ` rotations and entangling gates.

Each layer applies the rotations `RZ`, `RY` and `RZ` to every qubit, then a controlled-NOT (CNOT) gate from each qubit to the next one. The block is modelled on PennyLane's `StronglyEntanglingLayers`.

The gates are added one layer at a time: all the `RZ` rotations, then all the `RY` rotations, then all the `RZ` rotations, then the CNOT gates. They are not added as three rotations per qubit. The result is the same, because rotations on different qubits commute. The layered order lets the backends that support it apply each layer of rotations in one fast call, in place of `3 * n_qubits` calls on single qubits.

The block takes `3 * depth * n_qubits` weights and no inputs.

Parameters:

| Name    | Type  | Description                                                                      | Default    |
| ------- | ----- | -------------------------------------------------------------------------------- | ---------- |
| `depth` | `int` | The number of layers. It must be positive, and a ValueError is raised otherwise. | *required* |

#### `build(n_qubits)`

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

The layers act on every qubit of the circuit.

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                    |
| --------------------- | ------------------------------ |
| `'StrongVariational'` | The rebuilt StrongVariational. |

#### `required_qubits()`

Return `1`, because the block extends to all the qubits of the circuit.

#### `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. |
