> ## Documentation Index
> Fetch the complete documentation index at: https://docs.classiq.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Block Encoding

Quantum computers natively implement unitary operations. Many real-world problems, however,
are naturally expressed in terms of matrices that are not unitary.
For instance, a correlation matrix between assets in a financial portfolio,
or a differential operator discretizing a partial differential equation in engineering design.
To process such a matrix on a quantum computer, it must first be embedded into a larger unitary matrix acting on
additional qubits. This embedding procedure is known as block encoding.

Given a matrix $A$, block encoding it means embedding it into the top-left block of a larger
unitary $U$: applying $U$ to $\vert 0 \rangle^{\otimes b} \vert \psi \rangle$ and post-selecting
the first $b$ qubits on $\vert 0 \rangle$ recovers $A \vert \psi \rangle$ up to a scaling factor.

Formally, a unitary $U$ is an $(\alpha, b, \epsilon)$-block-encoding of $A$ if

$\left\lVert A - \alpha \left(\langle 0 \vert^{\otimes b} \otimes I\right) U \left(\vert 0 \rangle^{\otimes b} \otimes I\right) \right\rVert \le \epsilon,$

where $\alpha$ is a normalization factor, $b$ is the number of extra ("block") qubits, and
$\epsilon$ is the approximation error (0 for an exact encoding).

Classiq's `BlockEncoding` class represents $U$ as a qfunc, together with the $(\alpha, b, \epsilon)$
triple, as a single Python object. It also provides constructors and algebraic operations so you
can build and combine block encodings without hand-managing quantum variables.

## The `BlockEncoding` class

`BlockEncoding` is a frozen dataclass with the following fields:

| Field | Description |
| - | - |
| `unitary` | The qfunc implementing $U$. Called as `unitary(data, block)` when `block_size > 0`, or `unitary(data)` when `block_size == 0`. |
| `block_size` | Number of block qubits $b$. |
| `alpha` | The scaling factor $\alpha$. |
| `data_size` | Number of qubits the encoded matrix $A$ acts on. |
| `epsilon` | Approximation error $\epsilon$ (default `0.0`, i.e. exact). |
| `hermitian_be` | Whether $U$ itself (not just $A$) is Hermitian. Required by [`qubitize`](/user-guide/applications/block-encoding/composing-block-encodings#qubitize). |

You rarely construct a `BlockEncoding` by filling in these fields directly. Instead, use one of
its classmethod constructors — covered in
[Constructing Block Encodings](/user-guide/applications/block-encoding/constructing-block-encodings)
— to build one from a NumPy matrix, a set of diagonals, a sum of Pauli operators, and more.

## Quick start

`be.unitary` is a qfunc like any other, so you call it directly on your own variables inside
`main`. The following example block-encodes a diagonal matrix $A$, loads a non-uniform vector
`x_vector` onto the `data` variable, applies the block encoding,
synthesizes and executes the resulting circuit. Filtering the state vector on `block == 0`
recovers $(A / \alpha) \cdot \text{x\_vector}$, which is the block-encoded action of $A$ on the input, up
to the scaling factor $\alpha$:

```python theme={null}
import numpy as np
from classiq import *
from classiq.applications.block_encoding import BlockEncoding

diag_values = [4.0, 8.0, 6.0, 2.0]
be = BlockEncoding.from_dense_diagonals([(diag_values, 0)])
block_size = be.block_size

x_vector = [0.2, 0.4, 0.4, 0.8]


@qfunc
def main(data: Output[QNum], block: Output[QNum[block_size]]) -> None:
    prepare_amplitudes(x_vector, 0.0, data)
    allocate(block)
    be.unitary(data, block)


qprog = synthesize(main)

result = (
    calculate_state_vector(qprog, filters={"block": 0})
    .sort_values("data")
    .reset_index(drop=True)
)
print(result[["data", "amplitude"]])

# The amplitudes equal (A @ x_vector) / alpha, up to phase. Comparing norms sidesteps
# that and recovers alpha directly:
expected = np.diag(diag_values) @ np.array(x_vector)
result_vec = result["amplitude"].to_numpy()
print(np.linalg.norm(expected) / np.linalg.norm(result_vec))  # 8.0 == be.alpha
```

The first `print` gives:

| data | amplitude |
| - | - |
| 0 | 0.1-0.0j |
| 1 | 0.4-0.0j |
| 2 | 0.3-0.0j |
| 3 | 0.2-0.0j |

And the second gives `8.0`, matching `be.alpha`.

See [State Vector Filtering](/user-guide/execution/state-vector-filtering) for more on
`calculate_state_vector` and filtering by variable.

## Where to go next

* [Constructing Block Encodings](/user-guide/applications/block-encoding/constructing-block-encodings) —
  build a `BlockEncoding` from a matrix, diagonals, or a Pauli operator.
* [Composing and Transforming Block Encodings](/user-guide/applications/block-encoding/composing-block-encodings) —
  combine block encodings with `+`, `*`, `@`, `weighted_sum`, and `product`, and transform them
  with `inverse` and `qubitize`.
