# Encoding

The `Encoding` block maps classical features to quantum states, with one single-qubit rotation per feature.

```python
from eigenkit import Encoding

encoding = Encoding(in_features=4, rotation="RX")
```

## Parameters

| Parameter     | Type    | Default  | Description                                                                                                                                           |
| ------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `in_features` | `int`   | Required | The number of features to encode, one per qubit. It must be positive.                                                                                 |
| `rotation`    | `"RX"`  | `"RY"`   | `"RZ"`                                                                                                                                                |
| `scale`       | `float` | `1.0`    | A factor that multiplies every input angle. It is useful when the data is normalised to `[0, 1]` and you want angles in `[0, 2π]`. It must be finite. |

The arguments are checked when the block is created

A non-positive `in_features` is rejected immediately: `Encoding(in_features=0)` raises `ValueError: in_features must be positive, got 0`. A `rotation` other than `RX`, `RY` or `RZ`, and a `scale` that is not finite, are rejected in the same way. Every block in the library checks its arguments like this.

`Encoding` is the name exported from the top level and from `eigenkit.configs`. The class itself is AngleEncoding, which is the name that appears in tracebacks and in the heading of the API reference below.

## Methods

| Method              | What it does                                                                                                                                   |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `required_qubits()` | Returns the number of qubits that the block needs, which is `in_features`.                                                                     |
| `build(n_qubits)`   | Builds the gates of the block for a circuit with `n_qubits` qubits, as an `IRFragment`. `assemble()` calls it, so you rarely call it yourself. |
| `to_dict()`         | Returns a description of the block that can be saved as JSON, including a `type` key.                                                          |
| `from_dict(d)`      | A class method that rebuilds the block from the output of `to_dict()`.                                                                         |

The `.eigenkit` bundle format relies on `to_dict()` and `from_dict()` to save and load blocks. See [Model persistence](https://docs.kit.eigenorbit.com/user-guide/model-persistence/index.md).

## How the number of qubits is found

The number of qubits comes from `Encoding.in_features`. Every block reports the number of qubits that it needs through `required_qubits()`. `assemble()` combines a list of blocks into one `CircuitSpec`: it takes the largest of these numbers, and it concatenates the weights and the inputs of the blocks in the order of the blocks. `Circuit` calls `assemble()` for you when you pass `blocks=[...]`. See [Circuit Builder](https://docs.kit.eigenorbit.com/user-guide/circuit-builder/index.md) to work with a `CircuitSpec` directly.

```python
encoding = Encoding(in_features=6, rotation="RZ")
# This circuit uses 6 qubits.
```

## Rotation gates

- `"RX"` applies `RX(feature_value)` to each qubit.
- `"RY"` applies `RY(feature_value)` to each qubit.
- `"RZ"` applies `RZ(feature_value)` to each qubit.

For data in a bounded range that does not cover `[0, 2π]`, use the `scale` argument, or normalise the data first with `MinMaxNorm`:

```python
from eigenkit import MinMaxNorm
import torch

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

## API reference

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