## `eigenkit.configs`

The blocks that make up a circuit, and the configurations of the backends.

Blocks are combined into a circuit with `Circuit(blocks=[...])`:

- `Encoding` encodes classical features as rotation angles.
- `Variational` applies, in each layer, one trainable rotation to every qubit, followed by a ladder of controlled-NOT (CNOT) gates.
- `StrongVariational` applies, in each layer, trainable `RZ`, `RY` and `RZ` rotations to every qubit, followed by a ladder of CNOT gates.
- `Reuploading` encodes the input several times, between trainable layers. The encoding can have a trainable scale.

A configuration selects a backend and holds its settings. Pass it to `Circuit(provider=...)`, or register a name for it. `NumpyConfig` is documented below, and the other configurations are documented with their backends:

- `NumpyConfig`: the reference simulator, which needs no extra dependencies (`provider="numpy"`).
- PennyLaneConfig
- QiskitConfig
- QiskitAerConfig: the Qiskit Aer simulator, with the statevector and the matrix product state methods.
- QiskitRuntimeConfig
- IQMConfig
- BraketConfig
- CudaQuantumConfig
- TensorCircuitConfig
- QulacsConfig
- QsimConfig

The blocks and `NumpyConfig` are imported with this module, because they only need NumPy. The other configurations are imported when they are first used, so importing this module does not import PyTorch, PennyLane, Qiskit or Braket.

The quantities to measure are set with `Circuit(observables=...)`. They are not a block.

Example

```python
import numpy as np
from eigenkit import Circuit, all_z
from eigenkit.configs import Encoding, Variational, NumpyConfig

circuit = Circuit(
    blocks=[Encoding(in_features=3), Variational(depth=2)],
    observables=all_z(3),
    provider=NumpyConfig(),
)

print(circuit.spec.n_qubits, circuit.spec.n_weights)  # 3 6
print(circuit(np.zeros(circuit.spec.n_weights), np.zeros(3)))  # [1. 1. 1.]
```

To change the backend, you change one argument. `provider=PennyLaneConfig(...)` runs the same blocks on PennyLane, and `provider="numpy"` selects the same reference simulator by its registered name.

### `Encoding`

Encode `in_features` data values as single-qubit rotations.

Feature `i` sets the angle of one rotation on qubit `i`, so the block uses the qubits `0` to `in_features - 1`. This matters only when the block is combined with others. A circuit has as many qubits as its widest block, and when another block in the list needs more qubits, this block leaves the extra qubits unchanged.

The block takes `in_features` inputs and no weights.

Parameters:

| Name          | Type       | Description                                                                                                                                                                  | Default    |
| ------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `in_features` | `int`      | The number of features to encode, one per qubit. It must be positive, and a ValueError is raised otherwise.                                                                  | *required* |
| `rotation`    | `GateName` | The rotation gate: "RX", "RY" or "RZ". Default: "RY".                                                                                                                        | `'RY'`     |
| `scale`       | `float`    | A constant factor applied to every input angle. It is useful when the data is normalised to [0, 1] and the circuit needs angles in [0, 2π]. It must be finite. Default: 1.0. | `1.0`      |

#### `build(n_qubits)`

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

The qubits with an index of `in_features` or more are left unchanged.

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

#### `required_qubits()`

Return `in_features`, because the block uses one qubit per 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. |

### `NumpyConfig`

Configuration for the reference NumPy simulator.

It has no settings yet. It exists so that `Circuit(provider=NumpyConfig())` works in the same way as with the configurations of the other backends.

#### `make_backend()`

Build the backend that this configuration describes.

The provider layer calls it when a configuration is passed as `provider=`. There is no reason to call it directly.

Returns:

| Type               | Description                                |
| ------------------ | ------------------------------------------ |
| `'NumpySimulator'` | The NumpySimulator for this configuration. |

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

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

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