# Observables

What to measure is set with the `observables=` argument of `Circuit`. It is not a special block. There are three ways to build an observable, each suited to a different level of detail.

## Default: `Z` on every qubit

If you do not pass `observables=`, the circuit measures `Z` on every qubit:

```python
from eigenkit import Circuit, Encoding, Variational

circuit = Circuit(
    blocks=[Encoding(in_features=4), Variational(depth=2)],
    # observables= defaults to all-Z; output is shape (..., n_qubits).
)
```

## Typed constructors

`Observable.x(qubit)`, `Observable.y(qubit)` and `Observable.z(qubit)` build observables made of a single Pauli operator:

```python
from eigenkit import Observable, all_z

# Measure Z on every qubit.
observables = all_z(4)

# Measure X on qubit 0 only.
observables = [Observable.x(0)]
```

## Pauli algebra

The `X`, `Y` and `Z` constructors return a `PauliExpr`, which you can combine with arithmetic. Use this to write Hamiltonians:

```python
from eigenkit import Circuit, Encoding, Variational, X, Y, Z

H = 0.7 * Z(0) + 0.3 * X(1) * Z(2) - 0.5 * Y(0) * Y(1)

# H acts on qubits 0-2, so the circuit needs at least three wires.
circuit = Circuit(
    blocks=[Encoding(in_features=3), Variational(depth=2)],
    observables=H,
)
```

A `PauliExpr` passed as a single observable gives a scalar output, which is one expectation value. Pass a list of `PauliExpr` to get a vector output.

A measured observable holds only Pauli terms. So an expression with a constant term, such as the cost of an edge in MaxCut, `0.5 * (1 - Z(0) * Z(1))`, raises a `ValueError`, instead of losing the constant without an error. Measure the Pauli part, and add the constant back:

```python
cost = 0.5 * (1 - Z(0) * Z(1))
edge = Circuit(
    blocks=[Encoding(in_features=2), Variational(depth=1)],
    observables=cost - cost.constant,
)
value = edge(edge.spec.n_weights * [0.1], [0.3, 0.4]) + cost.constant
```

## Text format

For quick experiments, you can write the observable as a string:

```python
from eigenkit import from_string

H = from_string("0.5 Z0 X1 + 0.3 Y2")
# Equivalent to 0.5 * Z(0) * X(1) + 0.3 * Y(2)
```

## Output shape

The shape of the output depends on what you pass, **and on the number of dimensions of the input**. A 1-D `inputs` is one sample and has no batch axis. A 2-D input, of shape `(batch, n_inputs)`, has one.

| `observables=`                        | 1-D input      | 2-D `(batch, n_inputs)` |
| ------------------------------------- | -------------- | ----------------------- |
| `None`                                | `(n_qubits,)`  | `(batch, n_qubits)`     |
| A single `Observable` or `PauliExpr`  | `()`, a scalar | `(batch,)`              |
| A list of `Observable` or `PauliExpr` | `(len(list),)` | `(batch, len(list))`    |

These shapes were measured on a circuit with four qubits and a batch of shape `(32, 4)`. In both cases, `weights` stays 1-D, because the weights are shared across the batch.

## Measurements that are not Pauli operators

For observables based on fidelity (quantum kernels, swap tests), use `circuit.zero_state_probability(weights, inputs)` instead. It returns the probability of measuring the all-zeros state, without going through the Pauli machinery. See [Quantum Kernels](https://docs.kit.eigenorbit.com/user-guide/kernels/index.md).

## API reference

### `eigenkit.core.ir.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* |

### `eigenkit.core.observables`

Functions and classes that make observables easy to write.

There are three ways to choose what a circuit measures:

```python
# Pauli algebra
observable = Z(0) * X(1) + 0.5 * Y(2)

# class methods, or a string
observable = Observable.from_string("Z0 X1 + 0.5 Y2")
observable = Observable.z(0)

# no observable at all: by default, Circuit measures Z on every qubit
```

`PauliExpr` is a simple builder. It collects terms, each made of a coefficient and a tensor product of Pauli operators, and it converts them to an `Observable` when needed. A product of two operators on the same qubit is simplified when the two operators are equal (`P * P = I`). A product of different Pauli operators on the same qubit (`X * Y = iZ` and so on) needs complex arithmetic and is not supported.

#### `PauliExpr`

A sum of terms, each a coefficient times a product of Pauli operators.

In each term, the Pauli operators act on distinct qubits, so the product is a tensor product.

##### `constant`

The coefficient of the identity term, or `0.0` when there is none.

##### `__init__(terms)`

Build an expression from raw terms.

Parameters:

| Name    | Type              | Description                                                                                                                                                                                | Default    |
| ------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- |
| `terms` | `Iterable[_Term]` | Internal records of the terms. Prefer the X, Y and Z functions and ordinary arithmetic, such as 0.5 * Z(0) + X(1). They build the same expression without creating these records directly. | *required* |

##### `single(operator, qubit, coeff=1.0)`

Return `coeff` times a single Pauli `operator` on `qubit`.

Parameters:

| Name       | Type    | Description                          | Default    |
| ---------- | ------- | ------------------------------------ | ---------- |
| `operator` | `str`   | The Pauli operator: "X", "Y" or "Z". | *required* |
| `qubit`    | `int`   | The index of the qubit.              | *required* |
| `coeff`    | `float` | The coefficient. Default: 1.0.       | `1.0`      |

Returns:

| Type          | Description                              |
| ------------- | ---------------------------------------- |
| `'PauliExpr'` | The expression, which has a single term. |

##### `to_observable()`

Convert the expression to an `Observable`.

The terms with the same Pauli string are combined, and the terms whose coefficient is `0` (at most `1e-15` in magnitude) are dropped.

An `Observable` holds only Pauli terms, so a non-zero identity term raises an error. Such a term comes from a scalar offset, or from `P * P = I`. Dropping it without an error would shift every expectation value. To measure such an expression, measure `expr - expr.constant` and add `expr.constant` to the result. The chemistry entry points take this constant as `offset`.

Returns:

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

Raises:

| Type         | Description                                                                                      |
| ------------ | ------------------------------------------------------------------------------------------------ |
| `ValueError` | If the expression has a constant term that is not 0, or if no term that can be measured is left. |

#### `X(qubit)`

Return Pauli `X` on `qubit`, as an expression that can be added and scaled.

Parameters:

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

Returns:

| Type        | Description     |
| ----------- | --------------- |
| `PauliExpr` | The expression. |

#### `Y(qubit)`

Return Pauli `Y` on `qubit`, as an expression that can be added and scaled.

Parameters:

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

Returns:

| Type        | Description     |
| ----------- | --------------- |
| `PauliExpr` | The expression. |

#### `Z(qubit)`

Return Pauli `Z` on `qubit`, as an expression that can be added and scaled.

Parameters:

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

Returns:

| Type        | Description     |
| ----------- | --------------- |
| `PauliExpr` | The expression. |

#### `all_z(n_qubits)`

Return one `Z` observable for each qubit.

This is useful as a default measurement.

Parameters:

| Name       | Type  | Description                          | Default    |
| ---------- | ----- | ------------------------------------ | ---------- |
| `n_qubits` | `int` | The number of qubits of the circuit. | *required* |

Returns:

| Type               | Description                    |
| ------------------ | ------------------------------ |
| `list[Observable]` | One Observable for each qubit. |

#### `from_string(text)`

Parse an observable from a string.

This function is a shortcut for `Observable.from_string`.

Parameters:

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

Returns:

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