## `eigenkit.core.ir`

The intermediate representation (IR) of quantum circuits.

The IR is plain data and does not depend on any third-party package. Every object is a frozen dataclass, so circuits can be hashed and pickled, and they can be shared safely between threads or processes.

The parameter expressions (`Scale`, `Negate`, `Sum` and `Multiply`) let encoding blocks write angles such as `RY(2π * x)` or `RY(λ * x)`, so the user does not have to scale the inputs beforehand. They also let transforms such as `adjoint` work on symbolic references instead of fixed numbers.

Example

```python
from eigenkit import Encoding, Variational
from eigenkit.core import assemble
from eigenkit.core.ir import Observable, adjoint

spec = assemble([Encoding(in_features=2), Variational(depth=1)])

print(spec.n_qubits, len(spec.gates))       # 2 5
print(spec.gates[0].name, spec.gates[0].qubits)  # RY (0,)

# adjoint reverses the gate order and negates every angle
print(adjoint(spec).gates[0].name)          # CNOT

# an observable can be parsed from a sum of Pauli terms written as a string
print(Observable.from_string("0.5 Z0 X1").terms[0].coeff)  # 0.5
```

### `CircuitSpec`

A whole circuit as data.

It holds the list of gates, the number of qubits, and the lengths of the weight and input vectors that the gates refer to. Every provider compiles this object, and `assemble` builds it from a list of blocks. It holds no weights and no observables: the weights are passed at each call, and the observables are set by the `Circuit` that wraps the spec.

Attributes:

| Name        | Type               | Description                                                                |
| ----------- | ------------------ | -------------------------------------------------------------------------- |
| `n_qubits`  | `int`              | The number of qubits of the circuit.                                       |
| `n_weights` | `int`              | The length of the weight vector that the gates refer to through WeightRef. |
| `n_inputs`  | `int`              | The length of the input vector that the gates refer to through InputRef.   |
| `gates`     | `tuple[Gate, ...]` | The gates, in the order in which they are applied.                         |

### `Constant`

Bases: `_ParamOps`

A fixed angle, used where a block needs a rotation by a set amount.

Attributes:

| Name    | Type    | Description            |
| ------- | ------- | ---------------------- |
| `value` | `float` | The angle, in radians. |

### `Gate`

One gate of a circuit: which gate it is, where it acts, and its angles.

Attributes:

