## `eigenkit.core.backends`

Registration of the backends.

The NumPy reference simulator only needs NumPy, and it is registered when this package is imported, because it is the default and is always available. The other backends, apart from CUDA Quantum, are registered *lazily*: naming one imports nothing, and its software development kit (PyTorch, PennyLane, Qiskit or Braket) is imported only when a circuit first uses that provider. This keeps `import eigenkit` and `import eigenkit.core` free of these large dependencies.

`"pennylane"` is always registered lazily. `"qiskit"`, `"braket"` and the other optional backends are registered only when their extra is installed. `cudaq` is not registered automatically: once it is installed, register it explicitly:

```python
from eigenkit import CudaQuantumBackend, register

register("cudaq", CudaQuantumBackend())
```

The backend classes and the configuration dataclasses are re-exported lazily through `__getattr__`. For example, `from eigenkit.core import PennyLaneConfig` imports the PennyLane adapter only when the name is accessed.

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

### `NumpySimulator`

The provider for the reference NumPy statevector simulator.

It is registered automatically as `"numpy"`. It uses only NumPy and no optional dependency, so it is always available.

It declares `gradient_method = "adjoint"`, so the gradient code of the core package uses NumpyCompiled.jacobian() (analytic, in reverse mode, with one forward pass and one backward pass whatever the number of parameters) instead of the parameter-shift rule, whose cost is `O(n_params)`.

#### `capabilities`

What this backend supports.

It runs every gate of the library, evaluates a batch of inputs in one call, returns exact expectation values only (it does not sample shots), and has no limit on the number of qubits. See Capabilities.

#### `__init__(config=None, name='numpy')`

Build the backend from a device configuration.

Parameters:

| Name     | Type          | Description                                                   | Default   |
| -------- | ------------- | ------------------------------------------------------------- | --------- |
| `config` | `NumpyConfig` | The device settings. Default: None, which uses NumpyConfig(). | `None`    |
| `name`   | `str`         | The registry name, used in error messages. Default: "numpy".  | `'numpy'` |

#### `compile(circuit, observables)`

Prepare `circuit` for repeated evaluation of `observables`.

A `Circuit` calls it once and reuses the result for every call, so the expensive work belongs here.

Parameters:

| Name          | Type                   | Description                 | Default    |
| ------------- | ---------------------- | --------------------------- | ---------- |
| `circuit`     | `CircuitSpec`          | The circuit to prepare.     | *required* |
| `observables` | `Sequence[Observable]` | The observables to measure. | *required* |

Returns:

| Type            | Description                                      |
| --------------- | ------------------------------------------------ |
| `NumpyCompiled` | The prepared circuit, as a NumpyCompiled object. |