| Name      | Type                    | Description                                                                                                                                                                                                                                                                                                                              |
| --------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`    | `GateName`              | The name of the gate, from the fixed list in GateName.                                                                                                                                                                                                                                                                                   |
| `qubits`  | `tuple[int, ...]`       | The indices of the qubits that the gate acts on, in the order that the gate expects (the controls before the targets).                                                                                                                                                                                                                   |
| `params`  | `tuple[Parameter, ...]` | The angle expressions, one for each rotation parameter. Each one is a Constant, a WeightRef, an InputRef, or an arithmetic combination of them.                                                                                                                                                                                          |
| `data`    | `tuple`                 | The constant arrays of the gates that carry data (AmplitudeEncoding, QubitUnitary and ControlledQubitUnitary). They are stored as nested tuples so that the gate can still be hashed. The field is empty for every other gate. \_array_to_data and \_data_to_array convert between arrays and this form.                                 |
| `adjoint` | `bool`                  | If True, the conjugate transpose of the gate is applied instead of the gate itself. It is used only for AmplitudeEncoding gates, inside Grover iterates. The adjoint of a QubitUnitary is computed in the intermediate representation instead, by taking the conjugate transpose of the stored matrix, so this flag is never set for it. |

### `InputRef`

Bases: `_ParamOps`

A placeholder for the input feature at `index` in the input vector.

Attributes:

| Name    | Type  | Description                                                                                                      |
| ------- | ----- | ---------------------------------------------------------------------------------------------------------------- |
| `index` | `int` | The position in the input vector of the circuit. assemble shifts it in the same way as the indices of WeightRef. |

### `Multiply`

Bases: `_ParamOps`

The product of two parameter expressions.

It is used when a gate angle is the product of two symbolic terms. The most common case is a trainable scaling of a feature (Schuld et al. 2021): `RY(λ * x)`, where `λ` is a `WeightRef` and `x` is an `InputRef`. When one of the two factors is a constant number, use `Scale` instead, so that backends can see the number directly.

Backends that do not support the product of two symbolic terms raise `NotImplementedError` when this node appears. Currently, this is the case of the CUDA Quantum backend.

Attributes:

| Name | Type          | Description       |
| ---- | ------------- | ----------------- |
| `a`  | `'Parameter'` | The left factor.  |
| `b`  | `'Parameter'` | The right factor. |

### `Negate`

Bases: `_ParamOps`

The negation of another expression: `-expression`.

The `adjoint` transform builds this node when it reverses a rotation. When the angle is a `Constant` or a `Scale`, it changes the sign of the number instead, and when the angle is already a `Negate`, it removes the negation.

Attributes:

| Name         | Type          | Description               |
| ------------ | ------------- | ------------------------- |
| `expression` | `'Parameter'` | The expression to negate. |

### `Observable`

A Hermitian operator to measure, written as a sum of Pauli terms.

You can build one with the `z`, `x` and `y` methods for a single Pauli operator on one qubit, with `from_string` for a whole sum, or with the `X`, `Y` and `Z` functions of `eigenkit.core.observables`, which support arithmetic.

Attributes:

| Name    | Type                    | Description                                                                                                                                                                                                                                                             |
| ------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `terms` | `tuple[PauliTerm, ...]` | The Pauli terms of the sum. It can be empty: jordan_wigner returns an empty observable, together with an offset, when the operator reduces to a multiple of the identity. However, Circuit refuses to measure an empty observable, because there is nothing to measure. |

#### `from_string(text)`

Parse a sum of Pauli terms written as a string.

Examples of such strings are `"0.5 Z0 X1 + 0.3 Y2"` and `"-Z0 + Z1"`.

The grammar is as follows. The terms are joined by `+` or `-`. Each term starts with an optional numeric coefficient, in plain or scientific notation (`1e-3` is accepted). The coefficient is followed by an optional `*` and by one or more `[XYZ]<qubit>` operators on distinct qubits. Any amount of whitespace is accepted, including none.

Every part of the string must be a valid term, and every term must contain Pauli operators. A constant term (`"0.5 + Z0"`, `"0.5 I"`) raises an error instead of being dropped from the sum: an `Observable` holds only Pauli terms, and the constant must be kept next to it as an offset.

Parameters:

| Name   | Type  | Description          | Default    |
| ------ | ----- | -------------------- | ---------- |
| `text` | `str` | The string to parse. | *required* |

Returns:

| Type           | Description            |
| -------------- | ---------------------- |
| `'Observable'` | The parsed observable. |

Raises:

| Type         | Description                                                                                                                    |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------ |
| `ValueError` | If text is empty, if it contains a constant term, if a part of it is not a valid term, or if a term uses the same qubit twice. |

#### `x(qubit)`

Return the observable `X` on the single qubit `qubit`.

Parameters:

| Name    | Type  | Description             | Default    |
| ------- | ----- | ----------------------- | ---------- |
| `qubit` | `int` | The index of the qubit. | *required* |

#### `y(qubit)`

Return the observable `Y` on the single qubit `qubit`.

Parameters:

| Name    | Type  | Description             | Default    |
| ------- | ----- | ----------------------- | ---------- |
| `qubit` | `int` | The index of the qubit. | *required* |

#### `z(qubit)`

Return the observable `Z` on the single qubit `qubit`.

Parameters:

| Name    | Type  | Description             | Default    |
| ------- | ----- | ----------------------- | ---------- |
| `qubit` | `int` | The index of the qubit. | *required* |

### `PauliTerm`

A coefficient times a product of Pauli operators on distinct qubits.

`PauliTerm(0.5, (("Z", 0), ("X", 1)))` is the term `0.5 Z0 X1`.

Attributes:

| Name     | Type                          | Description                                                                                                                                                                                                                                                                    |
| -------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `coeff`  | `float`                       | The real coefficient. Complex values are rejected, because an Observable represents only Hermitian operators.                                                                                                                                                                  |
| `paulis` | `tuple[tuple[str, int], ...]` | Pairs of a Pauli letter ("X", "Y" or "Z") and a qubit index. It must not be empty, because a term without Pauli operators is a constant, which an Observable does not hold. Each qubit can appear at most once, so reduce products such as Z * Z = I before building the term. |

Raises:

| Type         | Description                                                                                   |
| ------------ | --------------------------------------------------------------------------------------------- |
| `TypeError`  | If coeff is complex.                                                                          |
| `ValueError` | If paulis is empty, if a Pauli letter is not X, Y or Z, or if a qubit appears more than once. |

### `Scale`

Bases: `_ParamOps`

A constant multiple of another expression: `factor * expression`.

Use it instead of `Multiply` when one of the two factors is a plain number. A backend can then include the constant factor in the gate angle, instead of treating it as a second symbolic operand.

Attributes:

| Name         | Type          | Description              |
| ------------ | ------------- | ------------------------ |
| `expression` | `'Parameter'` | The expression to scale. |
| `factor`     | `float`       | The constant factor.     |

### `Sum`

Bases: `_ParamOps`

The sum of several expressions.

Attributes:

| Name          | Type                      | Description                                                                                                                                                              |
| ------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `expressions` | `tuple['Parameter', ...]` | The terms to add. There must be at least one. An empty Sum is not evaluated as 0: resolve and the backends start from the first term, so they raise an IndexError on it. |

### `WeightRef`

Bases: `_ParamOps`

A placeholder for the trainable weight at `index` in the weight vector.

Attributes:

| Name    | Type  | Description                                                                                                                                                                             |
| ------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `index` | `int` | The position in the weight vector of the circuit. Each block numbers the weights of its own fragment from 0, and assemble shifts these indices to their positions in the whole circuit. |

### `adjoint(circuit)`

Reverse the order of the gates and invert each gate.

The transform does not depend on the provider. It returns another `CircuitSpec` that any backend can compile, so the transform does not have to be written again for each backend.

Each type of gate is inverted as follows:

- **Rotations with parameters** (`RX`, `RY`, `RZ`, `PhaseShift`, `CRY`, `CPHASE`): the angle parameters are negated.
- **Gates without parameters** (`H`, `X`, `Z`, `CNOT`, `SWAP`, `MCZ`, `MCX`): they are copied unchanged, because applying them twice gives the identity. `S` and `Sdg` are not their own inverse, so each one is replaced by the other.
- **`QubitUnitary(U)`**: the stored matrix is replaced by its conjugate transpose `U†`. It is computed without NumPy, so that the intermediate representation stays pure Python.
- **`ControlledQubitUnitary(U)`**: the target unitary (`data[0]`) is replaced by its conjugate transpose, and the control values (`data[1]`) are kept unchanged.
- **`AmplitudeEncoding`**: the transform flips `gate.adjoint` and leaves the inversion to the backend. PennyLane, for example, applies the conjugate transpose of the state-preparation unitary. The Qiskit backend does not support this adjoint: `Circuit` rejects such a circuit with a `ValueError`, and the backend itself raises `NotImplementedError`.

Parameters:

| Name      | Type          | Description            | Default    |
| --------- | ------------- | ---------------------- | ---------- |
| `circuit` | `CircuitSpec` | The circuit to invert. | *required* |

Returns:

| Type          | Description                                            |
| ------------- | ------------------------------------------------------ |
| `CircuitSpec` | The inverted circuit, with the gates in reverse order. |

### `chain_factor(parameter, weight_index, weights, inputs)`

Compute the partial derivative of `parameter` with respect to one weight.

The derivative with respect to `weights[weight_index]` is evaluated numerically at the given `weights` and `inputs`. The parameter-shift Jacobian that applies the chain rule uses it to turn the gradient with respect to a gate angle into its contribution to the gradient with respect to one weight.

Parameters:

| Name           | Type        | Description                                               | Default                                                                                                       |
| -------------- | ----------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `parameter`    | `Parameter` | The angle expression to differentiate.                    | *required*                                                                                                    |
| `weight_index` | `int`       | The index of the weight to differentiate with respect to. | *required*                                                                                                    |
| `weights`      | \`ndarray   | Sequence[float]\`                                         | The weight vector. It can be any container that supports indexing: a NumPy array, a PyTorch tensor or a list. |
| `inputs`       | \`ndarray   | Sequence[float]\`                                         | The input vector. It can be any container that weights can be.                                                |

Returns:

| Type    | Description                                                                                                                                                                                                                             |
| ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `float` | The partial derivative. For a single sample, it is a float. When inputs is a batch, transposed so that inputs[j] holds feature j of every sample, and the expression contains a Multiply, it can be an array with one value per sample. |

### `chain_factor_input(parameter, input_index, weights, inputs)`

Compute the partial derivative of `parameter` with respect to one input.

The derivative with respect to `inputs[input_index]` is evaluated numerically at the given `weights` and `inputs`. This function is the counterpart of `chain_factor` for the inputs.

Parameters:

| Name          | Type        | Description                                              | Default                                                                                                       |
| ------------- | ----------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `parameter`   | `Parameter` | The angle expression to differentiate.                   | *required*                                                                                                    |
| `input_index` | `int`       | The index of the input to differentiate with respect to. | *required*                                                                                                    |
| `weights`     | \`ndarray   | Sequence[float]\`                                        | The weight vector. It can be any container that supports indexing: a NumPy array, a PyTorch tensor or a list. |
| `inputs`      | \`ndarray   | Sequence[float]\`                                        | The input vector. It can be any container that weights can be.                                                |

Returns:

| Type    | Description                                                                                                                                                                                                                             |
| ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `float` | The partial derivative. For a single sample, it is a float. When inputs is a batch, transposed so that inputs[j] holds feature j of every sample, and the expression contains a Multiply, it can be an array with one value per sample. |

### `collect_input_dependencies(parameter)`

Return the set of input indices that a parameter expression depends on.

This function is the counterpart of `collect_weight_dependencies` for the inputs. The parameter-shift Jacobian that applies the chain rule uses it to find the gates that contribute to the gradient with respect to a given input.

Parameters:

| Name        | Type        | Description                      | Default    |
| ----------- | ----------- | -------------------------------- | ---------- |
| `parameter` | `Parameter` | The angle expression to inspect. | *required* |

Returns:

| Type       | Description                                          |
| ---------- | ---------------------------------------------------- |
| `set[int]` | The indices of the inputs that the expression reads. |

### `collect_weight_dependencies(parameter)`

Return the set of weight indices that a parameter expression depends on.

Parameters:

| Name        | Type        | Description                      | Default    |
| ----------- | ----------- | -------------------------------- | ---------- |
| `parameter` | `Parameter` | The angle expression to inspect. | *required* |

Returns:

| Type       | Description                                           |
| ---------- | ----------------------------------------------------- |
| `set[int]` | The indices of the weights that the expression reads. |

### `find_input_shift_unsafe_gates(circuit)`

Return the gates whose input angle needs the chain-rule path.

A gate is included when its angle wraps an `InputRef` in an arithmetic node, or when it uses an input that another angle also uses. `Reuploading` does this when it encodes each feature once per layer.

An arithmetic node is a `Scale`, `Multiply`, `Sum` or `Negate`. This function is the counterpart of `find_parameter_shift_unsafe_gates` for the inputs. When the list is empty, the fast path, which shifts each input directly, can be used. Otherwise, the gradients are correct only with the chain-rule path, which shifts single gate angles with `shift_gate_angle`.

Parameters:

| Name      | Type          | Description             | Default    |
| --------- | ------------- | ----------------------- | ---------- |
| `circuit` | `CircuitSpec` | The circuit to inspect. | *required* |

Returns:

| Type           | Description                              |
| -------------- | ---------------------------------------- |
| `list['Gate']` | The gates that need the chain-rule path. |

### `find_parameter_shift_unsafe_gates(circuit)`

Return the gates whose weight angle needs the chain-rule path.

A gate is included when its angle wraps a `WeightRef` in an arithmetic node, which is a `Scale`, `Multiply`, `Sum` or `Negate`, or when it uses a weight that another angle also uses. `Circuit` reads this list to choose how to compute gradients. A circuit without such gates takes the fast path, which applies the parameter-shift rule directly to each weight. A circuit with such gates takes the chain-rule path. Two examples are `Reuploading(trainable_scale=True)`, which builds `Multiply(weight, input)`, and a builder circuit that uses one weight in two gates.

Parameters:

| Name      | Type          | Description             | Default    |
| --------- | ------------- | ----------------------- | ---------- |
| `circuit` | `CircuitSpec` | The circuit to inspect. | *required* |

Returns:

| Type           | Description                              |
| -------------- | ---------------------------------------- |
| `list['Gate']` | The gates that need the chain-rule path. |

### `inputs_needing_four_term_shift(circuit)`

Return the inputs that the two-term parameter-shift rule cannot differentiate.

This function is the counterpart of `weights_needing_four_term_shift` for the inputs.

Parameters:

| Name      | Type          | Description             | Default    |
| --------- | ------------- | ----------------------- | ---------- |
| `circuit` | `CircuitSpec` | The circuit to analyse. | *required* |

Returns:

| Type             | Description                                             |
| ---------------- | ------------------------------------------------------- |
| `frozenset[int]` | The indices of the inputs that need the four-term rule. |

### `negate_parameter(parameter)`

Negate a parameter expression of any kind, symbolically.

Parameters:

| Name        | Type        | Description                     | Default    |
| ----------- | ----------- | ------------------------------- | ---------- |
| `parameter` | `Parameter` | The angle expression to negate. | *required* |

Returns:

| Type        | Description             |
| ----------- | ----------------------- |
| `Parameter` | The negated expression. |

### `resolve(parameter, weights, inputs)`

Compute the numeric value of a parameter expression, recursively.

`weights` and `inputs` can be any container that supports indexing (a NumPy array, a PyTorch tensor or a list). The type of the result follows the type of these containers, so the same function serves both the NumPy and the PyTorch evaluation paths.

Parameters:

| Name        | Type        | Description                       | Default                                                                                                       |
| ----------- | ----------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `parameter` | `Parameter` | The angle expression to evaluate. | *required*                                                                                                    |
| `weights`   | \`ndarray   | Sequence[float]\`                 | The weight vector. It can be any container that supports indexing: a NumPy array, a PyTorch tensor or a list. |
| `inputs`    | \`ndarray   | Sequence[float]\`                 | The input vector. It can be any container that weights can be.                                                |

Returns:

| Type    | Description        |
| ------- | ------------------ |
| `float` | The numeric angle. |

### `shift_gate_angle(circuit, gate_index, shift_value)`

Return a copy of the circuit in which the angle of one gate is shifted.

In the new `CircuitSpec`, the first parameter of the gate at `gate_index` is replaced by `Sum((original, Constant(shift_value)))`. The parameter-shift Jacobian that applies the chain rule uses it to shift the angle of one gate, without changing the weights or the other gates. The shifts are `±π/2`, and also `±3π/2` for the gates in `MULTI_FREQUENCY_GATES`.

Parameters:

| Name          | Type          | Description                                       | Default    |
| ------------- | ------------- | ------------------------------------------------- | ---------- |
| `circuit`     | `CircuitSpec` | The circuit to shift.                             | *required* |
| `gate_index`  | `int`         | The position of the gate in circuit.gates.        | *required* |
| `shift_value` | `float`       | The amount added to the first angle of that gate. | *required* |

Returns:

| Type            | Description                                           |
| --------------- | ----------------------------------------------------- |
| `'CircuitSpec'` | A new CircuitSpec in which only that gate is shifted. |

### `weights_needing_four_term_shift(circuit)`

Return the weights that the two-term parameter-shift rule cannot differentiate.

A weight is included when one of the gates that it feeds is in `MULTI_FREQUENCY_GATES`. A weight that feeds more than one gate never uses this direct shift: the circuit takes the chain-rule path, which shifts each gate on its own (see `find_parameter_shift_unsafe_gates`).

Parameters:

| Name      | Type          | Description             | Default    |
| --------- | ------------- | ----------------------- | ---------- |
| `circuit` | `CircuitSpec` | The circuit to analyse. | *required* |

Returns:

| Type             | Description                                              |
| ---------------- | -------------------------------------------------------- |
| `frozenset[int]` | The indices of the weights that need the four-term rule. |
